diff --git a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 new file mode 100644 index 000000000000..a9660d75e900 Binary files /dev/null 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 new file mode 100644 index 000000000000..989a7f9c5980 Binary files /dev/null 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 new file mode 100644 index 000000000000..2c574da66ae9 Binary files /dev/null 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 new file mode 100644 index 000000000000..a630fe549db8 Binary files /dev/null and b/public/audio/bgm/mystery_encounter_weird_dream.mp3 differ diff --git a/public/battle-anims/encounter-dance.json b/public/battle-anims/encounter-dance.json new file mode 100644 index 000000000000..4be7f0756ee6 --- /dev/null +++ b/public/battle-anims/encounter-dance.json @@ -0,0 +1,951 @@ +{ + "id": 686, + "graphic": "PRAS- Dragon Dance", + "frames": [ + [ + { + "x": 4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 12, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -12, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 16, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -16, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 20, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -20, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 24, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -24, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 28, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -28, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 12, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -12, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 16, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -16, + "y": -0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 20, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -20, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 24, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -24, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 155, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + }, + { + "x": 28, + "y": 0, + "zoomX": 108, + "zoomY": 100, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + }, + { + "x": -28, + "y": -0.5, + "zoomX": 108, + "zoomY": 100, + "mirror": true, + "visible": true, + "blendType": 1, + "target": 2, + "graphicFrame": 0, + "opacity": 70, + "tone": [ + 0, + 0, + 0, + 255 + ], + "priority": 1, + "focus": 3 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -36, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -32, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -24, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -12, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": -4, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 1, + "focus": 2 + } + ] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Attract.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ], + "1": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Ally Switch.wav", + "volume": 80, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 4, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-magma-bg.json b/public/battle-anims/encounter-magma-bg.json new file mode 100644 index 000000000000..bb22f721d9a4 --- /dev/null +++ b/public/battle-anims/encounter-magma-bg.json @@ -0,0 +1,66 @@ +{ + "frames": [ + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRAS- Fire BG", + "bgX": 0, + "bgY": 0, + "opacity": 0, + "duration": 35, + "eventType": "AnimTimedAddBgEvent" + }, + { + "frameIndex": 0, + "resourceName": "", + "bgX": 0, + "bgY": 0, + "opacity": 255, + "duration": 12, + "eventType": "AnimTimedUpdateBgEvent" + } + ], + "25": [ + { + "frameIndex": 25, + "resourceName": "", + "bgX": 0, + "bgY": 0, + "opacity": 0, + "duration": 8, + "eventType": "AnimTimedUpdateBgEvent" + } + ] + }, + "position": 1, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-magma-spout.json b/public/battle-anims/encounter-magma-spout.json new file mode 100644 index 000000000000..21f3bec585f0 --- /dev/null +++ b/public/battle-anims/encounter-magma-spout.json @@ -0,0 +1,902 @@ +{ + "graphic": "PRAS- Magma Storm", + "frames": [ + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 120, + "y": -56, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -84, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 100, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 140, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 136, + "y": -92, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 108, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 152, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 116, + "y": -88, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 128, + "y": -62.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 136, + "y": -96, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 100, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 148, + "y": -66.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 108, + "y": -92, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 120, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 100, + "y": -76, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 136, + "y": -68, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 128, + "y": -94.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 100.5, + "y": -70, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 144, + "y": -66, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 126, + "y": -86.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 4, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 255, + "priority": 4, + "focus": 1 + } + ], + [ + { + "x": 101, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 130, + "priority": 4, + "focus": 1 + }, + { + "x": 152, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 130, + "priority": 4, + "focus": 1 + }, + { + "x": 124.5, + "y": -78.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 140, + "priority": 4, + "focus": 1 + } + ] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Magma Storm1.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ], + "8": [ + { + "frameIndex": 8, + "resourceName": "PRSFX- Magma Storm2.wav", + "volume": 100, + "pitch": 100, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 1, + "hue": 0 +} \ No newline at end of file diff --git a/public/battle-anims/encounter-smokescreen.json b/public/battle-anims/encounter-smokescreen.json new file mode 100644 index 000000000000..286cbe13a035 --- /dev/null +++ b/public/battle-anims/encounter-smokescreen.json @@ -0,0 +1,822 @@ +{ + "graphic": "PRAS- Smokescreen", + "frames": [ + [ + { + "x": 15.5, + "y": 12.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 21.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 17.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 13.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 21, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -16, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 5.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -20, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 15.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -24, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -11, + "y": -2.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 4.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -6.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -28, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 23, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -10.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -32, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": 1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 19, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -14.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 0, + "y": -36, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -11, + "y": -18.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": -12.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -11, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": 3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 100, + "priority": 4, + "focus": 2 + }, + { + "x": 11, + "y": -15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": -1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": -12.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 50, + "priority": 4, + "focus": 2 + }, + { + "x": 4.5, + "y": -5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 4.5, + "y": -17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": 4, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 4, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": 4, + "focus": 3 + } + ], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [], + [] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Haze.wav", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + }, + { + "frameIndex": 0, + "resourceName": "Explosion1.m4a", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 2, + "hue": 0 +} \ No newline at end of file diff --git a/public/images/items.json b/public/images/items.json index 442b93d657b5..779823d12932 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -4,8 +4,8 @@ "image": "items.png", "format": "RGBA8888", "size": { - "w": 426, - "h": 426 + "w": 431, + "h": 431 }, "scale": 1, "frames": [ @@ -93,6 +93,27 @@ "h": 28 } }, + { + "filename": "leaders_crest", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 2, + "y": 3, + "w": 29, + "h": 27 + }, + "frame": { + "x": 61, + "y": 0, + "w": 29, + "h": 27 + } + }, { "filename": "ribbon_gen2", "rotated": false, @@ -171,8 +192,8 @@ "h": 26 }, "frame": { - "x": 61, - "y": 0, + "x": 59, + "y": 27, "w": 27, "h": 26 } @@ -339,7 +360,7 @@ "h": 26 }, "frame": { - "x": 88, + "x": 90, "y": 0, "w": 24, "h": 26 @@ -387,6 +408,27 @@ "h": 28 } }, + { + "filename": "black_glasses", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 0, + "y": 414, + "w": 23, + "h": 17 + } + }, { "filename": "ability_charm", "rotated": false, @@ -402,7 +444,7 @@ "h": 26 }, "frame": { - "x": 112, + "x": 114, "y": 0, "w": 23, "h": 26 @@ -423,7 +465,7 @@ "h": 22 }, "frame": { - "x": 135, + "x": 137, "y": 0, "w": 27, "h": 22 @@ -444,7 +486,7 @@ "h": 21 }, "frame": { - "x": 162, + "x": 164, "y": 0, "w": 28, "h": 21 @@ -465,7 +507,7 @@ "h": 21 }, "frame": { - "x": 190, + "x": 192, "y": 0, "w": 28, "h": 21 @@ -486,7 +528,7 @@ "h": 21 }, "frame": { - "x": 218, + "x": 220, "y": 0, "w": 28, "h": 21 @@ -507,7 +549,7 @@ "h": 21 }, "frame": { - "x": 246, + "x": 248, "y": 0, "w": 28, "h": 21 @@ -528,7 +570,7 @@ "h": 21 }, "frame": { - "x": 274, + "x": 276, "y": 0, "w": 28, "h": 21 @@ -549,7 +591,7 @@ "h": 21 }, "frame": { - "x": 302, + "x": 304, "y": 0, "w": 28, "h": 21 @@ -570,7 +612,7 @@ "h": 20 }, "frame": { - "x": 330, + "x": 332, "y": 0, "w": 26, "h": 20 @@ -591,7 +633,7 @@ "h": 20 }, "frame": { - "x": 356, + "x": 358, "y": 0, "w": 26, "h": 20 @@ -612,14 +654,14 @@ "h": 20 }, "frame": { - "x": 382, + "x": 384, "y": 0, "w": 25, "h": 20 } }, { - "filename": "lock_capsule", + "filename": "ribbon_gen6", "rotated": false, "trimmed": true, "sourceSize": { @@ -627,16 +669,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 19, - "h": 22 + "x": 5, + "y": 2, + "w": 22, + "h": 28 }, "frame": { - "x": 407, + "x": 409, "y": 0, - "w": 19, - "h": 22 + "w": 22, + "h": 28 } }, { @@ -723,27 +765,6 @@ "h": 30 } }, - { - "filename": "ribbon_gen6", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 2, - "w": 22, - "h": 28 - }, - "frame": { - "x": 22, - "y": 209, - "w": 22, - "h": 28 - } - }, { "filename": "ribbon_gen8", "rotated": false, @@ -760,7 +781,7 @@ }, "frame": { "x": 22, - "y": 237, + "y": 209, "w": 22, "h": 28 } @@ -781,7 +802,7 @@ }, "frame": { "x": 22, - "y": 265, + "y": 237, "w": 22, "h": 25 } @@ -802,7 +823,7 @@ }, "frame": { "x": 22, - "y": 290, + "y": 262, "w": 23, "h": 24 } @@ -823,7 +844,7 @@ }, "frame": { "x": 22, - "y": 314, + "y": 286, "w": 24, "h": 24 } @@ -844,7 +865,7 @@ }, "frame": { "x": 22, - "y": 338, + "y": 310, "w": 24, "h": 24 } @@ -865,7 +886,7 @@ }, "frame": { "x": 22, - "y": 362, + "y": 334, "w": 24, "h": 24 } @@ -886,13 +907,13 @@ }, "frame": { "x": 22, - "y": 386, + "y": 358, "w": 24, "h": 24 } }, { - "filename": "mega_bracelet", + "filename": "earth_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -900,20 +921,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 16 + "x": 4, + "y": 4, + "w": 24, + "h": 24 }, "frame": { "x": 22, - "y": 410, - "w": 20, - "h": 16 + "y": 382, + "w": 24, + "h": 24 } }, { - "filename": "relic_band", + "filename": "fist_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -921,16 +942,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 9, - "w": 17, - "h": 16 + "x": 4, + "y": 4, + "w": 24, + "h": 24 }, "frame": { - "x": 42, - "y": 410, - "w": 17, - "h": 16 + "x": 23, + "y": 406, + "w": 24, + "h": 24 } }, { @@ -955,7 +976,7 @@ } }, { - "filename": "abomasite", + "filename": "mega_bracelet", "rotated": false, "trimmed": true, "sourceSize": { @@ -963,20 +984,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, + "w": 20, "h": 16 }, "frame": { "x": 28, "y": 70, - "w": 16, + "w": 20, "h": 16 } }, { - "filename": "absolite", + "filename": "choice_specs", "rotated": false, "trimmed": true, "sourceSize": { @@ -984,20 +1005,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 4, "y": 8, - "w": 16, - "h": 16 + "w": 24, + "h": 18 }, "frame": { - "x": 44, - "y": 70, - "w": 16, - "h": 16 + "x": 59, + "y": 53, + "w": 24, + "h": 18 } }, { - "filename": "catching_charm", + "filename": "calcium", "rotated": false, "trimmed": true, "sourceSize": { @@ -1005,20 +1026,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 8, "y": 4, - "w": 21, + "w": 16, "h": 24 }, "frame": { "x": 39, "y": 86, - "w": 21, + "w": 16, "h": 24 } }, { - "filename": "earth_plate", + "filename": "carbos", "rotated": false, "trimmed": true, "sourceSize": { @@ -1026,20 +1047,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 8, "y": 4, - "w": 24, + "w": 16, "h": 24 }, "frame": { "x": 39, "y": 110, - "w": 24, + "w": 16, "h": 24 } }, { - "filename": "fist_plate", + "filename": "catching_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -1047,15 +1068,15 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 5, "y": 4, - "w": 24, + "w": 21, "h": 24 }, "frame": { "x": 39, "y": 134, - "w": 24, + "w": 21, "h": 24 } }, @@ -1158,7 +1179,7 @@ "h": 24 }, "frame": { - "x": 44, + "x": 45, "y": 254, "w": 24, "h": 24 @@ -1179,7 +1200,7 @@ "h": 24 }, "frame": { - "x": 45, + "x": 46, "y": 278, "w": 24, "h": 24 @@ -1270,7 +1291,7 @@ } }, { - "filename": "ability_capsule", + "filename": "kings_rock", "rotated": false, "trimmed": true, "sourceSize": { @@ -1278,20 +1299,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 9, - "w": 24, - "h": 14 + "x": 5, + "y": 4, + "w": 23, + "h": 24 }, "frame": { - "x": 135, - "y": 22, - "w": 24, - "h": 14 + "x": 47, + "y": 398, + "w": 23, + "h": 24 } }, { - "filename": "calcium", + "filename": "silver_powder", "rotated": false, "trimmed": true, "sourceSize": { @@ -1299,20 +1320,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 + "x": 4, + "y": 11, + "w": 24, + "h": 15 }, "frame": { - "x": 59, - "y": 27, - "w": 16, - "h": 24 + "x": 48, + "y": 71, + "w": 24, + "h": 15 } }, { - "filename": "lucky_punch_master", + "filename": "elixir", "rotated": false, "trimmed": true, "sourceSize": { @@ -1320,20 +1341,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 7, "y": 4, - "w": 24, + "w": 18, "h": 24 }, "frame": { - "x": 75, - "y": 26, - "w": 24, + "x": 55, + "y": 86, + "w": 18, "h": 24 } }, { - "filename": "lucky_punch_ultra", + "filename": "ether", "rotated": false, "trimmed": true, "sourceSize": { @@ -1341,20 +1362,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 7, "y": 4, - "w": 24, + "w": 18, "h": 24 }, "frame": { - "x": 99, - "y": 26, - "w": 24, + "x": 55, + "y": 110, + "w": 18, "h": 24 } }, { - "filename": "revive", + "filename": "full_restore", "rotated": false, "trimmed": true, "sourceSize": { @@ -1362,20 +1383,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 12, - "h": 17 + "x": 7, + "y": 4, + "w": 18, + "h": 24 }, "frame": { - "x": 123, - "y": 26, - "w": 12, - "h": 17 + "x": 60, + "y": 134, + "w": 18, + "h": 24 } }, { - "filename": "big_mushroom", + "filename": "hp_up", "rotated": false, "trimmed": true, "sourceSize": { @@ -1383,20 +1404,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 19 - }, - "frame": { - "x": 59, - "y": 51, - "w": 19, - "h": 19 + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 63, + "y": 158, + "w": 16, + "h": 24 } }, { - "filename": "clefairy_doll", + "filename": "iron", "rotated": false, "trimmed": true, "sourceSize": { @@ -1404,20 +1425,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 78, - "y": 50, - "w": 24, - "h": 23 + "x": 63, + "y": 182, + "w": 16, + "h": 24 } }, { - "filename": "elixir", + "filename": "lucky_punch_master", "rotated": false, "trimmed": true, "sourceSize": { @@ -1425,20 +1446,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 4, "y": 4, - "w": 18, + "w": 24, "h": 24 }, "frame": { - "x": 60, - "y": 70, - "w": 18, + "x": 68, + "y": 206, + "w": 24, "h": 24 } }, { - "filename": "coin_case", + "filename": "lucky_punch_ultra", "rotated": false, "trimmed": true, "sourceSize": { @@ -1447,19 +1468,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 5, + "y": 4, "w": 24, - "h": 23 + "h": 24 }, "frame": { - "x": 78, - "y": 73, + "x": 68, + "y": 230, "w": 24, - "h": 23 + "h": 24 } }, { - "filename": "kings_rock", + "filename": "lustrous_globe", "rotated": false, "trimmed": true, "sourceSize": { @@ -1467,20 +1488,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 4, "y": 4, - "w": 23, + "w": 24, "h": 24 }, "frame": { - "x": 102, - "y": 50, - "w": 23, + "x": 69, + "y": 254, + "w": 24, "h": 24 } }, { - "filename": "berry_pouch", + "filename": "meadow_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1489,19 +1510,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 5, - "w": 23, - "h": 23 + "y": 4, + "w": 24, + "h": 24 }, "frame": { - "x": 102, - "y": 74, - "w": 23, - "h": 23 + "x": 70, + "y": 278, + "w": 24, + "h": 24 } }, { - "filename": "aerodactylite", + "filename": "mind_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1509,20 +1530,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 4, + "y": 4, + "w": 24, + "h": 24 }, "frame": { - "x": 60, - "y": 94, - "w": 16, - "h": 16 + "x": 70, + "y": 302, + "w": 24, + "h": 24 } }, { - "filename": "carbos", + "filename": "muscle_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -1530,20 +1551,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 4, "y": 4, - "w": 16, + "w": 24, "h": 24 }, "frame": { - "x": 63, - "y": 110, - "w": 16, + "x": 70, + "y": 326, + "w": 24, "h": 24 } }, { - "filename": "ether", + "filename": "pixie_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1551,20 +1572,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 4, "y": 4, - "w": 18, + "w": 24, "h": 24 }, "frame": { - "x": 63, - "y": 134, - "w": 18, + "x": 70, + "y": 350, + "w": 24, "h": 24 } }, { - "filename": "full_restore", + "filename": "salac_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -1572,20 +1593,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 4, "y": 4, - "w": 18, + "w": 24, "h": 24 }, "frame": { - "x": 63, - "y": 158, - "w": 18, + "x": 70, + "y": 374, + "w": 24, "h": 24 } }, { - "filename": "lustrous_globe", + "filename": "scanner", "rotated": false, "trimmed": true, "sourceSize": { @@ -1599,14 +1620,14 @@ "h": 24 }, "frame": { - "x": 63, - "y": 182, + "x": 70, + "y": 398, "w": 24, "h": 24 } }, { - "filename": "max_revive", + "filename": "ability_capsule", "rotated": false, "trimmed": true, "sourceSize": { @@ -1614,20 +1635,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 24 + "x": 4, + "y": 9, + "w": 24, + "h": 14 }, "frame": { - "x": 68, - "y": 206, - "w": 22, - "h": 24 + "x": 137, + "y": 22, + "w": 24, + "h": 14 } }, { - "filename": "meadow_plate", + "filename": "lure", "rotated": false, "trimmed": true, "sourceSize": { @@ -1635,20 +1656,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 8, "y": 4, - "w": 24, + "w": 17, "h": 24 }, "frame": { - "x": 68, - "y": 230, - "w": 24, + "x": 86, + "y": 27, + "w": 17, "h": 24 } }, { - "filename": "mind_plate", + "filename": "silk_scarf", "rotated": false, "trimmed": true, "sourceSize": { @@ -1662,14 +1683,14 @@ "h": 24 }, "frame": { - "x": 68, - "y": 254, + "x": 103, + "y": 26, "w": 24, "h": 24 } }, { - "filename": "muscle_band", + "filename": "clefairy_doll", "rotated": false, "trimmed": true, "sourceSize": { @@ -1678,19 +1699,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 23 }, "frame": { - "x": 69, - "y": 278, + "x": 127, + "y": 36, "w": 24, - "h": 24 + "h": 23 } }, { - "filename": "pixie_plate", + "filename": "coin_case", "rotated": false, "trimmed": true, "sourceSize": { @@ -1699,19 +1720,40 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 23 }, "frame": { - "x": 70, - "y": 302, + "x": 103, + "y": 50, "w": 24, - "h": 24 + "h": 23 } }, { - "filename": "salac_berry", + "filename": "big_nugget", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 83, + "y": 53, + "w": 20, + "h": 20 + } + }, + { + "filename": "dragon_scale", "rotated": false, "trimmed": true, "sourceSize": { @@ -1720,19 +1762,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 8, "w": 24, - "h": 24 + "h": 18 }, "frame": { - "x": 70, - "y": 326, + "x": 127, + "y": 59, "w": 24, - "h": 24 + "h": 18 } }, { - "filename": "scanner", + "filename": "max_elixir", "rotated": false, "trimmed": true, "sourceSize": { @@ -1740,20 +1782,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 7, "y": 4, - "w": 24, + "w": 18, "h": 24 }, "frame": { - "x": 70, - "y": 350, - "w": 24, + "x": 151, + "y": 36, + "w": 18, "h": 24 } }, { - "filename": "silk_scarf", + "filename": "adamant_crystal", "rotated": false, "trimmed": true, "sourceSize": { @@ -1762,15 +1804,15 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, - "w": 24, - "h": 24 + "y": 6, + "w": 23, + "h": 21 }, "frame": { - "x": 70, - "y": 374, - "w": 24, - "h": 24 + "x": 151, + "y": 60, + "w": 23, + "h": 21 } }, { @@ -1788,14 +1830,14 @@ "h": 24 }, "frame": { - "x": 59, - "y": 398, + "x": 169, + "y": 21, "w": 24, "h": 24 } }, { - "filename": "hp_up", + "filename": "splash_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1803,20 +1845,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 4, "y": 4, - "w": 16, + "w": 24, "h": 24 }, "frame": { - "x": 83, - "y": 398, - "w": 16, + "x": 193, + "y": 21, + "w": 24, "h": 24 } }, { - "filename": "reveal_glass", + "filename": "spooky_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1826,18 +1868,18 @@ "spriteSourceSize": { "x": 4, "y": 4, - "w": 23, + "w": 24, "h": 24 }, "frame": { - "x": 79, - "y": 96, - "w": 23, + "x": 217, + "y": 21, + "w": 24, "h": 24 } }, { - "filename": "dynamax_band", + "filename": "stone_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1847,18 +1889,18 @@ "spriteSourceSize": { "x": 4, "y": 4, - "w": 23, - "h": 23 + "w": 24, + "h": 24 }, "frame": { - "x": 102, - "y": 97, - "w": 23, - "h": 23 + "x": 241, + "y": 21, + "w": 24, + "h": 24 } }, { - "filename": "splash_plate", + "filename": "sun_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -1872,14 +1914,14 @@ "h": 24 }, "frame": { - "x": 81, - "y": 120, + "x": 265, + "y": 21, "w": 24, "h": 24 } }, { - "filename": "spooky_plate", + "filename": "toxic_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1893,14 +1935,14 @@ "h": 24 }, "frame": { - "x": 81, - "y": 144, + "x": 289, + "y": 21, "w": 24, "h": 24 } }, { - "filename": "oval_charm", + "filename": "max_revive", "rotated": false, "trimmed": true, "sourceSize": { @@ -1908,20 +1950,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 5, "y": 4, - "w": 21, + "w": 22, "h": 24 }, "frame": { - "x": 105, - "y": 120, - "w": 21, + "x": 313, + "y": 21, + "w": 22, "h": 24 } }, { - "filename": "shiny_charm", + "filename": "zap_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1929,20 +1971,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 4, "y": 4, - "w": 21, + "w": 24, "h": 24 }, "frame": { - "x": 105, - "y": 144, - "w": 21, + "x": 335, + "y": 20, + "w": 24, "h": 24 } }, { - "filename": "stone_plate", + "filename": "expert_belt", "rotated": false, "trimmed": true, "sourceSize": { @@ -1953,17 +1995,17 @@ "x": 4, "y": 4, "w": 24, - "h": 24 + "h": 23 }, "frame": { - "x": 87, - "y": 168, + "x": 359, + "y": 20, "w": 24, - "h": 24 + "h": 23 } }, { - "filename": "iron", + "filename": "hearthflame_mask", "rotated": false, "trimmed": true, "sourceSize": { @@ -1971,20 +2013,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 4, "y": 4, - "w": 16, - "h": 24 + "w": 24, + "h": 23 }, "frame": { - "x": 111, - "y": 168, - "w": 16, - "h": 24 + "x": 383, + "y": 20, + "w": 24, + "h": 23 } }, { - "filename": "sun_stone", + "filename": "leppa_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -1993,19 +2035,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 23 }, "frame": { - "x": 90, - "y": 192, + "x": 407, + "y": 28, "w": 24, - "h": 24 + "h": 23 } }, { - "filename": "lure", + "filename": "candy_overlay", "rotated": false, "trimmed": true, "sourceSize": { @@ -2014,19 +2056,19 @@ }, "spriteSourceSize": { "x": 8, - "y": 4, - "w": 17, - "h": 24 + "y": 12, + "w": 16, + "h": 15 }, "frame": { - "x": 114, - "y": 192, - "w": 17, - "h": 24 + "x": 169, + "y": 45, + "w": 16, + "h": 15 } }, { - "filename": "toxic_plate", + "filename": "exp_balance", "rotated": false, "trimmed": true, "sourceSize": { @@ -2035,19 +2077,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 22 }, "frame": { - "x": 92, - "y": 216, + "x": 185, + "y": 45, "w": 24, - "h": 24 + "h": 22 } }, { - "filename": "zap_plate", + "filename": "exp_share", "rotated": false, "trimmed": true, "sourceSize": { @@ -2056,19 +2098,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, - "h": 24 + "h": 22 }, "frame": { - "x": 92, - "y": 240, + "x": 209, + "y": 45, "w": 24, - "h": 24 + "h": 22 } }, { - "filename": "max_elixir", + "filename": "peat_block", "rotated": false, "trimmed": true, "sourceSize": { @@ -2076,20 +2118,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 + "x": 4, + "y": 5, + "w": 24, + "h": 22 }, "frame": { - "x": 116, - "y": 216, - "w": 18, - "h": 24 + "x": 233, + "y": 45, + "w": 24, + "h": 22 } }, { - "filename": "max_ether", + "filename": "scope_lens", "rotated": false, "trimmed": true, "sourceSize": { @@ -2097,20 +2139,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 + "x": 4, + "y": 5, + "w": 24, + "h": 23 }, "frame": { - "x": 116, - "y": 240, - "w": 18, - "h": 24 + "x": 257, + "y": 45, + "w": 24, + "h": 23 } }, { - "filename": "expert_belt", + "filename": "twisted_spoon", "rotated": false, "trimmed": true, "sourceSize": { @@ -2119,17 +2161,38 @@ }, "spriteSourceSize": { "x": 4, - "y": 4, + "y": 5, "w": 24, "h": 23 }, "frame": { - "x": 93, - "y": 264, + "x": 281, + "y": 45, "w": 24, "h": 23 } }, + { + "filename": "berry_pouch", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 305, + "y": 45, + "w": 23, + "h": 23 + } + }, { "filename": "black_belt", "rotated": false, @@ -2145,14 +2208,14 @@ "h": 23 }, "frame": { - "x": 117, - "y": 264, + "x": 328, + "y": 45, "w": 22, "h": 23 } }, { - "filename": "silver_powder", + "filename": "max_ether", "rotated": false, "trimmed": true, "sourceSize": { @@ -2160,20 +2223,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 11, - "w": 24, - "h": 15 + "x": 7, + "y": 4, + "w": 18, + "h": 24 }, "frame": { - "x": 93, - "y": 287, - "w": 24, - "h": 15 + "x": 350, + "y": 44, + "w": 18, + "h": 24 } }, { - "filename": "griseous_core", + "filename": "reveal_glass", "rotated": false, "trimmed": true, "sourceSize": { @@ -2181,20 +2244,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, + "x": 4, + "y": 4, "w": 23, - "h": 23 + "h": 24 }, "frame": { - "x": 94, - "y": 302, + "x": 368, + "y": 43, "w": 23, - "h": 23 + "h": 24 } }, { - "filename": "hearthflame_mask", + "filename": "max_repel", "rotated": false, "trimmed": true, "sourceSize": { @@ -2202,20 +2265,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 8, "y": 4, - "w": 24, - "h": 23 + "w": 16, + "h": 24 }, "frame": { - "x": 94, - "y": 325, - "w": 24, - "h": 23 + "x": 391, + "y": 43, + "w": 16, + "h": 24 } }, { - "filename": "leppa_berry", + "filename": "golden_net", "rotated": false, "trimmed": true, "sourceSize": { @@ -2226,17 +2289,17 @@ "x": 4, "y": 5, "w": 24, - "h": 23 + "h": 21 }, "frame": { - "x": 94, - "y": 348, + "x": 407, + "y": 51, "w": 24, - "h": 23 + "h": 21 } }, { - "filename": "scope_lens", + "filename": "icy_reins_of_unity", "rotated": false, "trimmed": true, "sourceSize": { @@ -2245,19 +2308,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 5, + "y": 7, "w": 24, - "h": 23 + "h": 20 }, "frame": { - "x": 94, - "y": 371, + "x": 174, + "y": 67, "w": 24, - "h": 23 + "h": 20 } }, { - "filename": "bug_tera_shard", + "filename": "metal_powder", "rotated": false, "trimmed": true, "sourceSize": { @@ -2265,20 +2328,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 4, + "y": 6, + "w": 24, + "h": 20 }, "frame": { - "x": 117, - "y": 287, - "w": 22, - "h": 23 + "x": 198, + "y": 67, + "w": 24, + "h": 20 } }, { - "filename": "red_orb", + "filename": "quick_powder", "rotated": false, "trimmed": true, "sourceSize": { @@ -2286,20 +2349,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 20, - "h": 24 + "x": 4, + "y": 6, + "w": 24, + "h": 20 }, "frame": { - "x": 99, - "y": 394, - "w": 20, - "h": 24 + "x": 222, + "y": 67, + "w": 24, + "h": 20 } }, { - "filename": "candy_overlay", + "filename": "rusted_shield", "rotated": false, "trimmed": true, "sourceSize": { @@ -2307,20 +2370,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 12, - "w": 16, - "h": 15 + "x": 4, + "y": 6, + "w": 24, + "h": 20 }, "frame": { - "x": 117, - "y": 310, - "w": 16, - "h": 15 + "x": 246, + "y": 68, + "w": 24, + "h": 20 } }, { - "filename": "max_lure", + "filename": "sacred_ash", "rotated": false, "trimmed": true, "sourceSize": { @@ -2328,20 +2391,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 17, - "h": 24 + "x": 4, + "y": 7, + "w": 24, + "h": 20 }, "frame": { - "x": 118, - "y": 325, - "w": 17, - "h": 24 + "x": 270, + "y": 68, + "w": 24, + "h": 20 } }, { - "filename": "max_potion", + "filename": "shadow_reins_of_unity", "rotated": false, "trimmed": true, "sourceSize": { @@ -2349,20 +2412,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 + "x": 4, + "y": 7, + "w": 24, + "h": 20 }, "frame": { - "x": 118, - "y": 349, - "w": 18, - "h": 24 + "x": 294, + "y": 68, + "w": 24, + "h": 20 } }, { - "filename": "adamant_crystal", + "filename": "soft_sand", "rotated": false, "trimmed": true, "sourceSize": { @@ -2371,19 +2434,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 6, - "w": 23, - "h": 21 + "y": 7, + "w": 24, + "h": 20 }, "frame": { - "x": 118, - "y": 373, - "w": 23, - "h": 21 + "x": 318, + "y": 68, + "w": 24, + "h": 20 } }, { - "filename": "dark_tera_shard", + "filename": "amulet_coin", "rotated": false, "trimmed": true, "sourceSize": { @@ -2392,19 +2455,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 4, - "w": 22, - "h": 23 + "y": 5, + "w": 23, + "h": 21 }, "frame": { - "x": 119, - "y": 394, - "w": 22, - "h": 23 + "x": 342, + "y": 68, + "w": 23, + "h": 21 } }, { - "filename": "choice_specs", + "filename": "auspicious_armor", "rotated": false, "trimmed": true, "sourceSize": { @@ -2413,19 +2476,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 8, - "w": 24, - "h": 18 + "y": 5, + "w": 23, + "h": 21 }, "frame": { - "x": 135, - "y": 36, - "w": 24, - "h": 18 + "x": 368, + "y": 67, + "w": 23, + "h": 21 } }, { - "filename": "twisted_spoon", + "filename": "pp_max", "rotated": false, "trimmed": true, "sourceSize": { @@ -2433,20 +2496,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 125, - "y": 54, - "w": 24, - "h": 23 + "x": 391, + "y": 67, + "w": 16, + "h": 24 } }, { - "filename": "exp_balance", + "filename": "binding_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -2454,20 +2517,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 + "x": 5, + "y": 6, + "w": 23, + "h": 20 }, "frame": { - "x": 125, - "y": 77, - "w": 24, - "h": 22 + "x": 407, + "y": 72, + "w": 23, + "h": 20 } }, { - "filename": "amulet_coin", + "filename": "max_lure", "rotated": false, "trimmed": true, "sourceSize": { @@ -2475,20 +2538,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 23, - "h": 21 + "x": 8, + "y": 4, + "w": 17, + "h": 24 }, "frame": { - "x": 125, - "y": 99, - "w": 23, - "h": 21 + "x": 73, + "y": 87, + "w": 17, + "h": 24 } }, { - "filename": "dragon_tera_shard", + "filename": "bug_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2502,14 +2565,14 @@ "h": 23 }, "frame": { - "x": 126, - "y": 120, + "x": 73, + "y": 111, "w": 22, "h": 23 } }, { - "filename": "electric_tera_shard", + "filename": "max_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -2517,20 +2580,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 7, "y": 4, - "w": 22, - "h": 23 + "w": 18, + "h": 24 }, "frame": { - "x": 126, - "y": 143, - "w": 22, - "h": 23 + "x": 78, + "y": 134, + "w": 18, + "h": 24 } }, { - "filename": "dragon_fang", + "filename": "oval_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -2538,20 +2601,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, + "x": 6, + "y": 4, "w": 21, - "h": 23 + "h": 24 }, "frame": { - "x": 127, - "y": 166, + "x": 79, + "y": 158, "w": 21, - "h": 23 + "h": 24 } }, { - "filename": "super_lure", + "filename": "shiny_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -2559,20 +2622,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 4, - "w": 17, + "w": 21, "h": 24 }, "frame": { - "x": 131, - "y": 189, - "w": 17, + "x": 79, + "y": 182, + "w": 21, "h": 24 } }, { - "filename": "max_repel", + "filename": "dynamax_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -2580,20 +2643,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 4, "y": 4, - "w": 16, - "h": 24 + "w": 23, + "h": 23 }, "frame": { - "x": 134, - "y": 213, - "w": 16, - "h": 24 + "x": 90, + "y": 73, + "w": 23, + "h": 23 } }, { - "filename": "pp_max", + "filename": "eviolite", "rotated": false, "trimmed": true, "sourceSize": { @@ -2602,19 +2665,19 @@ }, "spriteSourceSize": { "x": 8, - "y": 4, - "w": 16, - "h": 24 + "y": 8, + "w": 15, + "h": 15 }, "frame": { - "x": 134, - "y": 237, - "w": 16, - "h": 24 + "x": 90, + "y": 96, + "w": 15, + "h": 15 } }, { - "filename": "pp_up", + "filename": "dark_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2622,20 +2685,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 4, - "w": 16, - "h": 24 + "w": 22, + "h": 23 }, "frame": { - "x": 149, - "y": 54, - "w": 16, - "h": 24 + "x": 95, + "y": 111, + "w": 22, + "h": 23 } }, { - "filename": "auspicious_armor", + "filename": "red_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -2643,20 +2706,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 21 + "x": 6, + "y": 4, + "w": 20, + "h": 24 }, "frame": { - "x": 149, - "y": 78, - "w": 23, - "h": 21 + "x": 96, + "y": 134, + "w": 20, + "h": 24 } }, { - "filename": "exp_share", + "filename": "pp_up", "rotated": false, "trimmed": true, "sourceSize": { @@ -2664,20 +2727,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 148, - "y": 99, - "w": 24, - "h": 22 + "x": 100, + "y": 158, + "w": 16, + "h": 24 } }, { - "filename": "leek", + "filename": "protein", "rotated": false, "trimmed": true, "sourceSize": { @@ -2685,20 +2748,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 148, - "y": 121, - "w": 23, - "h": 23 + "x": 100, + "y": 182, + "w": 16, + "h": 24 } }, { - "filename": "rare_candy", + "filename": "griseous_core", "rotated": false, "trimmed": true, "sourceSize": { @@ -2706,20 +2769,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 5, "y": 5, "w": 23, "h": 23 }, "frame": { - "x": 148, - "y": 144, + "x": 92, + "y": 206, "w": 23, "h": 23 } }, { - "filename": "rarer_candy", + "filename": "leek", "rotated": false, "trimmed": true, "sourceSize": { @@ -2733,14 +2796,14 @@ "h": 23 }, "frame": { - "x": 148, - "y": 167, + "x": 92, + "y": 229, "w": 23, "h": 23 } }, { - "filename": "fairy_tera_shard", + "filename": "dragon_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2754,14 +2817,14 @@ "h": 23 }, "frame": { - "x": 148, - "y": 190, + "x": 93, + "y": 252, "w": 22, "h": 23 } }, { - "filename": "fighting_tera_shard", + "filename": "dragon_fang", "rotated": false, "trimmed": true, "sourceSize": { @@ -2769,20 +2832,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, + "x": 5, + "y": 5, + "w": 21, "h": 23 }, "frame": { - "x": 150, - "y": 213, - "w": 22, + "x": 94, + "y": 275, + "w": 21, "h": 23 } }, { - "filename": "fire_stone", + "filename": "electric_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2790,20 +2853,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, + "x": 6, + "y": 4, "w": 22, "h": 23 }, "frame": { - "x": 150, - "y": 236, + "x": 94, + "y": 298, "w": 22, "h": 23 } }, { - "filename": "protein", + "filename": "fairy_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2811,20 +2874,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 4, - "w": 16, - "h": 24 + "w": 22, + "h": 23 }, "frame": { - "x": 139, - "y": 261, - "w": 16, - "h": 24 + "x": 94, + "y": 321, + "w": 22, + "h": 23 } }, { - "filename": "repel", + "filename": "fighting_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2832,20 +2895,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 4, - "w": 16, - "h": 24 + "w": 22, + "h": 23 }, "frame": { - "x": 139, - "y": 285, - "w": 16, - "h": 24 + "x": 94, + "y": 344, + "w": 22, + "h": 23 } }, { - "filename": "fire_tera_shard", + "filename": "fire_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -2853,20 +2916,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, + "x": 5, + "y": 5, "w": 22, "h": 23 }, "frame": { - "x": 155, - "y": 259, + "x": 94, + "y": 367, "w": 22, "h": 23 } }, { - "filename": "flying_tera_shard", + "filename": "fire_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -2880,14 +2943,14 @@ "h": 23 }, "frame": { - "x": 155, - "y": 282, + "x": 94, + "y": 390, "w": 22, "h": 23 } }, { - "filename": "super_repel", + "filename": "relic_crown", "rotated": false, "trimmed": true, "sourceSize": { @@ -2895,20 +2958,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 + "x": 4, + "y": 7, + "w": 23, + "h": 18 }, "frame": { - "x": 159, - "y": 22, - "w": 16, - "h": 24 + "x": 94, + "y": 413, + "w": 23, + "h": 18 } }, { - "filename": "peat_block", + "filename": "prism_scale", "rotated": false, "trimmed": true, "sourceSize": { @@ -2916,20 +2979,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 + "x": 9, + "y": 8, + "w": 15, + "h": 15 }, "frame": { - "x": 175, - "y": 21, - "w": 24, - "h": 22 + "x": 105, + "y": 96, + "w": 15, + "h": 15 } }, { - "filename": "healing_charm", + "filename": "coupon", "rotated": false, "trimmed": true, "sourceSize": { @@ -2937,20 +3000,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, + "x": 4, + "y": 7, "w": 23, - "h": 22 + "h": 19 }, "frame": { - "x": 199, - "y": 21, + "x": 113, + "y": 77, "w": 23, - "h": 22 + "h": 19 } }, { - "filename": "rusted_sword", + "filename": "full_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 4, + "w": 15, + "h": 23 + }, + "frame": { + "x": 136, + "y": 77, + "w": 15, + "h": 23 + } + }, + { + "filename": "golden_mystic_ticket", "rotated": false, "trimmed": true, "sourceSize": { @@ -2959,19 +3043,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 5, + "y": 7, "w": 23, - "h": 22 + "h": 19 }, "frame": { - "x": 222, - "y": 21, + "x": 151, + "y": 81, "w": 23, - "h": 22 + "h": 19 } }, { - "filename": "bug_memory", + "filename": "burn_drive", "rotated": false, "trimmed": true, "sourceSize": { @@ -2979,20 +3063,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 4, + "y": 8, + "w": 23, + "h": 17 }, "frame": { - "x": 245, - "y": 21, - "w": 22, - "h": 22 + "x": 174, + "y": 87, + "w": 23, + "h": 17 } }, { - "filename": "charcoal", + "filename": "chill_drive", "rotated": false, "trimmed": true, "sourceSize": { @@ -3000,20 +3084,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 4, + "y": 8, + "w": 23, + "h": 17 }, "frame": { - "x": 267, - "y": 21, - "w": 22, - "h": 22 + "x": 197, + "y": 87, + "w": 23, + "h": 17 } }, { - "filename": "dark_memory", + "filename": "douse_drive", "rotated": false, "trimmed": true, "sourceSize": { @@ -3021,20 +3105,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 4, + "y": 8, + "w": 23, + "h": 17 }, "frame": { - "x": 289, - "y": 21, - "w": 22, - "h": 22 + "x": 220, + "y": 87, + "w": 23, + "h": 17 } }, { - "filename": "dire_hit", + "filename": "healing_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -3044,18 +3128,18 @@ "spriteSourceSize": { "x": 5, "y": 5, - "w": 22, + "w": 23, "h": 22 }, "frame": { - "x": 311, - "y": 21, - "w": 22, + "x": 243, + "y": 88, + "w": 23, "h": 22 } }, { - "filename": "focus_sash", + "filename": "macho_brace", "rotated": false, "trimmed": true, "sourceSize": { @@ -3063,20 +3147,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, + "x": 4, + "y": 5, + "w": 23, "h": 23 }, "frame": { - "x": 333, - "y": 20, - "w": 22, + "x": 266, + "y": 88, + "w": 23, "h": 23 } }, { - "filename": "ghost_tera_shard", + "filename": "rare_candy", "rotated": false, "trimmed": true, "sourceSize": { @@ -3084,20 +3168,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, + "x": 4, + "y": 5, + "w": 23, "h": 23 }, "frame": { - "x": 355, - "y": 20, - "w": 22, + "x": 289, + "y": 88, + "w": 23, "h": 23 } }, { - "filename": "grass_tera_shard", + "filename": "rarer_candy", "rotated": false, "trimmed": true, "sourceSize": { @@ -3105,20 +3189,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, + "x": 4, + "y": 5, + "w": 23, "h": 23 }, "frame": { - "x": 377, - "y": 20, - "w": 22, + "x": 312, + "y": 88, + "w": 23, "h": 23 } }, { - "filename": "icy_reins_of_unity", + "filename": "rusted_sword", "rotated": false, "trimmed": true, "sourceSize": { @@ -3127,19 +3211,19 @@ }, "spriteSourceSize": { "x": 4, - "y": 7, - "w": 24, - "h": 20 + "y": 5, + "w": 23, + "h": 22 }, "frame": { - "x": 399, - "y": 22, - "w": 24, - "h": 20 + "x": 335, + "y": 89, + "w": 23, + "h": 22 } }, { - "filename": "dragon_scale", + "filename": "abomasite", "rotated": false, "trimmed": true, "sourceSize": { @@ -3147,20 +3231,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 8, "y": 8, - "w": 24, - "h": 18 + "w": 16, + "h": 16 }, "frame": { - "x": 175, - "y": 43, - "w": 24, - "h": 18 + "x": 120, + "y": 96, + "w": 16, + "h": 16 } }, { - "filename": "metal_powder", + "filename": "bug_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -3168,20 +3252,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 199, - "y": 43, - "w": 24, - "h": 20 + "x": 117, + "y": 112, + "w": 22, + "h": 22 } }, { - "filename": "quick_powder", + "filename": "flying_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3189,20 +3273,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 223, - "y": 43, - "w": 24, - "h": 20 + "x": 116, + "y": 134, + "w": 22, + "h": 23 } }, { - "filename": "rusted_shield", + "filename": "focus_sash", "rotated": false, "trimmed": true, "sourceSize": { @@ -3210,20 +3294,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 + "x": 5, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 247, - "y": 43, - "w": 24, - "h": 20 + "x": 116, + "y": 157, + "w": 22, + "h": 23 } }, { - "filename": "sacred_ash", + "filename": "ghost_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3231,20 +3315,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 271, - "y": 43, - "w": 24, - "h": 20 + "x": 116, + "y": 180, + "w": 22, + "h": 23 } }, { - "filename": "shadow_reins_of_unity", + "filename": "grass_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3252,20 +3336,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 295, - "y": 43, + "x": 139, + "y": 100, + "w": 22, + "h": 23 + } + }, + { + "filename": "berry_juice", + "rotated": false, + "trimmed": true, + "sourceSize": { "w": 24, - "h": 20 + "h": 23 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 22, + "h": 21 + }, + "frame": { + "x": 139, + "y": 123, + "w": 22, + "h": 21 } }, { - "filename": "soft_sand", + "filename": "ground_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3273,20 +3378,62 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 319, - "y": 43, + "x": 138, + "y": 144, + "w": 22, + "h": 23 + } + }, + { + "filename": "ice_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 138, + "y": 167, + "w": 22, + "h": 23 + } + }, + { + "filename": "black_sludge", + "rotated": false, + "trimmed": true, + "sourceSize": { "w": 24, - "h": 20 + "h": 24 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 22, + "h": 19 + }, + "frame": { + "x": 138, + "y": 190, + "w": 22, + "h": 19 } }, { - "filename": "binding_band", + "filename": "never_melt_ice", "rotated": false, "trimmed": true, "sourceSize": { @@ -3295,19 +3442,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 6, - "w": 23, - "h": 20 + "y": 5, + "w": 22, + "h": 23 }, "frame": { - "x": 343, - "y": 43, - "w": 23, - "h": 20 + "x": 161, + "y": 104, + "w": 22, + "h": 23 } }, { - "filename": "moon_stone", + "filename": "normal_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3315,20 +3462,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 366, - "y": 43, - "w": 23, - "h": 21 + "x": 183, + "y": 104, + "w": 22, + "h": 23 } }, { - "filename": "black_glasses", + "filename": "petaya_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -3336,20 +3483,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 + "x": 5, + "y": 5, + "w": 22, + "h": 23 }, "frame": { - "x": 165, - "y": 61, - "w": 23, - "h": 17 + "x": 205, + "y": 104, + "w": 22, + "h": 23 } }, { - "filename": "unknown", + "filename": "repel", "rotated": false, "trimmed": true, "sourceSize": { @@ -3363,14 +3510,14 @@ "h": 24 }, "frame": { - "x": 172, - "y": 78, + "x": 227, + "y": 104, "w": 16, "h": 24 } }, { - "filename": "apicot_berry", + "filename": "moon_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -3378,20 +3525,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 4, "y": 6, - "w": 19, - "h": 20 + "w": 23, + "h": 21 }, "frame": { - "x": 172, - "y": 102, - "w": 19, - "h": 20 + "x": 243, + "y": 110, + "w": 23, + "h": 21 } }, { - "filename": "ground_tera_shard", + "filename": "n_lunarizer", "rotated": false, "trimmed": true, "sourceSize": { @@ -3399,20 +3546,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 4, + "y": 6, + "w": 23, + "h": 21 }, "frame": { - "x": 171, - "y": 122, - "w": 22, - "h": 23 + "x": 266, + "y": 111, + "w": 23, + "h": 21 } }, { - "filename": "ice_tera_shard", + "filename": "n_solarizer", "rotated": false, "trimmed": true, "sourceSize": { @@ -3420,20 +3567,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 4, + "y": 6, + "w": 23, + "h": 21 }, "frame": { - "x": 171, - "y": 145, - "w": 22, - "h": 23 + "x": 289, + "y": 111, + "w": 23, + "h": 21 } }, { - "filename": "dna_splicers", + "filename": "wellspring_mask", "rotated": false, "trimmed": true, "sourceSize": { @@ -3441,20 +3588,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 4, "y": 5, - "w": 22, - "h": 22 + "w": 23, + "h": 21 }, "frame": { - "x": 171, - "y": 168, - "w": 22, - "h": 22 + "x": 312, + "y": 111, + "w": 23, + "h": 21 } }, { - "filename": "never_melt_ice", + "filename": "charcoal", "rotated": false, "trimmed": true, "sourceSize": { @@ -3465,17 +3612,17 @@ "x": 5, "y": 5, "w": 22, - "h": 23 + "h": 22 }, "frame": { - "x": 170, - "y": 190, + "x": 335, + "y": 111, "w": 22, - "h": 23 + "h": 22 } }, { - "filename": "lansat_berry", + "filename": "mystic_ticket", "rotated": false, "trimmed": true, "sourceSize": { @@ -3483,20 +3630,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 21, - "h": 23 + "x": 4, + "y": 7, + "w": 23, + "h": 19 }, "frame": { - "x": 172, - "y": 213, - "w": 21, - "h": 23 + "x": 161, + "y": 127, + "w": 23, + "h": 19 } }, { - "filename": "leaf_stone", + "filename": "poison_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3504,20 +3651,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 21, + "x": 6, + "y": 4, + "w": 22, "h": 23 }, "frame": { - "x": 172, - "y": 236, - "w": 21, + "x": 160, + "y": 146, + "w": 22, "h": 23 } }, { - "filename": "zinc", + "filename": "psychic_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3525,20 +3672,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 4, - "w": 16, - "h": 24 + "w": 22, + "h": 23 }, "frame": { - "x": 177, - "y": 259, - "w": 16, - "h": 24 + "x": 160, + "y": 169, + "w": 22, + "h": 23 } }, { - "filename": "berry_pot", + "filename": "pair_of_tickets", "rotated": false, "trimmed": true, "sourceSize": { @@ -3546,20 +3693,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 18, - "h": 22 + "x": 4, + "y": 7, + "w": 23, + "h": 19 }, "frame": { - "x": 177, - "y": 283, - "w": 18, - "h": 22 + "x": 184, + "y": 127, + "w": 23, + "h": 19 } }, { - "filename": "normal_tera_shard", + "filename": "reaper_cloth", "rotated": false, "trimmed": true, "sourceSize": { @@ -3567,20 +3714,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, + "x": 5, + "y": 5, "w": 22, "h": 23 }, "frame": { - "x": 188, - "y": 63, + "x": 182, + "y": 146, "w": 22, "h": 23 } }, { - "filename": "petaya_berry", + "filename": "rock_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3588,20 +3735,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, + "x": 6, + "y": 4, "w": 22, "h": 23 }, "frame": { - "x": 210, - "y": 63, + "x": 182, + "y": 169, "w": 22, "h": 23 } }, { - "filename": "poison_tera_shard", + "filename": "blue_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -3610,19 +3757,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 4, - "w": 22, - "h": 23 + "y": 6, + "w": 20, + "h": 20 }, "frame": { - "x": 232, - "y": 63, - "w": 22, - "h": 23 + "x": 207, + "y": 127, + "w": 20, + "h": 20 } }, { - "filename": "psychic_tera_shard", + "filename": "steel_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3636,14 +3783,14 @@ "h": 23 }, "frame": { - "x": 254, - "y": 63, + "x": 204, + "y": 147, "w": 22, "h": 23 } }, { - "filename": "reaper_cloth", + "filename": "dark_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -3654,17 +3801,17 @@ "x": 5, "y": 5, "w": 22, - "h": 23 + "h": 22 }, "frame": { - "x": 276, - "y": 63, + "x": 204, + "y": 170, "w": 22, - "h": 23 + "h": 22 } }, { - "filename": "rock_tera_shard", + "filename": "reviver_seed", "rotated": false, "trimmed": true, "sourceSize": { @@ -3672,20 +3819,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 5, + "y": 8, + "w": 23, + "h": 20 }, "frame": { - "x": 298, - "y": 63, - "w": 22, - "h": 23 + "x": 160, + "y": 192, + "w": 23, + "h": 20 } }, { - "filename": "steel_tera_shard", + "filename": "shell_bell", "rotated": false, "trimmed": true, "sourceSize": { @@ -3693,20 +3840,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 5, + "y": 7, + "w": 23, + "h": 20 }, "frame": { - "x": 320, - "y": 63, - "w": 22, - "h": 23 + "x": 183, + "y": 192, + "w": 23, + "h": 20 } }, { - "filename": "stellar_tera_shard", + "filename": "dawn_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -3715,19 +3862,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 4, - "w": 22, - "h": 23 + "y": 6, + "w": 20, + "h": 21 }, "frame": { - "x": 342, - "y": 63, - "w": 22, - "h": 23 + "x": 206, + "y": 192, + "w": 20, + "h": 21 } }, { - "filename": "dragon_memory", + "filename": "super_repel", "rotated": false, "trimmed": true, "sourceSize": { @@ -3735,20 +3882,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 364, - "y": 64, - "w": 22, - "h": 22 + "x": 227, + "y": 128, + "w": 16, + "h": 24 } }, { - "filename": "aggronite", + "filename": "deep_sea_tooth", "rotated": false, "trimmed": true, "sourceSize": { @@ -3756,20 +3903,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 5, + "y": 6, + "w": 22, + "h": 21 }, "frame": { - "x": 188, - "y": 86, - "w": 16, - "h": 16 + "x": 243, + "y": 131, + "w": 22, + "h": 21 } }, { - "filename": "burn_drive", + "filename": "stellar_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3777,20 +3924,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 204, - "y": 86, - "w": 23, - "h": 17 + "x": 226, + "y": 152, + "w": 22, + "h": 23 } }, { - "filename": "chill_drive", + "filename": "water_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3798,20 +3945,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 + "x": 6, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 227, - "y": 86, - "w": 23, - "h": 17 + "x": 226, + "y": 175, + "w": 22, + "h": 23 } }, { - "filename": "coupon", + "filename": "deep_sea_scale", "rotated": false, "trimmed": true, "sourceSize": { @@ -3819,20 +3966,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 + "x": 5, + "y": 6, + "w": 22, + "h": 20 }, "frame": { - "x": 250, - "y": 86, - "w": 23, - "h": 19 + "x": 265, + "y": 132, + "w": 22, + "h": 20 } }, { - "filename": "golden_mystic_ticket", + "filename": "wide_lens", "rotated": false, "trimmed": true, "sourceSize": { @@ -3840,20 +3987,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 + "x": 5, + "y": 4, + "w": 22, + "h": 23 }, "frame": { - "x": 273, - "y": 86, - "w": 23, - "h": 19 + "x": 248, + "y": 152, + "w": 22, + "h": 23 } }, { - "filename": "mystic_ticket", + "filename": "dire_hit", "rotated": false, "trimmed": true, "sourceSize": { @@ -3861,20 +4008,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 296, - "y": 86, - "w": 23, - "h": 19 + "x": 248, + "y": 175, + "w": 22, + "h": 22 } }, { - "filename": "n_lunarizer", + "filename": "dna_splicers", "rotated": false, "trimmed": true, "sourceSize": { @@ -3882,20 +4029,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 319, - "y": 86, - "w": 23, - "h": 21 + "x": 287, + "y": 132, + "w": 22, + "h": 22 } }, { - "filename": "n_solarizer", + "filename": "dragon_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -3903,20 +4050,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 342, - "y": 86, - "w": 23, - "h": 21 + "x": 309, + "y": 132, + "w": 22, + "h": 22 } }, { - "filename": "deep_sea_tooth", + "filename": "super_lure", "rotated": false, "trimmed": true, "sourceSize": { @@ -3924,20 +4071,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 + "x": 8, + "y": 4, + "w": 17, + "h": 24 }, "frame": { - "x": 365, - "y": 86, - "w": 22, - "h": 21 + "x": 270, + "y": 152, + "w": 17, + "h": 24 } }, { - "filename": "dawn_stone", + "filename": "electirizer", "rotated": false, "trimmed": true, "sourceSize": { @@ -3945,20 +4092,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 21 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 389, - "y": 43, - "w": 20, - "h": 21 + "x": 287, + "y": 154, + "w": 22, + "h": 22 } }, { - "filename": "hyper_potion", + "filename": "electric_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -3966,20 +4113,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 5, "y": 5, - "w": 17, - "h": 23 + "w": 22, + "h": 22 }, "frame": { - "x": 409, - "y": 42, - "w": 17, - "h": 23 + "x": 309, + "y": 154, + "w": 22, + "h": 22 } }, { - "filename": "electirizer", + "filename": "enigma_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -3993,14 +4140,14 @@ "h": 22 }, "frame": { - "x": 386, - "y": 64, + "x": 270, + "y": 176, "w": 22, "h": 22 } }, { - "filename": "sachet", + "filename": "fairy_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4008,20 +4155,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 18, - "h": 23 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 408, - "y": 65, - "w": 18, - "h": 23 + "x": 292, + "y": 176, + "w": 22, + "h": 22 } }, { - "filename": "dusk_stone", + "filename": "fighting_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4029,20 +4176,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 21, - "h": 21 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 387, - "y": 86, - "w": 21, - "h": 21 + "x": 331, + "y": 133, + "w": 22, + "h": 22 } }, { - "filename": "razor_fang", + "filename": "fire_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4050,20 +4197,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 18, - "h": 20 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 408, - "y": 88, - "w": 18, - "h": 20 + "x": 331, + "y": 155, + "w": 22, + "h": 22 } }, { - "filename": "pair_of_tickets", + "filename": "hyper_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -4071,20 +4218,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 + "x": 8, + "y": 5, + "w": 17, + "h": 23 }, "frame": { - "x": 191, - "y": 103, - "w": 23, - "h": 19 + "x": 314, + "y": 176, + "w": 17, + "h": 23 } }, { - "filename": "sharp_beak", + "filename": "flying_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4094,18 +4241,18 @@ "spriteSourceSize": { "x": 5, "y": 5, - "w": 21, - "h": 23 + "w": 22, + "h": 22 }, "frame": { - "x": 193, - "y": 122, - "w": 21, - "h": 23 + "x": 331, + "y": 177, + "w": 22, + "h": 22 } }, { - "filename": "water_tera_shard", + "filename": "blunder_policy", "rotated": false, "trimmed": true, "sourceSize": { @@ -4113,20 +4260,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, + "x": 5, + "y": 6, "w": 22, - "h": 23 + "h": 19 }, "frame": { - "x": 214, - "y": 103, + "x": 226, + "y": 198, "w": 22, - "h": 23 + "h": 19 } }, { - "filename": "whipped_dream", + "filename": "fairy_feather", "rotated": false, "trimmed": true, "sourceSize": { @@ -4135,19 +4282,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 4, - "w": 21, - "h": 23 + "y": 7, + "w": 22, + "h": 20 }, "frame": { - "x": 193, - "y": 145, - "w": 21, - "h": 23 + "x": 248, + "y": 197, + "w": 22, + "h": 20 } }, { - "filename": "wide_lens", + "filename": "dubious_disc", "rotated": false, "trimmed": true, "sourceSize": { @@ -4156,19 +4303,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 4, + "y": 7, "w": 22, - "h": 23 + "h": 19 }, "frame": { - "x": 214, - "y": 126, + "x": 270, + "y": 198, "w": 22, - "h": 23 + "h": 19 } }, { - "filename": "electric_memory", + "filename": "ganlon_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -4182,14 +4329,14 @@ "h": 22 }, "frame": { - "x": 193, - "y": 168, + "x": 292, + "y": 198, "w": 22, "h": 22 } }, { - "filename": "enigma_berry", + "filename": "ghost_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4203,14 +4350,14 @@ "h": 22 }, "frame": { - "x": 192, - "y": 190, + "x": 314, + "y": 199, "w": 22, "h": 22 } }, { - "filename": "blunder_policy", + "filename": "berry_pot", "rotated": false, "trimmed": true, "sourceSize": { @@ -4218,20 +4365,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 19 + "x": 7, + "y": 5, + "w": 18, + "h": 22 }, "frame": { - "x": 214, - "y": 149, - "w": 22, - "h": 19 + "x": 336, + "y": 199, + "w": 18, + "h": 22 } }, { - "filename": "fairy_memory", + "filename": "grass_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4245,14 +4392,14 @@ "h": 22 }, "frame": { - "x": 215, - "y": 168, + "x": 116, + "y": 203, "w": 22, "h": 22 } }, { - "filename": "fighting_memory", + "filename": "ground_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4266,14 +4413,14 @@ "h": 22 }, "frame": { - "x": 214, - "y": 190, + "x": 115, + "y": 225, "w": 22, "h": 22 } }, { - "filename": "fire_memory", + "filename": "guard_spec", "rotated": false, "trimmed": true, "sourceSize": { @@ -4287,14 +4434,14 @@ "h": 22 }, "frame": { - "x": 193, - "y": 212, + "x": 115, + "y": 247, "w": 22, "h": 22 } }, { - "filename": "flying_memory", + "filename": "ice_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4308,14 +4455,14 @@ "h": 22 }, "frame": { - "x": 193, - "y": 234, + "x": 115, + "y": 269, "w": 22, "h": 22 } }, { - "filename": "ganlon_berry", + "filename": "ice_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -4329,14 +4476,14 @@ "h": 22 }, "frame": { - "x": 193, - "y": 256, + "x": 138, + "y": 209, "w": 22, "h": 22 } }, { - "filename": "ghost_memory", + "filename": "lansat_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -4345,19 +4492,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, - "w": 22, - "h": 22 + "y": 4, + "w": 21, + "h": 23 }, "frame": { - "x": 215, - "y": 212, - "w": 22, - "h": 22 + "x": 137, + "y": 231, + "w": 21, + "h": 23 } }, { - "filename": "grass_memory", + "filename": "leaf_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -4367,18 +4514,18 @@ "spriteSourceSize": { "x": 5, "y": 5, - "w": 22, - "h": 22 + "w": 21, + "h": 23 }, "frame": { - "x": 215, - "y": 234, - "w": 22, - "h": 22 + "x": 137, + "y": 254, + "w": 21, + "h": 23 } }, { - "filename": "ground_memory", + "filename": "liechi_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -4387,19 +4534,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, + "y": 6, "w": 22, - "h": 22 + "h": 21 }, "frame": { - "x": 215, - "y": 256, + "x": 160, + "y": 212, "w": 22, - "h": 22 + "h": 21 } }, { - "filename": "guard_spec", + "filename": "magmarizer", "rotated": false, "trimmed": true, "sourceSize": { @@ -4413,14 +4560,14 @@ "h": 22 }, "frame": { - "x": 195, - "y": 278, + "x": 182, + "y": 212, "w": 22, "h": 22 } }, { - "filename": "hard_meteorite", + "filename": "mini_black_hole", "rotated": false, "trimmed": true, "sourceSize": { @@ -4428,20 +4575,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 5, "y": 5, - "w": 20, + "w": 22, "h": 22 }, "frame": { - "x": 217, - "y": 278, - "w": 20, + "x": 204, + "y": 213, + "w": 22, "h": 22 } }, { - "filename": "ice_memory", + "filename": "moon_flute", "rotated": false, "trimmed": true, "sourceSize": { @@ -4455,14 +4602,14 @@ "h": 22 }, "frame": { - "x": 236, - "y": 105, + "x": 158, + "y": 233, "w": 22, "h": 22 } }, { - "filename": "ice_stone", + "filename": "normal_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4476,14 +4623,14 @@ "h": 22 }, "frame": { - "x": 258, - "y": 105, + "x": 158, + "y": 255, "w": 22, "h": 22 } }, { - "filename": "magmarizer", + "filename": "poison_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4497,14 +4644,14 @@ "h": 22 }, "frame": { - "x": 236, - "y": 127, + "x": 180, + "y": 234, "w": 22, "h": 22 } }, { - "filename": "mini_black_hole", + "filename": "protector", "rotated": false, "trimmed": true, "sourceSize": { @@ -4518,14 +4665,14 @@ "h": 22 }, "frame": { - "x": 280, - "y": 105, + "x": 180, + "y": 256, "w": 22, "h": 22 } }, { - "filename": "normal_memory", + "filename": "psychic_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4539,14 +4686,14 @@ "h": 22 }, "frame": { - "x": 258, - "y": 127, + "x": 202, + "y": 235, "w": 22, "h": 22 } }, { - "filename": "poison_memory", + "filename": "rock_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4560,14 +4707,14 @@ "h": 22 }, "frame": { - "x": 280, - "y": 127, + "x": 202, + "y": 257, "w": 22, "h": 22 } }, { - "filename": "dubious_disc", + "filename": "malicious_armor", "rotated": false, "trimmed": true, "sourceSize": { @@ -4576,19 +4723,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, + "y": 6, "w": 22, - "h": 19 + "h": 20 }, "frame": { - "x": 236, - "y": 149, + "x": 226, + "y": 217, "w": 22, - "h": 19 + "h": 20 } }, { - "filename": "protector", + "filename": "scroll_of_darkness", "rotated": false, "trimmed": true, "sourceSize": { @@ -4602,14 +4749,14 @@ "h": 22 }, "frame": { - "x": 237, - "y": 168, + "x": 248, + "y": 217, "w": 22, "h": 22 } }, { - "filename": "psychic_memory", + "filename": "scroll_of_waters", "rotated": false, "trimmed": true, "sourceSize": { @@ -4623,14 +4770,14 @@ "h": 22 }, "frame": { - "x": 236, - "y": 190, + "x": 270, + "y": 217, "w": 22, "h": 22 } }, { - "filename": "mystic_water", + "filename": "sharp_beak", "rotated": false, "trimmed": true, "sourceSize": { @@ -4638,20 +4785,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 5, "y": 5, - "w": 20, + "w": 21, "h": 23 }, "frame": { - "x": 237, - "y": 212, - "w": 20, + "x": 224, + "y": 237, + "w": 21, "h": 23 } }, { - "filename": "potion", + "filename": "shed_shell", "rotated": false, "trimmed": true, "sourceSize": { @@ -4659,20 +4806,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 5, "y": 5, - "w": 17, - "h": 23 + "w": 22, + "h": 22 }, "frame": { - "x": 302, - "y": 105, - "w": 17, - "h": 23 + "x": 292, + "y": 220, + "w": 22, + "h": 22 } }, { - "filename": "wellspring_mask", + "filename": "starf_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -4680,20 +4827,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 5, "y": 5, - "w": 23, - "h": 21 + "w": 22, + "h": 22 }, "frame": { - "x": 319, - "y": 107, - "w": 23, - "h": 21 + "x": 314, + "y": 221, + "w": 22, + "h": 22 } }, { - "filename": "liechi_berry", + "filename": "dusk_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -4701,20 +4848,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 6, - "w": 22, + "w": 21, "h": 21 }, "frame": { - "x": 302, - "y": 128, - "w": 22, + "x": 224, + "y": 260, + "w": 21, "h": 21 } }, { - "filename": "reviver_seed", + "filename": "steel_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -4723,19 +4870,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 8, - "w": 23, - "h": 20 + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 342, - "y": 107, - "w": 23, - "h": 20 + "x": 245, + "y": 239, + "w": 22, + "h": 22 } }, { - "filename": "shell_bell", + "filename": "sun_flute", "rotated": false, "trimmed": true, "sourceSize": { @@ -4744,19 +4891,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, - "w": 23, - "h": 20 + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 365, - "y": 107, - "w": 23, - "h": 20 + "x": 267, + "y": 239, + "w": 22, + "h": 22 } }, { - "filename": "rock_memory", + "filename": "sweet_apple", "rotated": false, "trimmed": true, "sourceSize": { @@ -4765,19 +4912,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, + "y": 6, "w": 22, - "h": 22 + "h": 21 }, "frame": { - "x": 237, - "y": 235, + "x": 245, + "y": 261, "w": 22, - "h": 22 + "h": 21 } }, { - "filename": "scroll_of_darkness", + "filename": "syrupy_apple", "rotated": false, "trimmed": true, "sourceSize": { @@ -4786,19 +4933,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, + "y": 6, "w": 22, - "h": 22 + "h": 21 }, "frame": { - "x": 237, - "y": 257, + "x": 267, + "y": 261, "w": 22, - "h": 22 + "h": 21 } }, { - "filename": "scroll_of_waters", + "filename": "thick_club", "rotated": false, "trimmed": true, "sourceSize": { @@ -4812,14 +4959,14 @@ "h": 22 }, "frame": { - "x": 237, - "y": 279, + "x": 289, + "y": 242, "w": 22, "h": 22 } }, { - "filename": "upgrade", + "filename": "hard_meteorite", "rotated": false, "trimmed": true, "sourceSize": { @@ -4827,20 +4974,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 19 + "x": 7, + "y": 5, + "w": 20, + "h": 22 }, "frame": { - "x": 258, - "y": 149, - "w": 22, - "h": 19 + "x": 336, + "y": 221, + "w": 20, + "h": 22 } }, { - "filename": "shed_shell", + "filename": "tart_apple", "rotated": false, "trimmed": true, "sourceSize": { @@ -4849,19 +4996,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, + "y": 6, "w": 22, - "h": 22 + "h": 21 }, "frame": { - "x": 259, - "y": 168, + "x": 289, + "y": 264, "w": 22, - "h": 22 + "h": 21 } }, { - "filename": "starf_berry", + "filename": "thunder_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -4875,14 +5022,14 @@ "h": 22 }, "frame": { - "x": 258, - "y": 190, + "x": 311, + "y": 243, "w": 22, "h": 22 } }, { - "filename": "steel_memory", + "filename": "tm_bug", "rotated": false, "trimmed": true, "sourceSize": { @@ -4896,14 +5043,14 @@ "h": 22 }, "frame": { - "x": 257, - "y": 212, + "x": 333, + "y": 243, "w": 22, "h": 22 } }, { - "filename": "big_nugget", + "filename": "tera_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -4911,20 +5058,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 5, "y": 6, - "w": 20, + "w": 22, "h": 20 }, "frame": { - "x": 388, - "y": 107, - "w": 20, + "x": 311, + "y": 265, + "w": 22, "h": 20 } }, { - "filename": "oval_stone", + "filename": "tm_dark", "rotated": false, "trimmed": true, "sourceSize": { @@ -4932,20 +5079,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 19 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 408, - "y": 108, - "w": 18, - "h": 19 + "x": 333, + "y": 265, + "w": 22, + "h": 22 } }, { - "filename": "metal_alloy", + "filename": "shock_drive", "rotated": false, "trimmed": true, "sourceSize": { @@ -4953,20 +5100,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 21, - "h": 19 + "x": 4, + "y": 8, + "w": 23, + "h": 17 }, "frame": { - "x": 280, - "y": 149, - "w": 21, - "h": 19 + "x": 137, + "y": 277, + "w": 23, + "h": 17 } }, { - "filename": "sitrus_berry", + "filename": "whipped_dream", "rotated": false, "trimmed": true, "sourceSize": { @@ -4974,20 +5121,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 20, - "h": 22 + "x": 5, + "y": 4, + "w": 21, + "h": 23 }, "frame": { - "x": 281, - "y": 168, - "w": 20, - "h": 22 + "x": 116, + "y": 291, + "w": 21, + "h": 23 } }, { - "filename": "thick_club", + "filename": "mystic_water", "rotated": false, "trimmed": true, "sourceSize": { @@ -4995,20 +5142,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 5, - "w": 22, - "h": 22 + "w": 20, + "h": 23 }, "frame": { - "x": 301, - "y": 149, - "w": 22, - "h": 22 + "x": 116, + "y": 314, + "w": 20, + "h": 23 } }, { - "filename": "thunder_stone", + "filename": "sitrus_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -5016,20 +5163,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 5, - "w": 22, + "w": 20, "h": 22 }, "frame": { - "x": 280, - "y": 190, - "w": 22, + "x": 116, + "y": 337, + "w": 20, "h": 22 } }, { - "filename": "tm_bug", + "filename": "tm_dragon", "rotated": false, "trimmed": true, "sourceSize": { @@ -5043,14 +5190,14 @@ "h": 22 }, "frame": { - "x": 279, - "y": 212, + "x": 137, + "y": 294, "w": 22, "h": 22 } }, { - "filename": "tm_dark", + "filename": "tm_electric", "rotated": false, "trimmed": true, "sourceSize": { @@ -5064,14 +5211,14 @@ "h": 22 }, "frame": { - "x": 259, - "y": 234, + "x": 136, + "y": 316, "w": 22, "h": 22 } }, { - "filename": "tm_dragon", + "filename": "tm_fairy", "rotated": false, "trimmed": true, "sourceSize": { @@ -5085,14 +5232,14 @@ "h": 22 }, "frame": { - "x": 259, - "y": 256, + "x": 136, + "y": 338, "w": 22, "h": 22 } }, { - "filename": "tm_electric", + "filename": "gb", "rotated": false, "trimmed": true, "sourceSize": { @@ -5100,20 +5247,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 6, + "y": 6, + "w": 20, + "h": 20 }, "frame": { - "x": 259, - "y": 278, - "w": 22, - "h": 22 + "x": 116, + "y": 359, + "w": 20, + "h": 20 } }, { - "filename": "tm_fairy", + "filename": "tm_fighting", "rotated": false, "trimmed": true, "sourceSize": { @@ -5127,14 +5274,14 @@ "h": 22 }, "frame": { - "x": 281, - "y": 234, + "x": 116, + "y": 379, "w": 22, "h": 22 } }, { - "filename": "tm_fighting", + "filename": "upgrade", "rotated": false, "trimmed": true, "sourceSize": { @@ -5143,15 +5290,15 @@ }, "spriteSourceSize": { "x": 5, - "y": 5, + "y": 7, "w": 22, - "h": 22 + "h": 19 }, "frame": { - "x": 281, - "y": 256, + "x": 136, + "y": 360, "w": 22, - "h": 22 + "h": 19 } }, { @@ -5169,14 +5316,14 @@ "h": 22 }, "frame": { - "x": 281, - "y": 278, + "x": 138, + "y": 379, "w": 22, "h": 22 } }, { - "filename": "lum_berry", + "filename": "everstone", "rotated": false, "trimmed": true, "sourceSize": { @@ -5185,19 +5332,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 7, + "y": 8, "w": 20, - "h": 19 + "h": 17 }, "frame": { - "x": 301, - "y": 171, + "x": 160, + "y": 277, "w": 20, - "h": 19 + "h": 17 } }, { - "filename": "metal_coat", + "filename": "tm_flying", "rotated": false, "trimmed": true, "sourceSize": { @@ -5205,20 +5352,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 5, "y": 5, - "w": 19, + "w": 22, "h": 22 }, "frame": { - "x": 302, - "y": 190, - "w": 19, + "x": 159, + "y": 294, + "w": 22, "h": 22 } }, { - "filename": "tm_flying", + "filename": "tm_ghost", "rotated": false, "trimmed": true, "sourceSize": { @@ -5232,14 +5379,14 @@ "h": 22 }, "frame": { - "x": 301, - "y": 212, + "x": 158, + "y": 316, "w": 22, "h": 22 } }, { - "filename": "tm_ghost", + "filename": "tm_grass", "rotated": false, "trimmed": true, "sourceSize": { @@ -5253,14 +5400,14 @@ "h": 22 }, "frame": { - "x": 303, - "y": 234, + "x": 158, + "y": 338, "w": 22, "h": 22 } }, { - "filename": "tm_grass", + "filename": "metal_alloy", "rotated": false, "trimmed": true, "sourceSize": { @@ -5268,20 +5415,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 6, + "y": 7, + "w": 21, + "h": 19 }, "frame": { - "x": 303, - "y": 256, - "w": 22, - "h": 22 + "x": 158, + "y": 360, + "w": 21, + "h": 19 } }, { - "filename": "tm_ground", + "filename": "lock_capsule", "rotated": false, "trimmed": true, "sourceSize": { @@ -5289,20 +5436,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 7, "y": 5, - "w": 22, + "w": 19, "h": 22 }, "frame": { - "x": 303, - "y": 278, - "w": 22, + "x": 160, + "y": 379, + "w": 19, "h": 22 } }, { - "filename": "poison_barb", + "filename": "relic_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -5310,20 +5457,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 + "x": 7, + "y": 9, + "w": 17, + "h": 16 }, "frame": { - "x": 324, - "y": 128, - "w": 21, - "h": 21 + "x": 180, + "y": 278, + "w": 17, + "h": 16 } }, { - "filename": "tm_ice", + "filename": "metal_coat", "rotated": false, "trimmed": true, "sourceSize": { @@ -5331,20 +5478,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 5, - "w": 22, + "w": 19, "h": 22 }, "frame": { - "x": 323, - "y": 149, - "w": 22, + "x": 181, + "y": 294, + "w": 19, "h": 22 } }, { - "filename": "tm_normal", + "filename": "tm_ground", "rotated": false, "trimmed": true, "sourceSize": { @@ -5358,14 +5505,14 @@ "h": 22 }, "frame": { - "x": 321, - "y": 171, + "x": 180, + "y": 316, "w": 22, "h": 22 } }, { - "filename": "tm_poison", + "filename": "tm_ice", "rotated": false, "trimmed": true, "sourceSize": { @@ -5379,14 +5526,14 @@ "h": 22 }, "frame": { - "x": 345, - "y": 127, + "x": 180, + "y": 338, "w": 22, "h": 22 } }, { - "filename": "tm_psychic", + "filename": "tm_normal", "rotated": false, "trimmed": true, "sourceSize": { @@ -5400,14 +5547,14 @@ "h": 22 }, "frame": { - "x": 345, - "y": 149, + "x": 179, + "y": 360, "w": 22, "h": 22 } }, { - "filename": "tm_rock", + "filename": "tm_poison", "rotated": false, "trimmed": true, "sourceSize": { @@ -5421,14 +5568,14 @@ "h": 22 }, "frame": { - "x": 343, - "y": 171, + "x": 179, + "y": 382, "w": 22, "h": 22 } }, { - "filename": "tm_steel", + "filename": "tm_psychic", "rotated": false, "trimmed": true, "sourceSize": { @@ -5442,14 +5589,14 @@ "h": 22 }, "frame": { - "x": 367, - "y": 127, + "x": 117, + "y": 401, "w": 22, "h": 22 } }, { - "filename": "tm_water", + "filename": "tm_rock", "rotated": false, "trimmed": true, "sourceSize": { @@ -5463,14 +5610,14 @@ "h": 22 }, "frame": { - "x": 367, - "y": 149, + "x": 139, + "y": 401, "w": 22, "h": 22 } }, { - "filename": "water_memory", + "filename": "sachet", "rotated": false, "trimmed": true, "sourceSize": { @@ -5478,20 +5625,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 6, + "y": 4, + "w": 18, + "h": 23 }, "frame": { - "x": 365, - "y": 171, - "w": 22, - "h": 22 + "x": 161, + "y": 401, + "w": 18, + "h": 23 } }, { - "filename": "water_stone", + "filename": "tm_steel", "rotated": false, "trimmed": true, "sourceSize": { @@ -5505,14 +5652,14 @@ "h": 22 }, "frame": { - "x": 389, - "y": 127, + "x": 179, + "y": 404, "w": 22, "h": 22 } }, { - "filename": "full_heal", + "filename": "leftovers", "rotated": false, "trimmed": true, "sourceSize": { @@ -5520,20 +5667,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 9, - "y": 4, + "x": 8, + "y": 5, "w": 15, - "h": 23 + "h": 22 }, "frame": { - "x": 411, - "y": 127, + "x": 358, + "y": 89, "w": 15, - "h": 23 + "h": 22 } }, { - "filename": "x_accuracy", + "filename": "razor_fang", "rotated": false, "trimmed": true, "sourceSize": { @@ -5541,20 +5688,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 + "x": 7, + "y": 6, + "w": 18, + "h": 20 }, "frame": { - "x": 389, - "y": 149, - "w": 22, - "h": 22 + "x": 373, + "y": 88, + "w": 18, + "h": 20 } }, { - "filename": "leftovers", + "filename": "metronome", "rotated": false, "trimmed": true, "sourceSize": { @@ -5562,20 +5709,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 7, "y": 5, - "w": 15, + "w": 17, "h": 22 }, "frame": { - "x": 411, - "y": 150, - "w": 15, + "x": 357, + "y": 111, + "w": 17, "h": 22 } }, { - "filename": "x_attack", + "filename": "tm_water", "rotated": false, "trimmed": true, "sourceSize": { @@ -5589,14 +5736,14 @@ "h": 22 }, "frame": { - "x": 387, - "y": 171, + "x": 353, + "y": 133, "w": 22, "h": 22 } }, { - "filename": "super_potion", + "filename": "water_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -5604,20 +5751,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 5, "y": 5, - "w": 17, - "h": 23 + "w": 22, + "h": 22 }, "frame": { - "x": 409, - "y": 172, - "w": 17, - "h": 23 + "x": 353, + "y": 155, + "w": 22, + "h": 22 } }, { - "filename": "power_herb", + "filename": "water_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -5625,20 +5772,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 321, - "y": 193, - "w": 20, - "h": 19 + "x": 353, + "y": 177, + "w": 22, + "h": 22 } }, { - "filename": "x_defense", + "filename": "x_accuracy", "rotated": false, "trimmed": true, "sourceSize": { @@ -5652,14 +5799,14 @@ "h": 22 }, "frame": { - "x": 323, - "y": 212, + "x": 354, + "y": 199, "w": 22, "h": 22 } }, { - "filename": "razor_claw", + "filename": "x_attack", "rotated": false, "trimmed": true, "sourceSize": { @@ -5667,20 +5814,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 + "x": 5, + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 341, - "y": 193, - "w": 20, - "h": 19 + "x": 356, + "y": 221, + "w": 22, + "h": 22 } }, { - "filename": "x_sp_atk", + "filename": "x_defense", "rotated": false, "trimmed": true, "sourceSize": { @@ -5694,14 +5841,14 @@ "h": 22 }, "frame": { - "x": 325, - "y": 234, + "x": 355, + "y": 243, "w": 22, "h": 22 } }, { - "filename": "x_sp_def", + "filename": "x_sp_atk", "rotated": false, "trimmed": true, "sourceSize": { @@ -5715,14 +5862,14 @@ "h": 22 }, "frame": { - "x": 325, - "y": 256, + "x": 355, + "y": 265, "w": 22, "h": 22 } }, { - "filename": "x_speed", + "filename": "potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -5730,20 +5877,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 8, "y": 5, - "w": 22, - "h": 22 + "w": 17, + "h": 23 }, "frame": { - "x": 325, - "y": 278, - "w": 22, - "h": 22 + "x": 374, + "y": 108, + "w": 17, + "h": 23 } }, { - "filename": "deep_sea_scale", + "filename": "unknown", "rotated": false, "trimmed": true, "sourceSize": { @@ -5751,20 +5898,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 361, - "y": 193, - "w": 22, - "h": 20 + "x": 391, + "y": 91, + "w": 16, + "h": 24 } }, { - "filename": "fairy_feather", + "filename": "x_sp_def", "rotated": false, "trimmed": true, "sourceSize": { @@ -5773,19 +5920,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, + "y": 5, "w": 22, - "h": 20 + "h": 22 }, "frame": { - "x": 383, - "y": 193, + "x": 407, + "y": 92, "w": 22, - "h": 20 + "h": 22 } }, { - "filename": "shiny_stone", + "filename": "zinc", "rotated": false, "trimmed": true, "sourceSize": { @@ -5793,20 +5940,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 + "x": 8, + "y": 4, + "w": 16, + "h": 24 }, "frame": { - "x": 405, - "y": 195, - "w": 21, - "h": 21 + "x": 375, + "y": 131, + "w": 16, + "h": 24 } }, { - "filename": "mystery_egg", + "filename": "super_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -5815,19 +5962,19 @@ }, "spriteSourceSize": { "x": 8, - "y": 8, - "w": 16, - "h": 18 + "y": 5, + "w": 17, + "h": 23 }, "frame": { - "x": 345, - "y": 212, - "w": 16, - "h": 18 + "x": 391, + "y": 115, + "w": 17, + "h": 23 } }, { - "filename": "douse_drive", + "filename": "wise_glasses", "rotated": false, "trimmed": true, "sourceSize": { @@ -5841,14 +5988,35 @@ "h": 17 }, "frame": { - "x": 361, - "y": 213, + "x": 408, + "y": 114, "w": 23, "h": 17 } }, { - "filename": "masterpiece_teacup", + "filename": "soothe_bell", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 22 + }, + "frame": { + "x": 375, + "y": 155, + "w": 17, + "h": 22 + } + }, + { + "filename": "x_speed", "rotated": false, "trimmed": true, "sourceSize": { @@ -5857,19 +6025,19 @@ }, "spriteSourceSize": { "x": 5, - "y": 7, - "w": 21, - "h": 18 + "y": 5, + "w": 22, + "h": 22 }, "frame": { - "x": 384, - "y": 213, - "w": 21, - "h": 18 + "x": 375, + "y": 177, + "w": 22, + "h": 22 } }, { - "filename": "zoom_lens", + "filename": "poison_barb", "rotated": false, "trimmed": true, "sourceSize": { @@ -5883,14 +6051,14 @@ "h": 21 }, "frame": { - "x": 405, - "y": 216, + "x": 376, + "y": 199, "w": 21, "h": 21 } }, { - "filename": "sweet_apple", + "filename": "quick_claw", "rotated": false, "trimmed": true, "sourceSize": { @@ -5898,20 +6066,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 6, - "w": 22, + "w": 19, "h": 21 }, "frame": { - "x": 347, - "y": 230, - "w": 22, + "x": 378, + "y": 220, + "w": 19, "h": 21 } }, { - "filename": "syrupy_apple", + "filename": "absolite", "rotated": false, "trimmed": true, "sourceSize": { @@ -5919,20 +6087,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 347, - "y": 251, - "w": 22, - "h": 21 + "x": 391, + "y": 138, + "w": 16, + "h": 16 } }, { - "filename": "tart_apple", + "filename": "shiny_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -5942,18 +6110,18 @@ "spriteSourceSize": { "x": 5, "y": 6, - "w": 22, + "w": 21, "h": 21 }, "frame": { - "x": 347, - "y": 272, - "w": 22, + "x": 392, + "y": 154, + "w": 21, "h": 21 } }, { - "filename": "eviolite", + "filename": "oval_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -5961,20 +6129,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 15, - "h": 15 + "x": 7, + "y": 7, + "w": 18, + "h": 19 }, "frame": { - "x": 369, - "y": 230, - "w": 15, - "h": 15 + "x": 413, + "y": 131, + "w": 18, + "h": 19 } }, { - "filename": "sharp_meteorite", + "filename": "baton", "rotated": false, "trimmed": true, "sourceSize": { @@ -5982,20 +6150,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 21, + "x": 7, + "y": 7, + "w": 18, "h": 18 }, "frame": { - "x": 384, - "y": 231, - "w": 21, + "x": 413, + "y": 150, + "w": 18, "h": 18 } }, { - "filename": "unremarkable_teacup", + "filename": "candy", "rotated": false, "trimmed": true, "sourceSize": { @@ -6003,20 +6171,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 21, + "x": 7, + "y": 11, + "w": 18, "h": 18 }, "frame": { - "x": 405, - "y": 237, - "w": 21, + "x": 413, + "y": 168, + "w": 18, "h": 18 } }, { - "filename": "prism_scale", + "filename": "mystery_egg", "rotated": false, "trimmed": true, "sourceSize": { @@ -6024,20 +6192,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 9, + "x": 8, "y": 8, - "w": 15, - "h": 15 + "w": 16, + "h": 18 }, "frame": { - "x": 369, - "y": 245, - "w": 15, - "h": 15 + "x": 397, + "y": 175, + "w": 16, + "h": 18 } }, { - "filename": "metronome", + "filename": "dark_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6046,19 +6214,19 @@ }, "spriteSourceSize": { "x": 7, - "y": 5, - "w": 17, - "h": 22 + "y": 7, + "w": 18, + "h": 18 }, "frame": { - "x": 369, - "y": 260, - "w": 17, - "h": 22 + "x": 413, + "y": 186, + "w": 18, + "h": 18 } }, { - "filename": "quick_claw", + "filename": "aerodactylite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6066,20 +6234,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 21 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 386, - "y": 249, - "w": 19, - "h": 21 + "x": 397, + "y": 193, + "w": 16, + "h": 16 } }, { - "filename": "blue_orb", + "filename": "flame_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6087,20 +6255,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 + "x": 7, + "y": 7, + "w": 18, + "h": 18 }, "frame": { - "x": 405, - "y": 255, - "w": 20, - "h": 20 + "x": 413, + "y": 204, + "w": 18, + "h": 18 } }, { - "filename": "candy_jar", + "filename": "aggronite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6108,20 +6276,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 386, - "y": 270, - "w": 19, - "h": 20 + "x": 397, + "y": 209, + "w": 16, + "h": 16 } }, { - "filename": "golden_egg", + "filename": "light_ball", "rotated": false, "trimmed": true, "sourceSize": { @@ -6130,19 +6298,19 @@ }, "spriteSourceSize": { "x": 7, - "y": 6, - "w": 17, - "h": 20 + "y": 7, + "w": 18, + "h": 18 }, "frame": { - "x": 369, - "y": 282, - "w": 17, - "h": 20 + "x": 413, + "y": 222, + "w": 18, + "h": 18 } }, { - "filename": "malicious_armor", + "filename": "alakazite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6150,20 +6318,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 347, - "y": 293, - "w": 22, - "h": 20 + "x": 397, + "y": 225, + "w": 16, + "h": 16 } }, { - "filename": "everstone", + "filename": "light_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6171,20 +6339,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 17 + "x": 7, + "y": 7, + "w": 18, + "h": 18 }, "frame": { - "x": 405, - "y": 275, - "w": 20, - "h": 17 + "x": 413, + "y": 240, + "w": 18, + "h": 18 } }, { - "filename": "hard_stone", + "filename": "zoom_lens", "rotated": false, "trimmed": true, "sourceSize": { @@ -6192,20 +6360,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 5, "y": 6, - "w": 19, - "h": 20 + "w": 21, + "h": 21 }, "frame": { - "x": 386, - "y": 290, - "w": 19, - "h": 20 + "x": 200, + "y": 279, + "w": 21, + "h": 21 } }, { - "filename": "gb", + "filename": "lum_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -6214,19 +6382,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 6, + "y": 7, "w": 20, - "h": 20 + "h": 19 }, "frame": { - "x": 405, - "y": 292, + "x": 221, + "y": 281, "w": 20, - "h": 20 + "h": 19 } }, { - "filename": "lucky_egg", + "filename": "masterpiece_teacup", "rotated": false, "trimmed": true, "sourceSize": { @@ -6234,20 +6402,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 + "x": 5, + "y": 7, + "w": 21, + "h": 18 }, "frame": { - "x": 369, - "y": 302, - "w": 17, - "h": 20 + "x": 241, + "y": 282, + "w": 21, + "h": 18 } }, { - "filename": "miracle_seed", + "filename": "old_gateau", "rotated": false, "trimmed": true, "sourceSize": { @@ -6256,19 +6424,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 7, - "w": 19, - "h": 19 + "y": 8, + "w": 21, + "h": 18 }, "frame": { - "x": 386, - "y": 310, - "w": 19, - "h": 19 + "x": 262, + "y": 282, + "w": 21, + "h": 18 } }, { - "filename": "magnet", + "filename": "altarianite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6276,20 +6444,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 405, - "y": 312, - "w": 20, - "h": 20 + "x": 200, + "y": 300, + "w": 16, + "h": 16 } }, { - "filename": "relic_crown", + "filename": "sharp_meteorite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6297,20 +6465,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, + "x": 6, + "y": 8, + "w": 21, "h": 18 }, "frame": { - "x": 195, + "x": 216, "y": 300, - "w": 23, + "w": 21, "h": 18 } }, { - "filename": "spell_tag", + "filename": "unremarkable_teacup", "rotated": false, "trimmed": true, "sourceSize": { @@ -6318,20 +6486,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 19, - "h": 21 + "x": 5, + "y": 7, + "w": 21, + "h": 18 }, "frame": { - "x": 218, + "x": 237, "y": 300, - "w": 19, - "h": 21 + "w": 21, + "h": 18 } }, { - "filename": "tera_orb", + "filename": "magnet", "rotated": false, "trimmed": true, "sourceSize": { @@ -6339,15 +6507,15 @@ "h": 32 }, "spriteSourceSize": { - "x": 5, + "x": 6, "y": 6, - "w": 22, + "w": 20, "h": 20 }, "frame": { - "x": 237, - "y": 301, - "w": 22, + "x": 258, + "y": 300, + "w": 20, "h": 20 } }, @@ -6366,8 +6534,8 @@ "h": 20 }, "frame": { - "x": 259, - "y": 300, + "x": 283, + "y": 285, "w": 20, "h": 20 } @@ -6387,8 +6555,8 @@ "h": 20 }, "frame": { - "x": 279, - "y": 300, + "x": 303, + "y": 285, "w": 20, "h": 20 } @@ -6408,8 +6576,8 @@ "h": 20 }, "frame": { - "x": 299, - "y": 300, + "x": 278, + "y": 305, "w": 20, "h": 20 } @@ -6429,14 +6597,14 @@ "h": 20 }, "frame": { - "x": 319, - "y": 300, + "x": 298, + "y": 305, "w": 20, "h": 20 } }, { - "filename": "shock_drive", + "filename": "revive", "rotated": false, "trimmed": true, "sourceSize": { @@ -6444,20 +6612,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 10, "y": 8, - "w": 23, + "w": 12, "h": 17 }, "frame": { - "x": 155, - "y": 305, - "w": 23, + "x": 202, + "y": 316, + "w": 12, "h": 17 } }, { - "filename": "soothe_bell", + "filename": "power_herb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6465,20 +6633,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 17, - "h": 22 + "x": 6, + "y": 7, + "w": 20, + "h": 19 }, "frame": { - "x": 178, - "y": 305, - "w": 17, - "h": 22 + "x": 214, + "y": 318, + "w": 20, + "h": 19 } }, { - "filename": "wise_glasses", + "filename": "razor_claw", "rotated": false, "trimmed": true, "sourceSize": { @@ -6486,20 +6654,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 + "x": 6, + "y": 7, + "w": 20, + "h": 19 }, "frame": { - "x": 195, + "x": 234, "y": 318, - "w": 23, - "h": 17 + "w": 20, + "h": 19 + } + }, + { + "filename": "smooth_meteorite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 254, + "y": 320, + "w": 20, + "h": 20 } }, { - "filename": "smooth_meteorite", + "filename": "strange_ball", "rotated": false, "trimmed": true, "sourceSize": { @@ -6507,20 +6696,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 6, "y": 6, "w": 20, "h": 20 }, "frame": { - "x": 218, - "y": 321, + "x": 274, + "y": 325, "w": 20, "h": 20 } }, { - "filename": "strange_ball", + "filename": "ub", "rotated": false, "trimmed": true, "sourceSize": { @@ -6534,14 +6723,14 @@ "h": 20 }, "frame": { - "x": 238, - "y": 321, + "x": 294, + "y": 325, "w": 20, "h": 20 } }, { - "filename": "alakazite", + "filename": "spell_tag", "rotated": false, "trimmed": true, "sourceSize": { @@ -6549,20 +6738,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 + "x": 7, + "y": 6, + "w": 19, + "h": 21 }, "frame": { - "x": 139, - "y": 309, - "w": 16, - "h": 16 + "x": 202, + "y": 337, + "w": 19, + "h": 21 } }, { - "filename": "ub", + "filename": "apicot_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -6572,13 +6761,13 @@ "spriteSourceSize": { "x": 6, "y": 6, - "w": 20, + "w": 19, "h": 20 }, "frame": { - "x": 135, - "y": 325, - "w": 20, + "x": 221, + "y": 337, + "w": 19, "h": 20 } }, @@ -6597,14 +6786,14 @@ "h": 19 }, "frame": { - "x": 155, - "y": 322, + "x": 323, + "y": 287, "w": 20, "h": 19 } }, { - "filename": "wl_ability_urge", + "filename": "big_mushroom", "rotated": false, "trimmed": true, "sourceSize": { @@ -6613,19 +6802,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 8, - "w": 20, - "h": 18 + "y": 6, + "w": 19, + "h": 19 }, "frame": { - "x": 175, - "y": 327, - "w": 20, - "h": 18 + "x": 343, + "y": 287, + "w": 19, + "h": 19 } }, { - "filename": "wl_antidote", + "filename": "candy_jar", "rotated": false, "trimmed": true, "sourceSize": { @@ -6634,19 +6823,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 8, - "w": 20, - "h": 18 + "y": 6, + "w": 19, + "h": 20 }, "frame": { - "x": 136, - "y": 345, - "w": 20, - "h": 18 + "x": 362, + "y": 287, + "w": 19, + "h": 20 } }, { - "filename": "baton", + "filename": "hard_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6654,20 +6843,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 + "x": 6, + "y": 6, + "w": 19, + "h": 20 }, "frame": { - "x": 156, - "y": 341, - "w": 18, - "h": 18 + "x": 318, + "y": 306, + "w": 19, + "h": 20 } }, { - "filename": "wl_awakening", + "filename": "miracle_seed", "rotated": false, "trimmed": true, "sourceSize": { @@ -6676,19 +6865,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 8, - "w": 20, - "h": 18 + "y": 7, + "w": 19, + "h": 19 }, "frame": { - "x": 174, - "y": 345, - "w": 20, - "h": 18 + "x": 337, + "y": 306, + "w": 19, + "h": 19 } }, { - "filename": "wl_burn_heal", + "filename": "wl_ability_urge", "rotated": false, "trimmed": true, "sourceSize": { @@ -6702,14 +6891,14 @@ "h": 18 }, "frame": { - "x": 195, - "y": 335, + "x": 314, + "y": 326, "w": 20, "h": 18 } }, { - "filename": "wl_custom_spliced", + "filename": "wl_antidote", "rotated": false, "trimmed": true, "sourceSize": { @@ -6723,14 +6912,14 @@ "h": 18 }, "frame": { - "x": 215, - "y": 341, + "x": 356, + "y": 307, "w": 20, "h": 18 } }, { - "filename": "wl_custom_thief", + "filename": "golden_egg", "rotated": false, "trimmed": true, "sourceSize": { @@ -6738,20 +6927,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 + "x": 7, + "y": 6, + "w": 17, + "h": 20 }, "frame": { - "x": 235, - "y": 341, - "w": 20, - "h": 18 + "x": 376, + "y": 307, + "w": 17, + "h": 20 } }, { - "filename": "wl_elixir", + "filename": "wl_awakening", "rotated": false, "trimmed": true, "sourceSize": { @@ -6765,14 +6954,14 @@ "h": 18 }, "frame": { - "x": 194, - "y": 353, + "x": 393, + "y": 241, "w": 20, "h": 18 } }, { - "filename": "wl_ether", + "filename": "toxic_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6780,20 +6969,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, + "x": 7, + "y": 7, + "w": 18, "h": 18 }, "frame": { - "x": 214, - "y": 359, - "w": 20, + "x": 413, + "y": 258, + "w": 18, "h": 18 } }, { - "filename": "wl_full_heal", + "filename": "wl_burn_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -6807,14 +6996,14 @@ "h": 18 }, "frame": { - "x": 234, - "y": 359, + "x": 393, + "y": 259, "w": 20, "h": 18 } }, { - "filename": "candy", + "filename": "ampharosite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6822,20 +7011,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 11, - "w": 18, - "h": 18 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 156, - "y": 359, - "w": 18, - "h": 18 + "x": 377, + "y": 243, + "w": 16, + "h": 16 } }, { - "filename": "wl_full_restore", + "filename": "audinite", "rotated": false, "trimmed": true, "sourceSize": { @@ -6843,20 +7032,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 8, "y": 8, - "w": 20, - "h": 18 + "w": 16, + "h": 16 }, "frame": { - "x": 174, - "y": 363, - "w": 20, - "h": 18 + "x": 377, + "y": 259, + "w": 16, + "h": 16 } }, { - "filename": "wl_guard_spec", + "filename": "relic_gold", "rotated": false, "trimmed": true, "sourceSize": { @@ -6864,20 +7053,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 + "x": 9, + "y": 11, + "w": 15, + "h": 11 }, "frame": { - "x": 194, - "y": 371, - "w": 20, - "h": 18 + "x": 377, + "y": 275, + "w": 15, + "h": 11 } }, { - "filename": "wl_hyper_potion", + "filename": "lucky_egg", "rotated": false, "trimmed": true, "sourceSize": { @@ -6885,20 +7074,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 + "x": 7, + "y": 6, + "w": 17, + "h": 20 }, "frame": { - "x": 214, - "y": 377, - "w": 20, - "h": 18 + "x": 381, + "y": 286, + "w": 17, + "h": 20 } }, { - "filename": "wl_ice_heal", + "filename": "wl_custom_spliced", "rotated": false, "trimmed": true, "sourceSize": { @@ -6912,35 +7101,14 @@ "h": 18 }, "frame": { - "x": 234, - "y": 377, + "x": 398, + "y": 277, "w": 20, "h": 18 } }, { - "filename": "relic_gold", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 11, - "w": 15, - "h": 11 - }, - "frame": { - "x": 141, - "y": 363, - "w": 15, - "h": 11 - } - }, - { - "filename": "wl_item_drop", + "filename": "wl_custom_thief", "rotated": false, "trimmed": true, "sourceSize": { @@ -6954,14 +7122,14 @@ "h": 18 }, "frame": { - "x": 141, - "y": 377, + "x": 398, + "y": 295, "w": 20, "h": 18 } }, { - "filename": "wl_item_urge", + "filename": "wl_elixir", "rotated": false, "trimmed": true, "sourceSize": { @@ -6975,14 +7143,14 @@ "h": 18 }, "frame": { - "x": 141, - "y": 395, + "x": 393, + "y": 313, "w": 20, "h": 18 } }, { - "filename": "wl_max_elixir", + "filename": "wl_ether", "rotated": false, "trimmed": true, "sourceSize": { @@ -6996,14 +7164,14 @@ "h": 18 }, "frame": { - "x": 161, - "y": 381, + "x": 240, + "y": 340, "w": 20, "h": 18 } }, { - "filename": "wl_max_ether", + "filename": "banettite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7011,20 +7179,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 8, "y": 8, - "w": 20, - "h": 18 + "w": 16, + "h": 16 }, "frame": { - "x": 161, - "y": 399, - "w": 20, - "h": 18 + "x": 413, + "y": 313, + "w": 16, + "h": 16 } }, { - "filename": "wl_max_potion", + "filename": "wl_full_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -7038,14 +7206,14 @@ "h": 18 }, "frame": { - "x": 181, - "y": 389, + "x": 202, + "y": 358, "w": 20, "h": 18 } }, { - "filename": "wl_max_revive", + "filename": "wl_full_restore", "rotated": false, "trimmed": true, "sourceSize": { @@ -7059,14 +7227,14 @@ "h": 18 }, "frame": { - "x": 181, - "y": 407, + "x": 201, + "y": 376, "w": 20, "h": 18 } }, { - "filename": "wl_paralyze_heal", + "filename": "wl_guard_spec", "rotated": false, "trimmed": true, "sourceSize": { @@ -7081,13 +7249,13 @@ }, "frame": { "x": 201, - "y": 395, + "y": 394, "w": 20, "h": 18 } }, { - "filename": "wl_potion", + "filename": "wl_hyper_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -7101,14 +7269,14 @@ "h": 18 }, "frame": { - "x": 221, - "y": 395, + "x": 201, + "y": 412, "w": 20, "h": 18 } }, { - "filename": "dark_stone", + "filename": "beedrillite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7116,20 +7284,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 18 + "x": 8, + "y": 8, + "w": 16, + "h": 16 }, "frame": { - "x": 241, - "y": 395, - "w": 18, - "h": 18 + "x": 222, + "y": 357, + "w": 16, + "h": 16 } }, { - "filename": "flame_orb", + "filename": "wl_ice_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -7137,20 +7305,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, + "x": 6, + "y": 8, + "w": 20, "h": 18 }, "frame": { - "x": 255, - "y": 341, - "w": 18, + "x": 238, + "y": 358, + "w": 20, "h": 18 } }, { - "filename": "wl_reset_urge", + "filename": "blastoisinite", "rotated": false, "trimmed": true, "sourceSize": { @@ -7158,20 +7326,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, + "x": 8, "y": 8, - "w": 20, - "h": 18 + "w": 16, + "h": 16 }, "frame": { - "x": 254, - "y": 359, - "w": 20, - "h": 18 + "x": 222, + "y": 373, + "w": 16, + "h": 16 } }, { - "filename": "wl_revive", + "filename": "wl_item_drop", "rotated": false, "trimmed": true, "sourceSize": { @@ -7185,14 +7353,14 @@ "h": 18 }, "frame": { - "x": 254, - "y": 377, + "x": 221, + "y": 389, "w": 20, "h": 18 } }, { - "filename": "light_ball", + "filename": "wl_item_urge", "rotated": false, "trimmed": true, "sourceSize": { @@ -7200,20 +7368,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, + "x": 6, + "y": 8, + "w": 20, "h": 18 }, "frame": { - "x": 259, - "y": 395, - "w": 18, + "x": 221, + "y": 407, + "w": 20, "h": 18 } }, { - "filename": "wl_super_potion", + "filename": "wl_max_elixir", "rotated": false, "trimmed": true, "sourceSize": { @@ -7227,14 +7395,14 @@ "h": 18 }, "frame": { - "x": 259, - "y": 320, + "x": 241, + "y": 376, "w": 20, "h": 18 } }, { - "filename": "light_stone", + "filename": "wl_max_ether", "rotated": false, "trimmed": true, "sourceSize": { @@ -7242,20 +7410,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, + "x": 6, + "y": 8, + "w": 20, "h": 18 }, "frame": { - "x": 279, - "y": 320, - "w": 18, + "x": 241, + "y": 394, + "w": 20, "h": 18 } }, { - "filename": "toxic_orb", + "filename": "wl_max_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -7263,20 +7431,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, + "x": 6, + "y": 8, + "w": 20, "h": 18 }, "frame": { - "x": 297, - "y": 320, - "w": 18, + "x": 241, + "y": 412, + "w": 20, "h": 18 } }, { - "filename": "altarianite", + "filename": "wl_max_revive", "rotated": false, "trimmed": true, "sourceSize": { @@ -7284,20 +7452,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 315, - "y": 320, - "w": 16, - "h": 16 + "x": 258, + "y": 358, + "w": 20, + "h": 18 } }, { - "filename": "ampharosite", + "filename": "wl_paralyze_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -7305,20 +7473,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 273, - "y": 338, - "w": 16, - "h": 16 + "x": 261, + "y": 376, + "w": 20, + "h": 18 } }, { - "filename": "audinite", + "filename": "wl_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -7326,20 +7494,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 289, - "y": 338, - "w": 16, - "h": 16 + "x": 261, + "y": 394, + "w": 20, + "h": 18 } }, { - "filename": "banettite", + "filename": "wl_reset_urge", "rotated": false, "trimmed": true, "sourceSize": { @@ -7347,20 +7515,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 274, - "y": 354, - "w": 16, - "h": 16 + "x": 261, + "y": 412, + "w": 20, + "h": 18 } }, { - "filename": "beedrillite", + "filename": "wl_revive", "rotated": false, "trimmed": true, "sourceSize": { @@ -7368,20 +7536,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 274, - "y": 370, - "w": 16, - "h": 16 + "x": 278, + "y": 345, + "w": 20, + "h": 18 } }, { - "filename": "blastoisinite", + "filename": "wl_super_potion", "rotated": false, "trimmed": true, "sourceSize": { @@ -7389,16 +7557,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 6, "y": 8, - "w": 16, - "h": 16 + "w": 20, + "h": 18 }, "frame": { - "x": 290, - "y": 354, - "w": 16, - "h": 16 + "x": 298, + "y": 345, + "w": 20, + "h": 18 } }, { @@ -7416,8 +7584,8 @@ "h": 16 }, "frame": { - "x": 290, - "y": 370, + "x": 318, + "y": 344, "w": 16, "h": 16 } @@ -7437,8 +7605,8 @@ "h": 16 }, "frame": { - "x": 305, - "y": 338, + "x": 334, + "y": 326, "w": 16, "h": 16 } @@ -7458,8 +7626,8 @@ "h": 16 }, "frame": { - "x": 306, - "y": 354, + "x": 334, + "y": 342, "w": 16, "h": 16 } @@ -7479,8 +7647,8 @@ "h": 16 }, "frame": { - "x": 306, - "y": 370, + "x": 350, + "y": 325, "w": 16, "h": 16 } @@ -7500,8 +7668,8 @@ "h": 16 }, "frame": { - "x": 331, - "y": 320, + "x": 350, + "y": 341, "w": 16, "h": 16 } @@ -7521,8 +7689,8 @@ "h": 16 }, "frame": { - "x": 321, - "y": 336, + "x": 366, + "y": 327, "w": 16, "h": 16 } @@ -7542,8 +7710,8 @@ "h": 16 }, "frame": { - "x": 322, - "y": 352, + "x": 366, + "y": 343, "w": 16, "h": 16 } @@ -7563,8 +7731,8 @@ "h": 16 }, "frame": { - "x": 322, - "y": 368, + "x": 382, + "y": 331, "w": 16, "h": 16 } @@ -7584,8 +7752,8 @@ "h": 16 }, "frame": { - "x": 337, - "y": 336, + "x": 398, + "y": 331, "w": 16, "h": 16 } @@ -7605,8 +7773,8 @@ "h": 16 }, "frame": { - "x": 338, - "y": 352, + "x": 414, + "y": 329, "w": 16, "h": 16 } @@ -7626,8 +7794,8 @@ "h": 16 }, "frame": { - "x": 338, - "y": 368, + "x": 382, + "y": 347, "w": 16, "h": 16 } @@ -7647,8 +7815,8 @@ "h": 16 }, "frame": { - "x": 347, - "y": 313, + "x": 398, + "y": 347, "w": 16, "h": 16 } @@ -7668,8 +7836,8 @@ "h": 16 }, "frame": { - "x": 277, - "y": 386, + "x": 414, + "y": 345, "w": 16, "h": 16 } @@ -7689,8 +7857,8 @@ "h": 16 }, "frame": { - "x": 293, - "y": 386, + "x": 281, + "y": 363, "w": 16, "h": 16 } @@ -7710,8 +7878,8 @@ "h": 16 }, "frame": { - "x": 309, - "y": 386, + "x": 281, + "y": 379, "w": 16, "h": 16 } @@ -7731,8 +7899,8 @@ "h": 16 }, "frame": { - "x": 325, - "y": 384, + "x": 297, + "y": 363, "w": 16, "h": 16 } @@ -7752,8 +7920,8 @@ "h": 16 }, "frame": { - "x": 341, - "y": 384, + "x": 281, + "y": 395, "w": 16, "h": 16 } @@ -7773,8 +7941,8 @@ "h": 16 }, "frame": { - "x": 277, - "y": 402, + "x": 297, + "y": 379, "w": 16, "h": 16 } @@ -7794,8 +7962,8 @@ "h": 16 }, "frame": { - "x": 293, - "y": 402, + "x": 281, + "y": 411, "w": 16, "h": 16 } @@ -7815,8 +7983,8 @@ "h": 16 }, "frame": { - "x": 309, - "y": 402, + "x": 297, + "y": 395, "w": 16, "h": 16 } @@ -7836,8 +8004,8 @@ "h": 16 }, "frame": { - "x": 325, - "y": 400, + "x": 297, + "y": 411, "w": 16, "h": 16 } @@ -7857,8 +8025,8 @@ "h": 16 }, "frame": { - "x": 341, - "y": 400, + "x": 313, + "y": 363, "w": 16, "h": 16 } @@ -7878,8 +8046,8 @@ "h": 16 }, "frame": { - "x": 353, - "y": 329, + "x": 313, + "y": 379, "w": 16, "h": 16 } @@ -7899,8 +8067,8 @@ "h": 16 }, "frame": { - "x": 369, - "y": 322, + "x": 313, + "y": 395, "w": 16, "h": 16 } @@ -7920,8 +8088,8 @@ "h": 16 }, "frame": { - "x": 354, - "y": 345, + "x": 313, + "y": 411, "w": 16, "h": 16 } @@ -7941,8 +8109,8 @@ "h": 16 }, "frame": { - "x": 354, - "y": 361, + "x": 350, + "y": 357, "w": 16, "h": 16 } @@ -7962,8 +8130,8 @@ "h": 16 }, "frame": { - "x": 385, - "y": 329, + "x": 366, + "y": 359, "w": 16, "h": 16 } @@ -7983,8 +8151,8 @@ "h": 16 }, "frame": { - "x": 401, - "y": 332, + "x": 334, + "y": 358, "w": 16, "h": 16 } @@ -8004,8 +8172,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 377, + "x": 382, + "y": 363, "w": 16, "h": 16 } @@ -8025,8 +8193,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 393, + "x": 398, + "y": 363, "w": 16, "h": 16 } @@ -8046,8 +8214,8 @@ "h": 16 }, "frame": { - "x": 357, - "y": 409, + "x": 414, + "y": 361, "w": 16, "h": 16 } @@ -8067,8 +8235,8 @@ "h": 16 }, "frame": { - "x": 370, - "y": 345, + "x": 329, + "y": 374, "w": 16, "h": 16 } @@ -8088,8 +8256,8 @@ "h": 16 }, "frame": { - "x": 370, - "y": 361, + "x": 329, + "y": 390, "w": 16, "h": 16 } @@ -8109,8 +8277,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 377, + "x": 329, + "y": 406, "w": 16, "h": 16 } @@ -8130,8 +8298,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 393, + "x": 345, + "y": 374, "w": 16, "h": 16 } @@ -8151,8 +8319,8 @@ "h": 16 }, "frame": { - "x": 373, - "y": 409, + "x": 345, + "y": 390, "w": 16, "h": 16 } @@ -8172,8 +8340,8 @@ "h": 16 }, "frame": { - "x": 386, - "y": 348, + "x": 345, + "y": 406, "w": 16, "h": 16 } @@ -8193,8 +8361,8 @@ "h": 16 }, "frame": { - "x": 402, - "y": 348, + "x": 361, + "y": 375, "w": 16, "h": 16 } @@ -8214,8 +8382,8 @@ "h": 16 }, "frame": { - "x": 389, - "y": 364, + "x": 361, + "y": 391, "w": 16, "h": 16 } @@ -8235,8 +8403,8 @@ "h": 16 }, "frame": { - "x": 389, - "y": 380, + "x": 361, + "y": 407, "w": 16, "h": 16 } @@ -8247,6 +8415,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:c004184e48566e1da6f13477a3348fd3:dc1a5489f7821641aade35ba290bbea7:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:934ea4080bad980d4fea720cc771f133:ed564bc47b79b15a763de57045178e88:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index 4c366e4d72ad..5f032b30cfb0 100644 Binary files a/public/images/items.png and b/public/images/items.png differ diff --git a/public/images/items/berry_juice.png b/public/images/items/berry_juice.png new file mode 100644 index 000000000000..c0986b804f9f Binary files /dev/null and b/public/images/items/berry_juice.png differ diff --git a/public/images/items/black_sludge.png b/public/images/items/black_sludge.png new file mode 100644 index 000000000000..39684a403108 Binary files /dev/null and b/public/images/items/black_sludge.png differ diff --git a/public/images/items/golden_net.png b/public/images/items/golden_net.png new file mode 100644 index 000000000000..5fea1ee7dba1 Binary files /dev/null and b/public/images/items/golden_net.png differ diff --git a/public/images/items/leaders_crest.png b/public/images/items/leaders_crest.png new file mode 100644 index 000000000000..45cf16563747 Binary files /dev/null and b/public/images/items/leaders_crest.png differ diff --git a/public/images/items/macho_brace.png b/public/images/items/macho_brace.png new file mode 100644 index 000000000000..2085829e1ce7 Binary files /dev/null and b/public/images/items/macho_brace.png differ diff --git a/public/images/items/moon_flute.png b/public/images/items/moon_flute.png new file mode 100644 index 000000000000..893cb6a75796 Binary files /dev/null and b/public/images/items/moon_flute.png differ diff --git a/public/images/items/old_gateau.png b/public/images/items/old_gateau.png new file mode 100644 index 000000000000..c910e90f1016 Binary files /dev/null and b/public/images/items/old_gateau.png differ diff --git a/public/images/items/sun_flute.png b/public/images/items/sun_flute.png new file mode 100644 index 000000000000..7010c9fefbd9 Binary files /dev/null and b/public/images/items/sun_flute.png differ diff --git a/public/images/mystery-encounters/b2w2_lady.json b/public/images/mystery-encounters/b2w2_lady.json new file mode 100644 index 000000000000..e143086e1572 --- /dev/null +++ b/public/images/mystery-encounters/b2w2_lady.json @@ -0,0 +1,734 @@ +{ + "textures": [ + { + "image": "b2w2_lady.png", + "format": "RGBA8888", + "size": { + "w": 399, + "h": 360 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 57, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 171, + "y": 0, + "w": 55, + "h": 72 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 228, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 285, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 14, + "y": 8, + "w": 52, + "h": 72 + }, + "frame": { + "x": 342, + "y": 0, + "w": 52, + "h": 72 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 57, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 114, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 171, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 0, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 57, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 114, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 171, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 216, + "w": 48, + "h": 72 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 50, + "h": 72 + }, + "frame": { + "x": 57, + "y": 216, + "w": 50, + "h": 72 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 114, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 171, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 53, + "h": 72 + }, + "frame": { + "x": 228, + "y": 216, + "w": 53, + "h": 72 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 57, + "h": 72 + }, + "frame": { + "x": 285, + "y": 216, + "w": 57, + "h": 72 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 342, + "y": 216, + "w": 56, + "h": 72 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 57, + "y": 288, + "w": 55, + "h": 72 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 171, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 228, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 285, + "y": 288, + "w": 56, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e7f062304401dbd7b3ec79512f0ff4cb:0136dac01331f88892a3df26aeab78f5:1ed1e22abb9b55d76337a5a599835c06$" + } +} diff --git a/public/images/mystery-encounters/b2w2_lady.png b/public/images/mystery-encounters/b2w2_lady.png new file mode 100644 index 000000000000..9dcc1281c9eb Binary files /dev/null and b/public/images/mystery-encounters/b2w2_lady.png differ diff --git a/public/images/mystery-encounters/b2w2_veteran_m.json b/public/images/mystery-encounters/b2w2_veteran_m.json new file mode 100644 index 000000000000..8f07c7d44e21 --- /dev/null +++ b/public/images/mystery-encounters/b2w2_veteran_m.json @@ -0,0 +1,797 @@ +{ + "textures": [ + { + "image": "b2w2_veteran_m.png", + "format": "RGBA8888", + "size": { + "w": 424, + "h": 390 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 53, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 106, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 159, + "y": 0, + "w": 43, + "h": 78 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 212, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 265, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 318, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 371, + "y": 0, + "w": 44, + "h": 78 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 0, + "y": 78, + "w": 44, + "h": 78 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 53, + "y": 78, + "w": 44, + "h": 78 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 48, + "h": 78 + }, + "frame": { + "x": 106, + "y": 78, + "w": 48, + "h": 78 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 50, + "h": 78 + }, + "frame": { + "x": 159, + "y": 78, + "w": 50, + "h": 78 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 212, + "y": 78, + "w": 53, + "h": 78 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 265, + "y": 78, + "w": 53, + "h": 78 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 318, + "y": 78, + "w": 52, + "h": 78 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 371, + "y": 78, + "w": 51, + "h": 78 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 0, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 53, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 106, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 159, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 53, + "h": 78 + }, + "frame": { + "x": 212, + "y": 156, + "w": 53, + "h": 78 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 52, + "h": 78 + }, + "frame": { + "x": 265, + "y": 156, + "w": 52, + "h": 78 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 318, + "y": 156, + "w": 51, + "h": 78 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 371, + "y": 156, + "w": 51, + "h": 78 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 51, + "h": 78 + }, + "frame": { + "x": 0, + "y": 234, + "w": 51, + "h": 78 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 50, + "h": 78 + }, + "frame": { + "x": 53, + "y": 234, + "w": 50, + "h": 78 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 48, + "h": 78 + }, + "frame": { + "x": 106, + "y": 234, + "w": 48, + "h": 78 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 46, + "h": 78 + }, + "frame": { + "x": 159, + "y": 234, + "w": 46, + "h": 78 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 46, + "h": 78 + }, + "frame": { + "x": 212, + "y": 234, + "w": 46, + "h": 78 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 265, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 318, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 371, + "y": 234, + "w": 44, + "h": 78 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 44, + "h": 78 + }, + "frame": { + "x": 0, + "y": 312, + "w": 44, + "h": 78 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 53, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0034.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 106, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0035.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 159, + "y": 312, + "w": 43, + "h": 78 + } + }, + { + "filename": "0036.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 43, + "h": 78 + }, + "frame": { + "x": 212, + "y": 312, + "w": 43, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4deb068879a8ac195cb4f00c8b17b7f5:b32f0f90436649264b6f3c49b09ac06a:05e903aa75b8e50c28334d9b5e14c85a$" + } +} diff --git a/public/images/mystery-encounters/b2w2_veteran_m.png b/public/images/mystery-encounters/b2w2_veteran_m.png new file mode 100644 index 000000000000..967d82973e61 Binary files /dev/null and b/public/images/mystery-encounters/b2w2_veteran_m.png differ diff --git a/public/images/mystery-encounters/bait.json b/public/images/mystery-encounters/bait.json new file mode 100644 index 000000000000..ae9ee38ee138 --- /dev/null +++ b/public/images/mystery-encounters/bait.json @@ -0,0 +1,83 @@ +{ + "textures": [ + { + "image": "bait.png", + "format": "RGBA8888", + "size": { + "w": 14, + "h": 43 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 12, + "h": 13 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 16, + "w": 12, + "h": 13 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 5, + "w": 11, + "h": 11 + }, + "frame": { + "x": 1, + "y": 31, + "w": 11, + "h": 11 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:f0ec04fcd67ac346dce973693711d032:b697e09191c4312b8faaa0a080a309b7:1af241a52e61fa01ca849aa03c112f85$" + } +} diff --git a/public/images/mystery-encounters/bait.png b/public/images/mystery-encounters/bait.png new file mode 100644 index 000000000000..7de9169d1875 Binary files /dev/null and b/public/images/mystery-encounters/bait.png differ diff --git a/public/images/mystery-encounters/berry_bush.json b/public/images/mystery-encounters/berry_bush.json new file mode 100644 index 000000000000..397538d8af2d --- /dev/null +++ b/public/images/mystery-encounters/berry_bush.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "berry_bush.png", + "format": "RGBA8888", + "size": { + "w": 49, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 49, + "h": 53 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 49, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 49, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d5f83625477b5f98b726343f4a3a396f:f4665258986e97345cfeee041b4b8bcf:e7781fcc447e6d12deb2af78c9493c7f$" + } +} diff --git a/public/images/mystery-encounters/berry_bush.png b/public/images/mystery-encounters/berry_bush.png new file mode 100644 index 000000000000..e9be20b4863b Binary files /dev/null and b/public/images/mystery-encounters/berry_bush.png differ diff --git a/public/images/mystery-encounters/buoy.json b/public/images/mystery-encounters/buoy.json new file mode 100644 index 000000000000..ba5d9567fe5d --- /dev/null +++ b/public/images/mystery-encounters/buoy.json @@ -0,0 +1,19 @@ +{ "frames": [ + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 46, "h": 60 }, + "rotated": false, + "trimmed": false, + "spriteSourceSize": { "x": 0, "y": 0, "w": 46, "h": 60 }, + "sourceSize": { "w": 46, "h": 60 } + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "buoy-sheet.png", + "format": "RGBA8888", + "size": { "w": 46, "h": 60 }, + "scale": "1" + } +} diff --git a/public/images/mystery-encounters/buoy.png b/public/images/mystery-encounters/buoy.png new file mode 100644 index 000000000000..fb957ac29f04 Binary files /dev/null and b/public/images/mystery-encounters/buoy.png differ diff --git a/public/images/mystery-encounters/carnival_game.json b/public/images/mystery-encounters/carnival_game.json new file mode 100644 index 000000000000..0572b95990ce --- /dev/null +++ b/public/images/mystery-encounters/carnival_game.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_game.png", + "format": "RGBA8888", + "size": { + "w": 38, + "h": 82 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 38, + "h": 82 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + }, + "frame": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d40b6742392c2fe8ca0735b3f561e319:5dcda5410b12f0aa75eb0dd1fbcbe4f9:d171fb17d3017d1f655cd8dd14c252b7$" + } +} diff --git a/public/images/mystery-encounters/carnival_game.png b/public/images/mystery-encounters/carnival_game.png new file mode 100644 index 000000000000..03a3b9c9cbcb Binary files /dev/null and b/public/images/mystery-encounters/carnival_game.png differ diff --git a/public/images/mystery-encounters/carnival_man.json b/public/images/mystery-encounters/carnival_man.json new file mode 100644 index 000000000000..3e77765bbce0 --- /dev/null +++ b/public/images/mystery-encounters/carnival_man.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_man.png", + "format": "RGBA8888", + "size": { + "w": 50, + "h": 77 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 3, + "w": 50, + "h": 77 + }, + "frame": { + "x": 0, + "y": 0, + "w": 50, + "h": 77 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e80aa9a809a7cca6d05992cb82f6dbd9:ea9962edd1cdc1e503deecf2ce1863c1:55647352b6547cf03212506309f2abf5$" + } +} diff --git a/public/images/mystery-encounters/carnival_man.png b/public/images/mystery-encounters/carnival_man.png new file mode 100644 index 000000000000..05f94dbd33df Binary files /dev/null and b/public/images/mystery-encounters/carnival_man.png differ diff --git a/public/images/mystery-encounters/carnival_wobbuffet.json b/public/images/mystery-encounters/carnival_wobbuffet.json new file mode 100644 index 000000000000..c059bb35a963 --- /dev/null +++ b/public/images/mystery-encounters/carnival_wobbuffet.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_wobbuffet.png", + "format": "RGBA8888", + "size": { + "w": 45, + "h": 55 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 45, + "h": 55 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + }, + "frame": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:879de17da906ea52e5a71afacb88fcf6:90f64e8eaac4ff1e67373f60c3d98d36:a090cb3294ca1218a4f90ecb97df81d7$" + } +} diff --git a/public/images/mystery-encounters/carnival_wobbuffet.png b/public/images/mystery-encounters/carnival_wobbuffet.png new file mode 100644 index 000000000000..37e7220196a7 Binary files /dev/null and b/public/images/mystery-encounters/carnival_wobbuffet.png differ diff --git a/public/images/mystery-encounters/chest_blue.json b/public/images/mystery-encounters/chest_blue.json new file mode 100644 index 000000000000..916afc3242cd --- /dev/null +++ b/public/images/mystery-encounters/chest_blue.json @@ -0,0 +1,209 @@ +{ + "textures": [ + { + "image": "chest_blue.png", + "format": "RGBA8888", + "size": { + "w": 54, + "h": 492 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 47, + "h": 35 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 47, + "h": 35 + }, + "frame": { + "x": 0, + "y": 39, + "w": 47, + "h": 35 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 74, + "w": 46, + "h": 39 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 46 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 46 + }, + "frame": { + "x": 0, + "y": 113, + "w": 46, + "h": 46 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 53, + "h": 65 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 53, + "h": 65 + }, + "frame": { + "x": 0, + "y": 159, + "w": 53, + "h": 65 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 224, + "w": 54, + "h": 67 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 291, + "w": 54, + "h": 67 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 358, + "w": 54, + "h": 67 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 425, + "w": 54, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:017ecc2437e580a185f9843f97e80da5:f44ef1c27a4a17183a5bcf1f7fc8ce6a:f4f3c064e6c93b8d1290f93bee927f60$" + } +} diff --git a/public/images/mystery-encounters/chest_blue.png b/public/images/mystery-encounters/chest_blue.png new file mode 100644 index 000000000000..e67bdcafa045 Binary files /dev/null and b/public/images/mystery-encounters/chest_blue.png differ diff --git a/public/images/mystery-encounters/chest_red.json b/public/images/mystery-encounters/chest_red.json new file mode 100644 index 000000000000..579cf7bda061 --- /dev/null +++ b/public/images/mystery-encounters/chest_red.json @@ -0,0 +1,209 @@ +{ + "textures": [ + { + "image": "chest_red.png", + "format": "RGBA8888", + "size": { + "w": 54, + "h": 492 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 47, + "h": 35 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 47, + "h": 35 + }, + "frame": { + "x": 0, + "y": 39, + "w": 47, + "h": 35 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 39 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 39 + }, + "frame": { + "x": 0, + "y": 74, + "w": 46, + "h": 39 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 46 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 46 + }, + "frame": { + "x": 0, + "y": 113, + "w": 46, + "h": 46 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 53, + "h": 65 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 53, + "h": 65 + }, + "frame": { + "x": 0, + "y": 159, + "w": 53, + "h": 65 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 224, + "w": 54, + "h": 67 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 291, + "w": 54, + "h": 67 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 358, + "w": 54, + "h": 67 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 54, + "h": 67 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 54, + "h": 67 + }, + "frame": { + "x": 0, + "y": 425, + "w": 54, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:2a0b6c93c5be115efa635d40780603f0:b5fde49f991c2ecc49afedd80cc8a544:a163d960e9966469ae4dde4b53c13496$" + } +} diff --git a/public/images/mystery-encounters/chest_red.png b/public/images/mystery-encounters/chest_red.png new file mode 100644 index 000000000000..c20a8218be64 Binary files /dev/null and b/public/images/mystery-encounters/chest_red.png differ diff --git a/public/images/mystery-encounters/dark_deal_porygon.json b/public/images/mystery-encounters/dark_deal_porygon.json new file mode 100644 index 000000000000..5a48d95c18d4 --- /dev/null +++ b/public/images/mystery-encounters/dark_deal_porygon.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "dark_deal_porygon.png", + "format": "RGBA8888", + "size": { + "w": 36, + "h": 45 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 36, + "h": 45 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 44, + "h": 44 + }, + "frame": { + "x": 0, + "y": 0, + "w": 36, + "h": 45 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + } +} diff --git a/public/images/mystery-encounters/dark_deal_porygon.png b/public/images/mystery-encounters/dark_deal_porygon.png new file mode 100644 index 000000000000..168999fb0f46 Binary files /dev/null and b/public/images/mystery-encounters/dark_deal_porygon.png differ diff --git a/public/images/mystery-encounters/encounter_radar.png b/public/images/mystery-encounters/encounter_radar.png new file mode 100644 index 000000000000..deb9426c2696 Binary files /dev/null and b/public/images/mystery-encounters/encounter_radar.png differ diff --git a/public/images/mystery-encounters/exclaim.png b/public/images/mystery-encounters/exclaim.png new file mode 100644 index 000000000000..a7727f4da2e1 Binary files /dev/null and b/public/images/mystery-encounters/exclaim.png differ diff --git a/public/images/mystery-encounters/global_trade_system.json b/public/images/mystery-encounters/global_trade_system.json new file mode 100644 index 000000000000..ae5d96127b77 --- /dev/null +++ b/public/images/mystery-encounters/global_trade_system.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "global_trade_system.png", + "format": "RGBA8888", + "size": { + "w": 77, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 77, + "h": 78 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 77, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 77, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:8a51d7a17b3d8c32f0e5e4a0f15daeb4:6eba29c5345847f735d8b69a05fc49d1:98ad8b8b8d8c4865d7d23ec97b516594$" + } +} diff --git a/public/images/mystery-encounters/global_trade_system.png b/public/images/mystery-encounters/global_trade_system.png new file mode 100644 index 000000000000..cb0ffb0ab200 Binary files /dev/null and b/public/images/mystery-encounters/global_trade_system.png differ diff --git a/public/images/mystery-encounters/mad_scientist_m.json b/public/images/mystery-encounters/mad_scientist_m.json new file mode 100644 index 000000000000..10aa3d6f42af --- /dev/null +++ b/public/images/mystery-encounters/mad_scientist_m.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "mad_scientist_m.png", + "format": "RGBA8888", + "size": { + "w": 46, + "h": 76 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 44, + "h": 74 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 44, + "h": 74 + }, + "frame": { + "x": 1, + "y": 1, + "w": 44, + "h": 74 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:a7f8ff2bbb362868f51125c254eb6681:cf76e61ddd31a8f46af67ced168c44a2:4fc09abe16c0608828269e5da81d0744$" + } +} diff --git a/public/images/mystery-encounters/mad_scientist_m.png b/public/images/mystery-encounters/mad_scientist_m.png new file mode 100644 index 000000000000..453cb767ec14 Binary files /dev/null and b/public/images/mystery-encounters/mad_scientist_m.png differ diff --git a/public/images/mystery-encounters/mud.json b/public/images/mystery-encounters/mud.json new file mode 100644 index 000000000000..505a6fadd27c --- /dev/null +++ b/public/images/mystery-encounters/mud.json @@ -0,0 +1,104 @@ +{ + "textures": [ + { + "image": "mud.png", + "format": "RGBA8888", + "size": { + "w": 14, + "h": 68 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 12, + "h": 13 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 12, + "h": 14 + }, + "frame": { + "x": 1, + "y": 16, + "w": 12, + "h": 14 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 12, + "h": 16 + }, + "frame": { + "x": 1, + "y": 32, + "w": 12, + "h": 16 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 20 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 17 + }, + "frame": { + "x": 1, + "y": 50, + "w": 12, + "h": 17 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4f18a8effb8f01eb70f9f25b8294c1bf:ad663a73c51f780bbf45d00a52519553:c64f6b8befc3d5e9f836246d2b9536be$" + } +} diff --git a/public/images/mystery-encounters/mud.png b/public/images/mystery-encounters/mud.png new file mode 100644 index 000000000000..2ba7cb00047a Binary files /dev/null and b/public/images/mystery-encounters/mud.png differ diff --git a/public/images/mystery-encounters/pokemon_salesman.json b/public/images/mystery-encounters/pokemon_salesman.json new file mode 100644 index 000000000000..23d9df44f2b2 --- /dev/null +++ b/public/images/mystery-encounters/pokemon_salesman.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "pokemon_salesman.png", + "format": "RGBA8888", + "size": { + "w": 40, + "h": 80 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 21, + "y": 2, + "w": 38, + "h": 78 + }, + "frame": { + "x": 1, + "y": 1, + "w": 38, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:dd57e3db21f3933c15be65bec261f4c1:05c7ef32252a5c2d3ad007b7e26fabd7:ae82f52e471ed81e2558206f05476cd7$" + } +} diff --git a/public/images/mystery-encounters/pokemon_salesman.png b/public/images/mystery-encounters/pokemon_salesman.png new file mode 100644 index 000000000000..1251dd8eda70 Binary files /dev/null and b/public/images/mystery-encounters/pokemon_salesman.png differ diff --git a/public/images/mystery-encounters/safari_zone.json b/public/images/mystery-encounters/safari_zone.json new file mode 100644 index 000000000000..fe81d1b9f539 --- /dev/null +++ b/public/images/mystery-encounters/safari_zone.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "safari_zone.png", + "format": "RGBA8888", + "size": { + "w": 120, + "h": 84 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 118, + "h": 82 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 118, + "h": 82 + }, + "frame": { + "x": 1, + "y": 1, + "w": 118, + "h": 82 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:6fad7a61e47043b974153148b4fd3997:5ec4d0890f2f03446daf22c8ae8ba77b:87aa745cd95eef6cbf38935230f4e10f$" + } +} diff --git a/public/images/mystery-encounters/safari_zone.png b/public/images/mystery-encounters/safari_zone.png new file mode 100644 index 000000000000..375d66ebbe98 Binary files /dev/null and b/public/images/mystery-encounters/safari_zone.png differ diff --git a/public/images/mystery-encounters/teacher.json b/public/images/mystery-encounters/teacher.json new file mode 100644 index 000000000000..457d440a010c --- /dev/null +++ b/public/images/mystery-encounters/teacher.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "teacher.png", + "format": "RGBA8888", + "size": { + "w": 43, + "h": 74 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 19, + "y": 8, + "w": 41, + "h": 72 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:506e5a4ce79c134a7b4af90a90aef244:1b81d3d84bf12cedc419805eaff82548:59bc5dd000b5e72588320b473e31c312$" + } +} diff --git a/public/images/mystery-encounters/teacher.png b/public/images/mystery-encounters/teacher.png new file mode 100644 index 000000000000..b4332bc0032c Binary files /dev/null and b/public/images/mystery-encounters/teacher.png differ diff --git a/public/images/mystery-encounters/teleporter.json b/public/images/mystery-encounters/teleporter.json new file mode 100644 index 000000000000..4fe45807be2c --- /dev/null +++ b/public/images/mystery-encounters/teleporter.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "teleporter.png", + "format": "RGBA8888", + "size": { + "w": 74, + "h": 79 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 74, + "h": 79 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 74, + "h": 79 + }, + "frame": { + "x": 0, + "y": 0, + "w": 74, + "h": 79 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:937d8502b98f79720118061b6021e108:2b4f9db00d5b0997b42a5466f808509b:ce1615396ce7b0a146766d50b319bb81$" + } +} diff --git a/public/images/mystery-encounters/teleporter.png b/public/images/mystery-encounters/teleporter.png new file mode 100644 index 000000000000..9a049c30ab17 Binary files /dev/null and b/public/images/mystery-encounters/teleporter.png differ diff --git a/public/images/mystery-encounters/training_gear.json b/public/images/mystery-encounters/training_gear.json new file mode 100644 index 000000000000..fb8f4ec9c8ea --- /dev/null +++ b/public/images/mystery-encounters/training_gear.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "training_gear.png", + "format": "RGBA8888", + "size": { + "w": 76, + "h": 57 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 76, + "h": 57 + }, + "spriteSourceSize": { + "x": 10, + "y": 3, + "w": 56, + "h": 54 + }, + "frame": { + "x": 8, + "y": 0, + "w": 56, + "h": 54 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + } +} diff --git a/public/images/mystery-encounters/training_gear.png b/public/images/mystery-encounters/training_gear.png new file mode 100644 index 000000000000..42c3a9bb7d4e Binary files /dev/null and b/public/images/mystery-encounters/training_gear.png differ diff --git a/public/images/mystery-encounters/warehouse_crate.json b/public/images/mystery-encounters/warehouse_crate.json new file mode 100644 index 000000000000..fa86d1a511de --- /dev/null +++ b/public/images/mystery-encounters/warehouse_crate.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "warehouse_crate.png", + "format": "RGBA8888", + "size": { + "w": 71, + "h": 52 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 56 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 71, + "h": 52 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 52 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:c8df5f0b35fb9c2a69b0e4aaa9fa9f91:f1d4643c26f2aed86ad77d354e669aaf:0c073e3c2048ea0779db9429e5e1d8bc$" + } +} diff --git a/public/images/mystery-encounters/warehouse_crate.png b/public/images/mystery-encounters/warehouse_crate.png new file mode 100644 index 000000000000..fb70a6e534a8 Binary files /dev/null and b/public/images/mystery-encounters/warehouse_crate.png differ diff --git a/public/images/mystery-encounters/weird_dream_woman.json b/public/images/mystery-encounters/weird_dream_woman.json new file mode 100644 index 000000000000..66a9b8d68dbf --- /dev/null +++ b/public/images/mystery-encounters/weird_dream_woman.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "weird_dream_woman.png", + "format": "RGBA8888", + "size": { + "w": 78, + "h": 87 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 87 + }, + "spriteSourceSize": { + "x": 1, + "y": 0, + "w": 78, + "h": 87 + }, + "frame": { + "x": 0, + "y": 0, + "w": 78, + "h": 87 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d3cce87ee0e3a880d840bffe9373d5d4:7c776d33b75abad1fe36b14a5e5734af:56468b7a2883e66dadcd2af13ebd8010$" + } +} diff --git a/public/images/mystery-encounters/weird_dream_woman.png b/public/images/mystery-encounters/weird_dream_woman.png new file mode 100644 index 000000000000..1b8d142ed5b2 Binary files /dev/null and b/public/images/mystery-encounters/weird_dream_woman.png differ diff --git a/public/images/pokemon/exp/back/745.png b/public/images/pokemon/exp/back/745.png index 46a354be8a4b..f49491351649 100644 Binary files a/public/images/pokemon/exp/back/745.png and b/public/images/pokemon/exp/back/745.png differ diff --git a/public/images/pokemon/exp/back/shiny/745.json b/public/images/pokemon/exp/back/shiny/745.json index 8e83c592ce47..4867604448d4 100644 --- a/public/images/pokemon/exp/back/shiny/745.json +++ b/public/images/pokemon/exp/back/shiny/745.json @@ -1,440 +1,230 @@ -{ - "textures": [ - { - "image": "745.png", - "format": "RGBA8888", - "size": { - "w": 300, - "h": 300 - }, - "scale": 1, - "frames": [ - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 0, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 60, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0015.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 120, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0016.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 4, - "w": 60, - "h": 67 - }, - "frame": { - "x": 180, - "y": 0, - "w": 60, - "h": 67 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 3, - "w": 60, - "h": 68 - }, - "frame": { - "x": 240, - "y": 0, - "w": 60, - "h": 68 - } - }, - { - "filename": "0008.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 3, - "w": 61, - "h": 68 - }, - "frame": { - "x": 0, - "y": 67, - "w": 61, - "h": 68 - } - }, - { - "filename": "0014.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 3, - "w": 60, - "h": 68 - }, - "frame": { - "x": 61, - "y": 67, - "w": 60, - "h": 68 - } - }, - { - "filename": "0018.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 3, - "w": 61, - "h": 68 - }, - "frame": { - "x": 121, - "y": 67, - "w": 61, - "h": 68 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 60, - "h": 69 - }, - "frame": { - "x": 182, - "y": 68, - "w": 60, - "h": 69 - } - }, - { - "filename": "0017.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 60, - "h": 69 - }, - "frame": { - "x": 0, - "y": 135, - "w": 60, - "h": 69 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 61, - "h": 70 - }, - "frame": { - "x": 60, - "y": 135, - "w": 61, - "h": 70 - } - }, - { - "filename": "0013.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 61, - "h": 70 - }, - "frame": { - "x": 121, - "y": 135, - "w": 61, - "h": 70 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 182, - "y": 137, - "w": 61, - "h": 71 - } - }, - { - "filename": "0011.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 182, - "y": 137, - "w": 61, - "h": 71 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 0, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0009.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 61, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0019.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 61, - "y": 205, - "w": 61, - "h": 71 - } - }, - { - "filename": "0010.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 122, - "y": 208, - "w": 61, - "h": 71 - } - }, - { - "filename": "0020.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 122, - "y": 208, - "w": 61, - "h": 71 - } - }, - { - "filename": "0012.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 61, - "h": 71 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 61, - "h": 71 - }, - "frame": { - "x": 183, - "y": 208, - "w": 61, - "h": 71 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:8d47c2cedd75d15c81c3aa0a0b14133c:28c19026319cfbbb59916e3d1b92f732:f9304907e03a5223c5bc78c934419106$" - } -} +{ + "textures": [ + { + "image": "745.png", + "format": "RGBA8888", + "size": { + "w": 181, + "h": 181 + }, + "scale": 1, + "frames": [ + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 0, + "w": 71, + "h": 61 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 61 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 0, + "w": 71, + "h": 61 + }, + "frame": { + "x": 0, + "y": 0, + "w": 71, + "h": 61 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 71, + "h": 60 + }, + "frame": { + "x": 71, + "y": 0, + "w": 71, + "h": 60 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 71, + "h": 60 + }, + "frame": { + "x": 71, + "y": 0, + "w": 71, + "h": 60 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 71, + "h": 59 + }, + "frame": { + "x": 71, + "y": 60, + "w": 71, + "h": 59 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 70, + "h": 61 + }, + "frame": { + "x": 0, + "y": 61, + "w": 70, + "h": 61 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 70, + "h": 61 + }, + "frame": { + "x": 0, + "y": 61, + "w": 70, + "h": 61 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 68, + "h": 59 + }, + "frame": { + "x": 0, + "y": 122, + "w": 68, + "h": 59 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 69, + "h": 61 + }, + "frame": { + "x": 70, + "y": 119, + "w": 69, + "h": 61 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 72, + "h": 61 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 69, + "h": 61 + }, + "frame": { + "x": 70, + "y": 119, + "w": 69, + "h": 61 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:9bdd7250af45db121574c90e718874a8:ca85d052f16849220d83acd876b20b8b:f9304907e03a5223c5bc78c934419106$" + } +} diff --git a/public/images/pokemon/exp/back/shiny/745.png b/public/images/pokemon/exp/back/shiny/745.png index 5eb15a8cf497..49f2d0569af8 100644 Binary files a/public/images/pokemon/exp/back/shiny/745.png and b/public/images/pokemon/exp/back/shiny/745.png differ diff --git a/public/images/pokemon/exp/shiny/745.json b/public/images/pokemon/exp/shiny/745.json index 6cabccff28d1..d0989a1ccd34 100644 --- a/public/images/pokemon/exp/shiny/745.json +++ b/public/images/pokemon/exp/shiny/745.json @@ -1,167 +1,524 @@ -{ - "textures": [ - { - "image": "745.png", - "format": "RGBA8888", - "size": { - "w": 189, - "h": 189 - }, - "scale": 1, - "frames": [ - { - "filename": "0006.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 1, - "y": 0, - "w": 65, - "h": 58 - }, - "frame": { - "x": 0, - "y": 0, - "w": 65, - "h": 58 - } - }, - { - "filename": "0005.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 0, - "y": 1, - "w": 66, - "h": 57 - }, - "frame": { - "x": 65, - "y": 0, - "w": 66, - "h": 57 - } - }, - { - "filename": "0007.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 2, - "y": 0, - "w": 64, - "h": 58 - }, - "frame": { - "x": 65, - "y": 57, - "w": 64, - "h": 58 - } - }, - { - "filename": "0003.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 2, - "y": 1, - "w": 64, - "h": 57 - }, - "frame": { - "x": 0, - "y": 58, - "w": 64, - "h": 57 - } - }, - { - "filename": "0004.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 1, - "y": 2, - "w": 65, - "h": 56 - }, - "frame": { - "x": 0, - "y": 115, - "w": 65, - "h": 56 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 62, - "h": 58 - }, - "frame": { - "x": 65, - "y": 115, - "w": 62, - "h": 58 - } - }, - { - "filename": "0002.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 66, - "h": 58 - }, - "spriteSourceSize": { - "x": 3, - "y": 0, - "w": 62, - "h": 58 - }, - "frame": { - "x": 127, - "y": 115, - "w": 62, - "h": 58 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:1b95a218abc87c12576165b943d3cb77:4d796dc75302ca2e18ce15e67dcf3f0f:f9304907e03a5223c5bc78c934419106$" - } -} +{ + "textures": [ + { + "image": "745.png", + "format": "RGBA8888", + "size": { + "w": 286, + "h": 286 + }, + "scale": 1, + "frames": [ + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 0, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 60, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 120, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 60, + "h": 55 + }, + "frame": { + "x": 180, + "y": 0, + "w": 60, + "h": 55 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 0, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 60, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 120, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 180, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 180, + "y": 55, + "w": 60, + "h": 57 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 0, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 60, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 1, + "w": 60, + "h": 57 + }, + "frame": { + "x": 120, + "y": 112, + "w": 60, + "h": 57 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 112, + "w": 60, + "h": 58 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 112, + "w": 60, + "h": 58 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 60, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 120, + "y": 169, + "w": 60, + "h": 58 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 170, + "w": 60, + "h": 58 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 0, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 60, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 120, + "y": 227, + "w": 60, + "h": 58 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 60, + "h": 58 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 60, + "h": 58 + }, + "frame": { + "x": 180, + "y": 228, + "w": 60, + "h": 58 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d67741bfb78b7ff0c920c5395dd91fc2:e78172ef76e3b6327173461a595a8a6b:f9304907e03a5223c5bc78c934419106$" + } +} diff --git a/public/images/pokemon/exp/shiny/745.png b/public/images/pokemon/exp/shiny/745.png index 7679c44ba138..c3256cf3f64d 100644 Binary files a/public/images/pokemon/exp/shiny/745.png and b/public/images/pokemon/exp/shiny/745.png differ diff --git a/public/images/trainer/buck.json b/public/images/trainer/buck.json new file mode 100644 index 000000000000..d2d215f716a9 --- /dev/null +++ b/public/images/trainer/buck.json @@ -0,0 +1,524 @@ +{ + "textures": [ + { + "image": "buck.png", + "format": "RGBA8888", + "size": { + "w": 120, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 33, + "y": 4, + "w": 35, + "h": 76 + }, + "frame": { + "x": 1, + "y": 1, + "w": 35, + "h": 76 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 8, + "w": 44, + "h": 72 + }, + "frame": { + "x": 38, + "y": 1, + "w": 44, + "h": 72 + } + }, + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 34, + "y": 5, + "w": 35, + "h": 75 + }, + "frame": { + "x": 84, + "y": 1, + "w": 35, + "h": 75 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:033f3d363b4192f64c92e02c19622c15:0d06141bef5af87ef82da967253207cb:3347efe478119141b0e3e6eccdecd0f5$" + } +} diff --git a/public/images/trainer/buck.png b/public/images/trainer/buck.png new file mode 100644 index 000000000000..2384fb42a336 Binary files /dev/null and b/public/images/trainer/buck.png differ diff --git a/public/images/trainer/bug_type_superfan.json b/public/images/trainer/bug_type_superfan.json new file mode 100644 index 000000000000..74dca3583d5d --- /dev/null +++ b/public/images/trainer/bug_type_superfan.json @@ -0,0 +1,1469 @@ +{ + "textures": [ + { + "image": "bug_type_superfan.png", + "format": "RGBA8888", + "size": { + "w": 224, + "h": 224 + }, + "scale": 1, + "frames": [ + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 5, + "y": 1, + "w": 52, + "h": 85 + }, + "frame": { + "x": 1, + "y": 1, + "w": 52, + "h": 85 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 5, + "y": 1, + "w": 52, + "h": 85 + }, + "frame": { + "x": 1, + "y": 1, + "w": 52, + "h": 85 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 9, + "y": 11, + "w": 60, + "h": 75 + }, + "frame": { + "x": 55, + "y": 1, + "w": 60, + "h": 75 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0034.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0035.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0036.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0037.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0038.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0039.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0040.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0041.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0042.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0043.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0044.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0045.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0046.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0047.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0048.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0049.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0050.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0051.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0052.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0053.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0054.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0055.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0056.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0057.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0058.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0059.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0060.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0061.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0062.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0063.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0064.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0065.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0066.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0067.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0068.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0069.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 6, + "y": 18, + "w": 65, + "h": 68 + }, + "frame": { + "x": 117, + "y": 1, + "w": 65, + "h": 68 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 10, + "y": 0, + "w": 46, + "h": 86 + }, + "frame": { + "x": 1, + "y": 88, + "w": 46, + "h": 86 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 19, + "w": 66, + "h": 67 + }, + "frame": { + "x": 49, + "y": 88, + "w": 66, + "h": 67 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 19, + "w": 66, + "h": 67 + }, + "frame": { + "x": 49, + "y": 88, + "w": 66, + "h": 67 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 49, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 49, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 8, + "y": 20, + "w": 65, + "h": 66 + }, + "frame": { + "x": 116, + "y": 157, + "w": 65, + "h": 66 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 7, + "y": 19, + "w": 65, + "h": 67 + }, + "frame": { + "x": 117, + "y": 71, + "w": 65, + "h": 67 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 86 + }, + "spriteSourceSize": { + "x": 7, + "y": 19, + "w": 65, + "h": 67 + }, + "frame": { + "x": 117, + "y": 71, + "w": 65, + "h": 67 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:442c13442d70348845d7f5fcdfc121b3:3b8402aa64ee8990e64c7f03ffffbc55:568199339797fd79d11ae8d741953c1c$" + } +} diff --git a/public/images/trainer/bug_type_superfan.png b/public/images/trainer/bug_type_superfan.png new file mode 100644 index 000000000000..59316fe6ed89 Binary files /dev/null and b/public/images/trainer/bug_type_superfan.png differ diff --git a/public/images/trainer/cheryl.json b/public/images/trainer/cheryl.json new file mode 100644 index 000000000000..4cac665a5886 --- /dev/null +++ b/public/images/trainer/cheryl.json @@ -0,0 +1,398 @@ +{ + "textures": [ + { + "image": "cheryl.png", + "format": "RGBA8888", + "size": { + "w": 154, + "h": 83 + }, + "scale": 1, + "frames": [ + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 25, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 25, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 24, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 24, + "y": 0, + "w": 41, + "h": 81 + }, + "frame": { + "x": 44, + "y": 1, + "w": 41, + "h": 81 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 27, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 26, + "y": 0, + "w": 33, + "h": 81 + }, + "frame": { + "x": 87, + "y": 1, + "w": 33, + "h": 81 + } + }, + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 20, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 81 + }, + "spriteSourceSize": { + "x": 21, + "y": 0, + "w": 31, + "h": 81 + }, + "frame": { + "x": 122, + "y": 1, + "w": 31, + "h": 81 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:dfcf7aedbd588c4e42427a2e17c171bf:206549943a0e3325d20a017ef01eefee:a233cd27590422717866c66e366b68fb$" + } +} diff --git a/public/images/trainer/cheryl.png b/public/images/trainer/cheryl.png new file mode 100644 index 000000000000..c46505f6b258 Binary files /dev/null and b/public/images/trainer/cheryl.png differ diff --git a/public/images/trainer/marley.json b/public/images/trainer/marley.json new file mode 100644 index 000000000000..92d9f1449e53 --- /dev/null +++ b/public/images/trainer/marley.json @@ -0,0 +1,83 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 0, "y": 0, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 26, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 32, "y": 0, "w": 28, "h": 78 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 1, "w": 28, "h": 78 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 32, "y": 0, "w": 28, "h": 78 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 1, "w": 28, "h": 78 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 78, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 0, "y": 78, "w": 31, "h": 77 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 2, "w": 31, "h": 77 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.pngprite.org/", + "version": "1.3.7-x64", + "image": "marley.png", + "format": "I8", + "size": { "w": 60, "h": 155 }, + "scale": "1" + } +} diff --git a/public/images/trainer/marley.png b/public/images/trainer/marley.png new file mode 100644 index 000000000000..8e78e11e8ad6 Binary files /dev/null and b/public/images/trainer/marley.png differ diff --git a/public/images/trainer/mira.json b/public/images/trainer/mira.json new file mode 100644 index 000000000000..7bd29f534759 --- /dev/null +++ b/public/images/trainer/mira.json @@ -0,0 +1,209 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0008.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0009.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0010.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0011.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0012.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0013.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0014.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0015.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 23, "y": 14, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0016.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 22, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0017.png", + "frame": { "x": 53, "y": 0, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0018.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 11, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0019.png", + "frame": { "x": 0, "y": 0, "w": 53, "h": 63 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 21, "y": 12, "w": 53, "h": 63 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0020.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 13, "y": 11, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 63, "w": 44, "h": 65 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 13, "w": 44, "h": 65 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "mira.png", + "format": "I8", + "size": { "w": 97, "h": 128 }, + "scale": "1" + } +} diff --git a/public/images/trainer/mira.png b/public/images/trainer/mira.png new file mode 100644 index 000000000000..5c1afe5d2410 Binary files /dev/null and b/public/images/trainer/mira.png differ diff --git a/public/images/trainer/riley.json b/public/images/trainer/riley.json new file mode 100644 index 000000000000..f0f84a909db7 --- /dev/null +++ b/public/images/trainer/riley.json @@ -0,0 +1,209 @@ +{ "frames": [ + { + "filename": "0000.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0001.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0002.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0003.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0004.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 31, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0005.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 31, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0006.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 30, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0007.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 30, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0008.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0009.png", + "frame": { "x": 55, "y": 80, "w": 37, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 28, "y": 0, "w": 37, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0010.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0011.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 10, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0012.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0013.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 11, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0014.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0015.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0016.png", + "frame": { "x": 0, "y": 80, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0017.png", + "frame": { "x": 0, "y": 80, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0018.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0019.png", + "frame": { "x": 55, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0020.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + }, + { + "filename": "0021.png", + "frame": { "x": 0, "y": 0, "w": 55, "h": 80 }, + "rotated": false, + "trimmed": true, + "spriteSourceSize": { "x": 12, "y": 0, "w": 55, "h": 80 }, + "sourceSize": { "w": 80, "h": 80 }, + "duration": 100 + } + ], + "meta": { + "app": "https://www.aseprite.org/", + "version": "1.3.7-x64", + "image": "riley.png", + "format": "I8", + "size": { "w": 110, "h": 160 }, + "scale": "1" + } +} diff --git a/public/images/trainer/riley.png b/public/images/trainer/riley.png new file mode 100644 index 000000000000..a9f0e3b53a9c Binary files /dev/null and b/public/images/trainer/riley.png differ diff --git a/public/images/trainer/vicky.json b/public/images/trainer/vicky.json new file mode 100644 index 000000000000..c19cf11622d3 --- /dev/null +++ b/public/images/trainer/vicky.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vicky.png", + "format": "RGBA8888", + "size": { + "w": 52, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 27, + "w": 52, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 52, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:bf9d2d417a1982282dd711456ac71206:101e07828e3d6e2a2a7a80aebfa802ad:cabe44a4410c334298b1984a219f8160$" + } +} diff --git a/public/images/trainer/vicky.png b/public/images/trainer/vicky.png new file mode 100644 index 000000000000..3e2d6c136969 Binary files /dev/null and b/public/images/trainer/vicky.png differ diff --git a/public/images/trainer/victor.json b/public/images/trainer/victor.json new file mode 100644 index 000000000000..5afa97045672 --- /dev/null +++ b/public/images/trainer/victor.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "victor.png", + "format": "RGBA8888", + "size": { + "w": 55, + "h": 53 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 12, + "y": 27, + "w": 55, + "h": 53 + }, + "frame": { + "x": 0, + "y": 0, + "w": 55, + "h": 53 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:64eff0f697754cdf9552b46342c9292a:611e0e2cacbd90c1229ce5443b2414f0:0cc0f5a2c1b2eedb46dd8318e8feb1d8$" + } +} diff --git a/public/images/trainer/victor.png b/public/images/trainer/victor.png new file mode 100644 index 000000000000..3ffddea24bbf Binary files /dev/null and b/public/images/trainer/victor.png differ diff --git a/public/images/trainer/victoria.json b/public/images/trainer/victoria.json new file mode 100644 index 000000000000..7917113621a4 --- /dev/null +++ b/public/images/trainer/victoria.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "victoria.png", + "format": "RGBA8888", + "size": { + "w": 52, + "h": 54 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 14, + "y": 26, + "w": 52, + "h": 54 + }, + "frame": { + "x": 0, + "y": 0, + "w": 52, + "h": 54 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:4dafeae3674d63b12cc4d8044f67b5a3:7834687d784c31169256927f419c7958:cf0eb39e0a3f2e42f23ca29747d73c40$" + } +} diff --git a/public/images/trainer/victoria.png b/public/images/trainer/victoria.png new file mode 100644 index 000000000000..e2874f266adf Binary files /dev/null and b/public/images/trainer/victoria.png differ diff --git a/public/images/trainer/vito.json b/public/images/trainer/vito.json new file mode 100644 index 000000000000..61dcf7af0ef2 --- /dev/null +++ b/public/images/trainer/vito.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vito.png", + "format": "RGBA8888", + "size": { + "w": 41, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 2, + "w": 41, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 41, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:cb988be58fcd5381174e9d120b051e38:4d4723dbbcd9713ee0ed3c2d84ef4bfb:1c7723b536b218346e3138016d865ce9$" + } +} diff --git a/public/images/trainer/vito.png b/public/images/trainer/vito.png new file mode 100644 index 000000000000..a7c6c0444f4a Binary files /dev/null and b/public/images/trainer/vito.png differ diff --git a/public/images/trainer/vivi.json b/public/images/trainer/vivi.json new file mode 100644 index 000000000000..b36ebcd7c0cf --- /dev/null +++ b/public/images/trainer/vivi.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "vivi.png", + "format": "RGBA8888", + "size": { + "w": 48, + "h": 69 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 13, + "y": 11, + "w": 48, + "h": 69 + }, + "frame": { + "x": 0, + "y": 0, + "w": 48, + "h": 69 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:0a51b4df0b2ed0fed7e3bdb5dffd9e28:af1f3b1480023b3e3761c49e49faf5f1:4fc6bf2bec74c4bb8809df38231deb01$" + } +} diff --git a/public/images/trainer/vivi.png b/public/images/trainer/vivi.png new file mode 100644 index 000000000000..cd97e676cfba Binary files /dev/null and b/public/images/trainer/vivi.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index f06ae607b0e7..516662617f16 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2,18 +2,28 @@ import Phaser from "phaser"; import UI from "./ui/ui"; import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; -import { Constructor } from "#app/utils"; +import { Constructor, isNullOrUndefined } from "#app/utils"; import * as Utils from "./utils"; -import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier"; +import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems, PokemonIncrementingStatModifier, ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims"; import { Phase } from "./phase"; import { initGameSpeed } from "./system/game-speed"; import { Arena, ArenaBase } from "./field/arena"; import { GameData } from "./system/game-data"; -import { TextStyle, addTextObject, getTextColor } from "./ui/text"; +import { addTextObject, getTextColor, TextStyle } from "./ui/text"; import { allMoves } from "./data/move"; -import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type"; +import { + ModifierPoolType, + getDefaultModifierTypeForTier, + getEnemyModifierTypesForWave, + getLuckString, + getLuckTextTint, + getModifierPoolForType, + getModifierType, + getPartyLuckValue, + modifierTypes, PokemonHeldItemModifierType +} from "./modifier/modifier-type"; import AbilityBar from "./ui/ability-bar"; import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, ChangeMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; import { allAbilities } from "./data/ability"; @@ -22,14 +32,14 @@ import { GameMode, GameModes, getGameMode } from "./game-mode"; import FieldSpritePipeline from "./pipelines/field-sprite"; import SpritePipeline from "./pipelines/sprite"; import PartyExpBar from "./ui/party-exp-bar"; -import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; +import { trainerConfigs, TrainerSlot } from "./data/trainer-config"; import Trainer, { TrainerVariant } from "./field/trainer"; import TrainerData from "./system/trainer-data"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { pokemonPrevolutions } from "./data/pokemon-evolutions"; import PokeballTray from "./ui/pokeball-tray"; import InvertPostFX from "./pipelines/invert"; -import { Achv, ModifierAchv, MoneyAchv, achvs } from "./system/achv"; +import { Achv, achvs, ModifierAchv, MoneyAchv } from "./system/achv"; import { Voucher, vouchers } from "./system/voucher"; import { Gender } from "./data/gender"; import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin"; @@ -86,6 +96,16 @@ import { TitlePhase } from "./phases/title-phase"; import { ToggleDoublePositionPhase } from "./phases/toggle-double-position-phase"; import { TurnInitPhase } from "./phases/turn-init-phase"; import { ShopCursorTarget } from "./enums/shop-cursor-target"; +import MysteryEncounter from "./data/mystery-encounters/mystery-encounter"; +import { allMysteryEncounters, ANTI_VARIANCE_WEIGHT_MODIFIER, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { ExpPhase } from "#app/phases/exp-phase"; +import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { ExpGainsSpeed } from "./enums/exp-gains-speed"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -161,7 +181,7 @@ export default class BattleScene extends SceneBase { public experimentalSprites: boolean = false; public musicPreference: integer = 0; public moveAnimations: boolean = true; - public expGainsSpeed: integer = 0; + public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public skipSeenDialogues: boolean = false; /** * Determines if the egg hatching animation should be skipped @@ -246,6 +266,10 @@ export default class BattleScene extends SceneBase { public money: integer; public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; + /** Session save data that pertains to Mystery Encounters */ + public mysteryEncounterSaveData: MysteryEncounterSaveData = new MysteryEncounterSaveData(); + /** If the previous wave was a MysteryEncounter, tracks the object with this variable. Mostly used for visual object cleanup */ + public lastMysteryEncounter?: MysteryEncounter; /** Combined Biome and Wave count text */ private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; @@ -884,6 +908,26 @@ export default class BattleScene extends SceneBase { return pokemon; } + /** + * Removes a {@linkcode PlayerPokemon} from the party, and clears modifiers for that Pokemon's id + * Useful for MEs/Challenges that remove Pokemon from the player party temporarily or permanently + * @param pokemon + * @param destroy Default true. If true, will destroy the {@linkcode PlayerPokemon} after removing + */ + removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) { + if (!pokemon) { + return; + } + + const partyIndex = this.party.indexOf(pokemon); + this.party.splice(partyIndex, 1); + if (destroy) { + this.field.remove(pokemon, true); + pokemon.destroy(); + } + this.updateModifiers(true); + } + addPokemonIcon(pokemon: Pokemon, x: number, y: number, originX: number = 0.5, originY: number = 0.5, ignoreOverride: boolean = false): Phaser.GameObjects.Container { const container = this.add.container(x, y); container.setName(`${pokemon.name}-icon`); @@ -1086,7 +1130,7 @@ export default class BattleScene extends SceneBase { } } - newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle | null { + newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounterType?: MysteryEncounterType): Battle | null { const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave; const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1); let newDouble: boolean | undefined; @@ -1135,6 +1179,36 @@ export default class BattleScene extends SceneBase { newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, variant); this.field.add(newTrainer); } + + // Check for mystery encounter + // Can only occur in place of a standard (non-boss) wild battle, waves 10-180 + const [lowestMysteryEncounterWave, highestMysteryEncounterWave] = this.gameMode.getMysteryEncounterLegalWaves(); + if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < highestMysteryEncounterWave && newWaveIndex > lowestMysteryEncounterWave) { + const roll = Utils.randSeedInt(MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT); + + // Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor + const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance; + const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents; + + // If total number of encounters is lower than expected for the run, slightly favor a new encounter spawn (reverse as well) + // Reduces occurrence of runs with total encounters significantly different from AVERAGE_ENCOUNTERS_PER_RUN_TARGET + const expectedEncountersByFloor = AVERAGE_ENCOUNTERS_PER_RUN_TARGET / (highestMysteryEncounterWave - lowestMysteryEncounterWave) * (newWaveIndex - lowestMysteryEncounterWave); + const currentRunDiffFromAvg = expectedEncountersByFloor - encounteredEvents.length; + const favoredEncounterRate = sessionEncounterRate + currentRunDiffFromAvg * ANTI_VARIANCE_WEIGHT_MODIFIER; + + const successRate = isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) ? favoredEncounterRate : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE!; + + // If the most recent ME was 3 or fewer waves ago, can never spawn a ME + const canSpawn = encounteredEvents.length === 0 || (newWaveIndex - encounteredEvents[encounteredEvents.length - 1].waveIndex) > 3 || !isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE); + + if (canSpawn && roll < successRate) { + newBattleType = BattleType.MYSTERY_ENCOUNTER; + // Reset base spawn weight + this.mysteryEncounterSaveData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + } else { + this.mysteryEncounterSaveData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS; + } + } } if (double === undefined && newWaveIndex > 1) { @@ -1167,12 +1241,21 @@ export default class BattleScene extends SceneBase { const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; + this.lastMysteryEncounter = lastBattle?.mysteryEncounter; this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); }, newWaveIndex << 3, this.waveSeed); this.currentBattle.incrementTurn(this); + if (newBattleType === BattleType.MYSTERY_ENCOUNTER) { + // Disable double battle on mystery encounters (it may be re-enabled as part of encounter) + this.currentBattle.double = false; + this.executeWithSeedOffset(() => { + this.currentBattle.mysteryEncounter = this.getMysteryEncounter(mysteryEncounterType); + }, this.currentBattle.waveIndex << 4); + } + //this.pushPhase(new TrainerMessageTestPhase(this, TrainerType.RIVAL, TrainerType.RIVAL_2, TrainerType.RIVAL_3, TrainerType.RIVAL_4, TrainerType.RIVAL_5, TrainerType.RIVAL_6)); if (!waveIndex && lastBattle) { @@ -1181,7 +1264,7 @@ export default class BattleScene extends SceneBase { const isEndlessFifthWave = this.gameMode.hasShortBiomes && (lastBattle.waveIndex % 5) === 0; const isWaveIndexMultipleOfFiftyMinusOne = (lastBattle.waveIndex % 50) === 49; const isNewBiome = isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne); - const resetArenaState = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; + const resetArenaState = isNewBiome || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS; this.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy()); this.trySpreadPokerus(); if (!isNewBiome && (newWaveIndex % 10) === 5) { @@ -1189,14 +1272,21 @@ export default class BattleScene extends SceneBase { } if (resetArenaState) { this.arena.resetArenaEffects(); - playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p))); + + playerField.forEach((pokemon, p) => { + if (pokemon.isOnField()) { + this.pushPhase(new ReturnPhase(this, p)); + } + }); for (const pokemon of this.getParty()) { pokemon.resetBattleData(); applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); } - this.pushPhase(new ShowTrainerPhase(this)); + if (!this.trainer.visible) { + this.pushPhase(new ShowTrainerPhase(this)); + } } for (const pokemon of this.getParty()) { @@ -1290,7 +1380,6 @@ export default class BattleScene extends SceneBase { case Species.ZARUDE: case Species.SQUAWKABILLY: case Species.TATSUGIRI: - case Species.GIMMIGHOUL: case Species.PALDEA_TAUROS: return Utils.randSeedInt(species.forms.length); case Species.PIKACHU: @@ -1316,6 +1405,13 @@ export default class BattleScene extends SceneBase { return 1; } return 0; + case Species.GIMMIGHOUL: + // Chest form can only be found in Mysterious Chest Encounter, if this is a game mode with MEs + if (this.gameMode.hasMysteryEncounters) { + return 1; // Wandering form + } else { + return Utils.randSeedInt(species.forms.length); + } } if (ignoreArena) { @@ -2485,7 +2581,7 @@ export default class BattleScene extends SceneBase { }); } - generateEnemyModifiers(): Promise { + generateEnemyModifiers(heldModifiersConfigs?: HeldModifierConfig[][]): Promise { return new Promise(resolve => { if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return resolve(); @@ -2507,29 +2603,47 @@ export default class BattleScene extends SceneBase { } party.forEach((enemyPokemon: EnemyPokemon, i: integer) => { - const isBoss = enemyPokemon.isBoss() || (this.currentBattle.battleType === BattleType.TRAINER && !!this.currentBattle.trainer?.config.isBoss); - let upgradeChance = 32; - if (isBoss) { - upgradeChance /= 2; - } - if (isFinalBoss) { - upgradeChance /= 8; - } - const modifierChance = this.gameMode.getEnemyModifierChance(isBoss); - let pokemonModifierChance = modifierChance; - if (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer) - pokemonModifierChance = Math.ceil(pokemonModifierChance * this.currentBattle.trainer.getPartyMemberModifierChanceMultiplier(i)); // eslint-disable-line - let count = 0; - for (let c = 0; c < chances; c++) { - if (!Utils.randSeedInt(modifierChance)) { - count++; + if (heldModifiersConfigs && i < heldModifiersConfigs.length && heldModifiersConfigs[i] && heldModifiersConfigs[i].length > 0) { + heldModifiersConfigs[i].forEach(mt => { + let modifier: PokemonHeldItemModifier; + if (mt.modifier instanceof PokemonHeldItemModifierType) { + modifier = mt.modifier.newModifier(enemyPokemon); + } else { + modifier = mt.modifier as PokemonHeldItemModifier; + modifier.pokemonId = enemyPokemon.id; + } + const stackCount = mt.stackCount ?? 1; + modifier.stackCount = stackCount; + // TODO: set isTransferable + // modifier.isTransferrable = mt.isTransferable ?? true; + this.addEnemyModifier(modifier, true); + }); + } else { + const isBoss = enemyPokemon.isBoss() || (this.currentBattle.battleType === BattleType.TRAINER && !!this.currentBattle.trainer?.config.isBoss); + let upgradeChance = 32; + if (isBoss) { + upgradeChance /= 2; } + if (isFinalBoss) { + upgradeChance /= 8; + } + const modifierChance = this.gameMode.getEnemyModifierChance(isBoss); + let pokemonModifierChance = modifierChance; + if (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer) + pokemonModifierChance = Math.ceil(pokemonModifierChance * this.currentBattle.trainer.getPartyMemberModifierChanceMultiplier(i)); // eslint-disable-line + let count = 0; + for (let c = 0; c < chances; c++) { + if (!Utils.randSeedInt(modifierChance)) { + count++; + } + } + if (isBoss) { + count = Math.max(count, Math.floor(chances / 2)); + } + getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance) + .map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this)); } - if (isBoss) { - count = Math.max(count, Math.floor(chances / 2)); - } - getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance) - .map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this)); + return true; }); this.updateModifiers(false).then(() => resolve()); }); @@ -2837,4 +2951,220 @@ export default class BattleScene extends SceneBase { this.shiftPhase(); } + + /** + * Updates Exp and level values for Player's party, adding new level up phases as required + * @param expValue raw value of exp to split among participants, OR the base multiplier to use with waveIndex + * @param pokemonDefeated If true, will increment Macho Brace stacks and give the party Pokemon friendship increases + * @param useWaveIndexMultiplier Default false. If true, will multiply expValue by a scaling waveIndex multiplier. Not needed if expValue is already scaled by level/wave + * @param pokemonParticipantIds Participants. If none are defined, no exp will be given. To spread evenly among the party, should pass all ids of party members. + */ + applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set): void { + const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds; + const party = this.getParty(); + const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; + const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; + const multipleParticipantExpBonusModifier = this.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; + const nonFaintedPartyMembers = party.filter(p => p.hp); + const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel()); + const partyMemberExp: number[] = []; + // EXP value calculation is based off Pokemon.getExpValue + if (useWaveIndexMultiplier) { + expValue = Math.floor(expValue * this.currentBattle.waveIndex / 5 + 1); + } + + if (participantIds.size > 0) { + if (this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + expValue = Math.floor(expValue * 1.5); + } else if (this.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.currentBattle.mysteryEncounter) { + expValue = Math.floor(expValue * this.currentBattle.mysteryEncounter.expMultiplier); + } + for (const partyMember of nonFaintedPartyMembers) { + const pId = partyMember.id; + const participated = participantIds.has(pId); + if (participated && pokemonDefeated) { + partyMember.addFriendship(2); + const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); + if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) { + machoBraceModifier.stackCount++; + this.updateModifiers(true, true); + partyMember.updateInfo(); + } + } + if (!expPartyMembers.includes(partyMember)) { + continue; + } + if (!participated && !expShareModifier) { + partyMemberExp.push(0); + continue; + } + let expMultiplier = 0; + if (participated) { + expMultiplier += (1 / participantIds.size); + if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { + expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + } + } else if (expShareModifier) { + expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; + } + if (partyMember.pokerus) { + expMultiplier *= 1.5; + } + if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { + expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; + } + const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); + this.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + partyMemberExp.push(Math.floor(pokemonExp.value)); + } + + if (expBalanceModifier) { + let totalLevel = 0; + let totalExp = 0; + expPartyMembers.forEach((expPartyMember, epm) => { + totalExp += partyMemberExp[epm]; + totalLevel += expPartyMember.level; + }); + + const medianLevel = Math.floor(totalLevel / expPartyMembers.length); + + const recipientExpPartyMemberIndexes: number[] = []; + expPartyMembers.forEach((expPartyMember, epm) => { + if (expPartyMember.level <= medianLevel) { + recipientExpPartyMemberIndexes.push(epm); + } + }); + + const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); + + expPartyMembers.forEach((_partyMember, pm) => { + partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); + }); + } + + for (let pm = 0; pm < expPartyMembers.length; pm++) { + const exp = partyMemberExp[pm]; + + if (exp) { + const partyMemberIndex = party.indexOf(expPartyMembers[pm]); + this.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this, partyMemberIndex, exp)); + } + } + } + } + + /** + * Loads or generates a mystery encounter + * @param encounterType used to load session encounter when restarting game, etc. + * @returns + */ + getMysteryEncounter(encounterType?: MysteryEncounterType): MysteryEncounter { + // Loading override or session encounter + let encounter: MysteryEncounter | null; + if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE!)) { + encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE!]; + } else { + encounter = !isNullOrUndefined(encounterType) ? allMysteryEncounters[encounterType!] : null; + } + + // Check for queued encounters first + if (!encounter && this.mysteryEncounterSaveData?.queuedEncounters && this.mysteryEncounterSaveData.queuedEncounters.length > 0) { + let i = 0; + while (i < this.mysteryEncounterSaveData.queuedEncounters.length && !!encounter) { + const candidate = this.mysteryEncounterSaveData.queuedEncounters[i]; + const forcedChance = candidate.spawnPercent; + if (Utils.randSeedInt(100) < forcedChance) { + encounter = allMysteryEncounters[candidate.type]; + } + + i++; + } + } + + if (encounter) { + encounter = new MysteryEncounter(encounter); + encounter.populateDialogueTokensFromRequirements(this); + return encounter; + } + + // See Enum values for base tier weights + const tierWeights = [MysteryEncounterTier.COMMON, MysteryEncounterTier.GREAT, MysteryEncounterTier.ULTRA, MysteryEncounterTier.ROGUE]; + + // Adjust tier weights by previously encountered events to lower odds of only Common/Great in run + this.mysteryEncounterSaveData.encounteredEvents.forEach(seenEncounterData => { + if (seenEncounterData.tier === MysteryEncounterTier.COMMON) { + tierWeights[0] = tierWeights[0] - 6; + } else if (seenEncounterData.tier === MysteryEncounterTier.GREAT) { + tierWeights[1] = tierWeights[1] - 4; + } + }); + + const totalWeight = tierWeights.reduce((a, b) => a + b); + const tierValue = Utils.randSeedInt(totalWeight); + const commonThreshold = totalWeight - tierWeights[0]; + const greatThreshold = totalWeight - tierWeights[0] - tierWeights[1]; + const ultraThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; + let tier: MysteryEncounterTier | null = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > greatThreshold ? MysteryEncounterTier.GREAT : tierValue > ultraThreshold ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; + + if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { + tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE!; + } + + let availableEncounters: MysteryEncounter[] = []; + // New encounter should never be the same as the most recent encounter + const previousEncounter = this.mysteryEncounterSaveData.encounteredEvents.length > 0 ? this.mysteryEncounterSaveData.encounteredEvents[this.mysteryEncounterSaveData.encounteredEvents.length - 1].type : null; + const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? []; + // If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available + while (availableEncounters.length === 0 && tier !== null) { + availableEncounters = biomeMysteryEncounters + .filter((encounterType) => { + const encounterCandidate = allMysteryEncounters[encounterType]; + if (!encounterCandidate) { + return false; + } + if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier + return false; + } + const disabledModes = encounterCandidate.disabledGameModes; + if (disabledModes && disabledModes.length > 0 + && disabledModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode + return false; + } + if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements + return false; + } + if (previousEncounter !== null && encounterType === previousEncounter) { // Previous encounter was not this one + return false; + } + if (this.mysteryEncounterSaveData.encounteredEvents.length > 0 && // Encounter has not exceeded max allowed encounters + (encounterCandidate.maxAllowedEncounters && encounterCandidate.maxAllowedEncounters > 0) + && this.mysteryEncounterSaveData.encounteredEvents.filter(e => e.type === encounterType).length >= encounterCandidate.maxAllowedEncounters) { + return false; + } + return true; + }) + .map((m) => (allMysteryEncounters[m])); + // Decrement tier + if (tier === MysteryEncounterTier.ROGUE) { + tier = MysteryEncounterTier.ULTRA; + } else if (tier === MysteryEncounterTier.ULTRA) { + tier = MysteryEncounterTier.GREAT; + } else if (tier === MysteryEncounterTier.GREAT) { + tier = MysteryEncounterTier.COMMON; + } else { + tier = null; // Ends loop + } + } + + // If absolutely no encounters are available, spawn 0th encounter + if (availableEncounters.length === 0) { + console.log("No Mystery Encounters found, falling back to Mysterious Challengers."); + return allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS]; + } + encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)]; + // New encounter object to not dirty flags + encounter = new MysteryEncounter(encounter); + encounter.populateDialogueTokensFromRequirements(this); + return encounter; + } } diff --git a/src/battle.ts b/src/battle.ts index a3e7b0a43360..a886a0eb771b 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,28 +14,31 @@ 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 { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export enum BattleType { - WILD, - TRAINER, - CLEAR + WILD, + TRAINER, + CLEAR, + MYSTERY_ENCOUNTER } export enum BattlerIndex { - ATTACKER = -1, - PLAYER, - PLAYER_2, - ENEMY, - ENEMY_2 + ATTACKER = -1, + PLAYER, + PLAYER_2, + ENEMY, + ENEMY_2 } export interface TurnCommand { - command: Command; - cursor?: number; - move?: QueuedMove; - targets?: BattlerIndex[]; - skip?: boolean; - args?: any[]; + command: Command; + cursor?: number; + move?: QueuedMove; + targets?: BattlerIndex[]; + skip?: boolean; + args?: any[]; } export interface FaintLogEntry { @@ -44,7 +47,7 @@ export interface FaintLogEntry { } interface TurnCommands { - [key: number]: TurnCommand | null + [key: number]: TurnCommand | null } export default class Battle { @@ -77,6 +80,9 @@ export default class Battle { public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; + /** If the current battle is a Mystery Encounter, this will always be defined */ + public mysteryEncounter?: MysteryEncounter; + private rngCounter: number = 0; constructor(gameMode: GameMode, waveIndex: number, battleType: BattleType, trainer?: Trainer, double?: boolean) { @@ -99,7 +105,7 @@ export default class Battle { this.battleSpec = spec; } - private getLevelForWave(): number { + public getLevelForWave(): number { const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex); const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2); const bossMultiplier = 1.2; @@ -197,7 +203,11 @@ export default class Battle { getBgmOverride(scene: BattleScene): string | null { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); - if (this.battleType === BattleType.TRAINER) { + if (this.battleType === BattleType.MYSTERY_ENCOUNTER && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) { + // Music is overridden for MEs during ME onInit() + // Should not use any BGM overrides before swapping from DEFAULT mode + return null; + } else if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) { return `encounter_${this.trainer?.getEncounterBgm()}`; } diff --git a/src/data/ability.ts b/src/data/ability.ts index b38d9ea0fb35..944ee10244a5 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1086,7 +1086,7 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (attacker.getTag(BattlerTagType.DISABLED) === null) { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !attacker.isMax()) { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) { if (simulated) { return true; } diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 102d435fc60f..f07d6cb24091 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -7,6 +7,9 @@ import { BattlerIndex } from "../battle"; import { Element } from "json-stable-stringify"; import { Moves } from "#enums/moves"; import { SubstituteTag } from "./battler-tags"; +import { isNullOrUndefined } from "../utils"; +import Phaser from "phaser"; +import { EncounterAnim } from "#enums/encounter-anims"; //import fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { @@ -304,7 +307,7 @@ abstract class AnimTimedEvent { this.resourceName = resourceName; } - abstract execute(scene: BattleScene, battleAnim: BattleAnim): integer; + abstract execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer; abstract getEventType(): string; } @@ -322,7 +325,7 @@ class AnimTimedSoundEvent extends AnimTimedEvent { } } - execute(scene: BattleScene, battleAnim: BattleAnim): integer { + execute(scene: BattleScene, battleAnim: BattleAnim, priority?: number): integer { const soundConfig = { rate: (this.pitch * 0.01), volume: (this.volume * 0.01) }; if (this.resourceName) { try { @@ -384,7 +387,7 @@ class AnimTimedUpdateBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): integer { + execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer { const tweenProps = {}; if (this.bgX !== undefined) { tweenProps["x"] = (this.bgX * 0.5) - 320; @@ -414,7 +417,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { super(frameIndex, resourceName, source); } - execute(scene: BattleScene, moveAnim: MoveAnim): integer { + execute(scene: BattleScene, moveAnim: MoveAnim, priority?: number): integer { if (moveAnim.bgSprite) { moveAnim.bgSprite.destroy(); } @@ -426,7 +429,9 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { moveAnim.bgSprite.setAlpha(this.opacity / 255); scene.field.add(moveAnim.bgSprite); const fieldPokemon = scene.getEnemyPokemon() || scene.getPlayerPokemon(); - if (fieldPokemon?.isOnField()) { + if (!isNullOrUndefined(priority)) { + scene.field.moveTo(moveAnim.bgSprite as Phaser.GameObjects.GameObject, priority!); + } else if (fieldPokemon?.isOnField()) { scene.field.moveBelow(moveAnim.bgSprite as Phaser.GameObjects.GameObject, fieldPokemon); } @@ -446,6 +451,7 @@ class AnimTimedAddBgEvent extends AnimTimedBgEvent { export const moveAnims = new Map(); export const chargeAnims = new Map(); export const commonAnims = new Map(); +export const encounterAnims = new Map(); export function initCommonAnims(scene: BattleScene): Promise { return new Promise(resolve => { @@ -516,6 +522,26 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { }); } +/** + * Fetches animation configs to be used in a Mystery Encounter + * @param scene + * @param encounterAnim one or more animations to fetch + */ +export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise { + const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; + const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); + const encounterAnimFetches: Promise>[] = []; + for (const anim of anims) { + if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { + continue; + } + encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) + .then(response => response.json()) + .then(cas => encounterAnims.set(anim, new AnimConfig(cas)))); + } + await Promise.allSettled(encounterAnimFetches); +} + export function initMoveChargeAnim(scene: BattleScene, chargeAnim: ChargeAnim): Promise { return new Promise(resolve => { if (chargeAnims.has(chargeAnim)) { @@ -570,6 +596,16 @@ export function loadCommonAnimAssets(scene: BattleScene, startLoad?: boolean): P }); } +/** + * Loads encounter animation assets to scene + * MUST be called after {@linkcode initEncounterAnims()} to load all required animations properly + * @param scene + * @param startLoad + */ +export async function loadEncounterAnimAssets(scene: BattleScene, startLoad?: boolean): Promise { + await loadAnimAssets(scene, Array.from(encounterAnims.values()), startLoad); +} + export function loadMoveAnimAssets(scene: BattleScene, moveIds: Moves[], startLoad?: boolean): Promise { return new Promise(resolve => { const moveAnimations = moveIds.map(m => moveAnims.get(m) as AnimConfig).flat(); @@ -679,14 +715,16 @@ export abstract class BattleAnim { public target: Pokemon | null; public sprites: Phaser.GameObjects.Sprite[]; public bgSprite: Phaser.GameObjects.TileSprite | Phaser.GameObjects.Rectangle; + public playOnEmptyField: boolean; private srcLine: number[]; private dstLine: number[]; - constructor(user?: Pokemon, target?: Pokemon) { + constructor(user?: Pokemon, target?: Pokemon, playOnEmptyField: boolean = false) { this.user = user ?? null; this.target = target ?? null; this.sprites = []; + this.playOnEmptyField = playOnEmptyField; } abstract getAnim(): AnimConfig | null; @@ -761,9 +799,9 @@ export abstract class BattleAnim { play(scene: BattleScene, onSubstitute?: boolean, callback?: Function) { const isOppAnim = this.isOppAnim(); const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct? - const target = !isOppAnim ? this.target : this.user; + const target = !isOppAnim ? this.target! : this.user!; - if (!target?.isOnField()) { + if (!target?.isOnField() && !this.playOnEmptyField) { if (callback) { callback(); } @@ -866,11 +904,13 @@ export abstract class BattleAnim { const isUser = frame.target === AnimFrameTarget.USER; if (isUser && target === user) { continue; + } else if (this.playOnEmptyField && frame.target === AnimFrameTarget.TARGET && !target.isOnField()) { + continue; } const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET]; const spriteSource = isUser ? userSprite : targetSprite; if ((isUser ? u : t) === sprites.length) { - if (!isUser && !!targetSubstitute) { + if (isUser || !targetSubstitute) { const sprite = scene.addPokemonSprite(isUser ? user! : target, 0, 0, spriteSource!.texture, spriteSource!.frame.name, true); // TODO: are those bangs correct? [ "spriteColors", "fusionSpriteColors" ].map(k => sprite.pipelineData[k] = (isUser ? user! : target).getSprite().pipelineData[k]); // TODO: are those bangs correct? sprite.setPipelineData("spriteKey", (isUser ? user! : target).getBattleSpriteKey()); @@ -1017,13 +1057,175 @@ export abstract class BattleAnim { } }); } + + private getGraphicFrameDataWithoutTarget(frames: AnimFrame[], targetInitialX: number, targetInitialY: number): Map> { + const ret: Map> = new Map([ + [AnimFrameTarget.GRAPHIC, new Map() ], + [AnimFrameTarget.USER, new Map() ], + [AnimFrameTarget.TARGET, new Map() ] + ]); + + let g = 0; + let u = 0; + let t = 0; + + for (const frame of frames) { + let { x, y } = frame; + const scaleX = (frame.zoomX / 100) * (!frame.mirror ? 1 : -1); + const scaleY = (frame.zoomY / 100); + x += targetInitialX; + y += targetInitialY; + const angle = -frame.angle; + const key = frame.target === AnimFrameTarget.GRAPHIC ? g++ : frame.target === AnimFrameTarget.USER ? u++ : t++; + ret.get(frame.target)?.set(key, { x: x, y: y, scaleX: scaleX, scaleY: scaleY, angle: angle }); + } + + return ret; + } + + /** + * + * @param scene + * @param targetInitialX + * @param targetInitialY + * @param frameTimeMult + * @param frameTimedEventPriority + * - 0 is behind all other sprites (except BG) + * - 1 on top of player field + * - 3 is on top of both fields + * - 5 is on top of player sprite + * @param callback + */ + playWithoutTargets(scene: BattleScene, targetInitialX: number, targetInitialY: number, frameTimeMult: number, frameTimedEventPriority?: 0 | 1 | 3 | 5, callback?: Function) { + const spriteCache: SpriteCache = { + [AnimFrameTarget.GRAPHIC]: [], + [AnimFrameTarget.USER]: [], + [AnimFrameTarget.TARGET]: [] + }; + + const cleanUpAndComplete = () => { + for (const ms of Object.values(spriteCache).flat()) { + if (ms) { + ms.destroy(); + } + } + if (this.bgSprite) { + this.bgSprite.destroy(); + } + if (callback) { + callback(); + } + }; + + if (!scene.moveAnimations) { + return cleanUpAndComplete(); + } + + const anim = this.getAnim(); + + this.srcLine = [ userFocusX, userFocusY, targetFocusX, targetFocusY ]; + this.dstLine = [ 150, 75, targetInitialX, targetInitialY ]; + + let totalFrames = anim!.frames.length; + let frameCount = 0; + + let existingFieldSprites = scene.field.getAll().slice(0); + + scene.tweens.addCounter({ + duration: Utils.getFrameMs(3) * frameTimeMult, + repeat: anim!.frames.length, + onRepeat: () => { + existingFieldSprites = scene.field.getAll().slice(0); + const spriteFrames = anim!.frames[frameCount]; + const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[frameCount], targetInitialX, targetInitialY); + let graphicFrameCount = 0; + for (const frame of spriteFrames) { + if (frame.target !== AnimFrameTarget.GRAPHIC) { + console.log("Encounter animations do not support targets"); + continue; + } + + const sprites = spriteCache[AnimFrameTarget.GRAPHIC]; + if (graphicFrameCount === sprites.length) { + const newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim!.graphic, 1); + sprites.push(newSprite); + scene.field.add(newSprite); + } + + const graphicIndex = graphicFrameCount++; + const moveSprite = sprites[graphicIndex]; + if (!isNullOrUndefined(frame.priority)) { + const setSpritePriority = (priority: integer) => { + if (existingFieldSprites.length > priority) { + // Move to specified priority index + const index = scene.field.getIndex(existingFieldSprites[priority]); + scene.field.moveTo(moveSprite, index); + } else { + // Move to top of scene + scene.field.moveTo(moveSprite, scene.field.getAll().length - 1); + } + }; + setSpritePriority(frame.priority); + } + moveSprite.setFrame(frame.graphicFrame); + + const graphicFrameData = frameData.get(frame.target)?.get(graphicIndex); + if (graphicFrameData) { + moveSprite.setPosition(graphicFrameData.x, graphicFrameData.y); + moveSprite.setAngle(graphicFrameData.angle); + moveSprite.setScale(graphicFrameData.scaleX, graphicFrameData.scaleY); + + moveSprite.setAlpha(frame.opacity / 255); + moveSprite.setVisible(frame.visible); + moveSprite.setBlendMode(frame.blendType === AnimBlendType.NORMAL ? Phaser.BlendModes.NORMAL : frame.blendType === AnimBlendType.ADD ? Phaser.BlendModes.ADD : Phaser.BlendModes.DIFFERENCE); + } + } + if (anim?.frameTimedEvents.get(frameCount)) { + for (const event of anim.frameTimedEvents.get(frameCount)!) { + totalFrames = Math.max((anim.frames.length - frameCount) + event.execute(scene, this, frameTimedEventPriority), totalFrames); + } + } + const targets = Utils.getEnumValues(AnimFrameTarget); + for (const i of targets) { + const count = graphicFrameCount; + if (count < spriteCache[i].length) { + const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length); + for (const sprite of spritesToRemove) { + if (!sprite.getData("locked") as boolean) { + const spriteCacheIndex = spriteCache[i].indexOf(sprite); + spriteCache[i].splice(spriteCacheIndex, 1); + sprite.destroy(); + } + } + } + } + frameCount++; + totalFrames--; + }, + onComplete: () => { + for (const sprite of Object.values(spriteCache).flat()) { + if (sprite && !sprite.getData("locked")) { + sprite.destroy(); + } + } + if (totalFrames) { + scene.tweens.addCounter({ + duration: Utils.getFrameMs(totalFrames), + onComplete: () => cleanUpAndComplete() + }); + } else { + cleanUpAndComplete(); + } + } + }); + } } export class CommonBattleAnim extends BattleAnim { public commonAnim: CommonAnim | null; - constructor(commonAnim: CommonAnim | null, user: Pokemon, target?: Pokemon) { - super(user, target || user); + constructor(commonAnim: CommonAnim | null, user: Pokemon, target?: Pokemon, playOnEmptyField: boolean = false) { + super(user, target || user, playOnEmptyField); this.commonAnim = commonAnim; } @@ -1040,8 +1242,8 @@ export class CommonBattleAnim extends BattleAnim { export class MoveAnim extends BattleAnim { public move: Moves; - constructor(move: Moves, user: Pokemon, target: BattlerIndex) { - super(user, user.scene.getField()[target]); + constructor(move: Moves, user: Pokemon, target: BattlerIndex, playOnEmptyField: boolean = false) { + super(user, user.scene.getField()[target], playOnEmptyField); this.move = move; } @@ -1085,6 +1287,26 @@ export class MoveChargeAnim extends MoveAnim { } } +export class EncounterBattleAnim extends BattleAnim { + public encounterAnim: EncounterAnim; + public oppAnim: boolean; + + constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon, oppAnim?: boolean) { + super(user, target ?? user, true); + + this.encounterAnim = encounterAnim; + this.oppAnim = oppAnim ?? false; + } + + getAnim(): AnimConfig | null { + return encounterAnims.get(this.encounterAnim) ?? null; + } + + isOppAnim(): boolean { + return this.oppAnim; + } +} + export async function populateAnims() { const commonAnimNames = Utils.getEnumKeys(CommonAnim).map(k => k.toLowerCase()); const commonAnimMatchNames = commonAnimNames.map(k => k.replace(/\_/g, "")); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index a43fa58ba1d8..32b3d54f3028 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -5,7 +5,7 @@ import { StatusEffect } from "./status-effect"; import * as Utils from "../utils"; import { ChargeAttr, MoveFlags, allMoves } from "./move"; import { Type } from "./type"; -import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability"; +import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability"; import { TerrainType } from "./terrain"; import { WeatherType } from "./weather"; import { allAbilities } from "./ability"; @@ -524,10 +524,6 @@ export class FlinchedTag extends BattlerTag { applyAbAttrs(FlinchEffectAbAttr, pokemon, null); } - canAdd(pokemon: Pokemon): boolean { - return !pokemon.isMax(); - } - /** * Cancels the Pokemon's next Move on the turn this tag is applied * @param pokemon The {@linkcode Pokemon} with this tag @@ -878,10 +874,6 @@ export class EncoreTag extends BattlerTag { } canAdd(pokemon: Pokemon): boolean { - if (pokemon.isMax()) { - return false; - } - const lastMoves = pokemon.getLastXMoves(1); if (!lastMoves.length) { return false; @@ -1060,19 +1052,11 @@ export class MinimizeTag extends BattlerTag { super(BattlerTagType.MINIMIZED, BattlerTagLapseType.TURN_END, 1, Moves.MINIMIZE); } - canAdd(pokemon: Pokemon): boolean { - return !pokemon.isMax(); - } - onAdd(pokemon: Pokemon): void { super.onAdd(pokemon); } lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { - //If a pokemon dynamaxes they lose minimized status - if (pokemon.isMax()) { - return false; - } return lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); } @@ -2238,8 +2222,8 @@ export class SubstituteTag extends BattlerTag { pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.SUBSTITUTE_ADD); pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), 1500); - // Remove any trapping effects from the user - pokemon.findAndRemoveTags(tag => tag instanceof TrappedTag); + // Remove any binding effects from the user + pokemon.findAndRemoveTags(tag => tag instanceof DamagingTrapTag); } /** Queues an on-remove battle animation that removes the Substitute's sprite. */ @@ -2304,6 +2288,45 @@ export class SubstituteTag extends BattlerTag { } } +/** + * Tag that adds extra post-summon effects to a battle for a specific Pokemon. + * These post-summon effects are performed through {@linkcode Pokemon.mysteryEncounterBattleEffects}, + * and can be used to unshift special phases, etc. + * Currently used only in MysteryEncounters to provide start of fight stat buffs. + */ +export class MysteryEncounterPostSummonTag extends BattlerTag { + constructor() { + super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1); + } + + /** Event when tag is added */ + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + + /** Performs post-summon effects through {@linkcode Pokemon.mysteryEncounterBattleEffects} */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + const ret = super.lapse(pokemon, lapseType); + + if (lapseType === BattlerTagLapseType.CUSTOM) { + const cancelled = new Utils.BooleanHolder(false); + applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + if (!cancelled.value) { + if (pokemon.mysteryEncounterBattleEffects) { + pokemon.mysteryEncounterBattleEffects(pokemon); + } + } + } + + return ret; + } + + /** Event when tag is removed */ + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} + /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. * @@ -2465,6 +2488,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new GorillaTacticsTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); + case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: + return new MysteryEncounterPostSummonTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 355f05523d14..b01242d083a7 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -1093,6 +1093,136 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { ] } ], + [TrainerType.BUCK]: [ + { + encounter: [ + "dialogue:stat_trainer_buck.encounter.1", + "dialogue:stat_trainer_buck.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_buck.victory.1", + "dialogue:stat_trainer_buck.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_buck.defeat.1", + "dialogue:stat_trainer_buck.defeat.2" + ] + } + ], + [TrainerType.CHERYL]: [ + { + encounter: [ + "dialogue:stat_trainer_cheryl.encounter.1", + "dialogue:stat_trainer_cheryl.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_cheryl.victory.1", + "dialogue:stat_trainer_cheryl.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_cheryl.defeat.1", + "dialogue:stat_trainer_cheryl.defeat.2" + ] + } + ], + [TrainerType.MARLEY]: [ + { + encounter: [ + "dialogue:stat_trainer_marley.encounter.1", + "dialogue:stat_trainer_marley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_marley.victory.1", + "dialogue:stat_trainer_marley.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_marley.defeat.1", + "dialogue:stat_trainer_marley.defeat.2" + ] + } + ], + [TrainerType.MIRA]: [ + { + encounter: [ + "dialogue:stat_trainer_mira.encounter.1", + "dialogue:stat_trainer_mira.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_mira.victory.1", + "dialogue:stat_trainer_mira.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_mira.defeat.1", + "dialogue:stat_trainer_mira.defeat.2" + ] + } + ], + [TrainerType.RILEY]: [ + { + encounter: [ + "dialogue:stat_trainer_riley.encounter.1", + "dialogue:stat_trainer_riley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_riley.victory.1", + "dialogue:stat_trainer_riley.victory.2" + ], + defeat: [ + "dialogue:stat_trainer_riley.defeat.1", + "dialogue:stat_trainer_riley.defeat.2" + ] + } + ], + [TrainerType.VICTOR]: [ + { + encounter: [ + "dialogue:winstrates_victor.encounter.1", + ], + victory: [ + "dialogue:winstrates_victor.victory.1" + ], + } + ], + [TrainerType.VICTORIA]: [ + { + encounter: [ + "dialogue:winstrates_victoria.encounter.1", + ], + victory: [ + "dialogue:winstrates_victoria.victory.1" + ], + } + ], + [TrainerType.VIVI]: [ + { + encounter: [ + "dialogue:winstrates_vivi.encounter.1", + ], + victory: [ + "dialogue:winstrates_vivi.victory.1" + ], + } + ], + [TrainerType.VICKY]: [ + { + encounter: [ + "dialogue:winstrates_vicky.encounter.1", + ], + victory: [ + "dialogue:winstrates_vicky.victory.1" + ], + } + ], + [TrainerType.VITO]: [ + { + encounter: [ + "dialogue:winstrates_vito.encounter.1", + ], + victory: [ + "dialogue:winstrates_vito.victory.1" + ], + } + ], [TrainerType.BROCK]: { encounter: [ "dialogue:brock.encounter.1", diff --git a/src/data/egg.ts b/src/data/egg.ts index 1cd5c65fc18b..0219f4f5b47a 100644 --- a/src/data/egg.ts +++ b/src/data/egg.ts @@ -61,7 +61,10 @@ export interface IEggOptions { /** Defines if the egg will hatch with the hidden ability of this species. * If no hidden ability exist, a random one will get choosen. */ - overrideHiddenAbility?: boolean + overrideHiddenAbility?: boolean, + + /** Can customize the message displayed for where the egg was obtained */ + eggDescriptor?: string; } export class Egg { @@ -83,6 +86,8 @@ export class Egg { private _overrideHiddenAbility: boolean; + private _eggDescriptor?: string; + //// // #endregion //// @@ -191,6 +196,8 @@ export class Egg { } else { // For legacy eggs without scene generateEggProperties(eggOptions); } + + this._eggDescriptor = eggOptions?.eggDescriptor; } //// @@ -292,13 +299,15 @@ export class Egg { public getEggTypeDescriptor(scene: BattleScene): string { switch (this.sourceType) { case EggSourceType.SAME_SPECIES_EGG: - return i18next.t("egg:sameSpeciesEgg", { species: getPokemonSpecies(this._species).getName()}); + return this._eggDescriptor ?? i18next.t("egg:sameSpeciesEgg", { species: getPokemonSpecies(this._species).getName()}); case EggSourceType.GACHA_LEGENDARY: - return `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp)).getName()})`; + return this._eggDescriptor ?? `${i18next.t("egg:gachaTypeLegendary")} (${getPokemonSpecies(getLegendaryGachaSpeciesForTimestamp(scene, this.timestamp)).getName()})`; case EggSourceType.GACHA_SHINY: - return i18next.t("egg:gachaTypeShiny"); + return this._eggDescriptor ?? i18next.t("egg:gachaTypeShiny"); case EggSourceType.GACHA_MOVE: - return i18next.t("egg:gachaTypeMove"); + return this._eggDescriptor ?? i18next.t("egg:gachaTypeMove"); + case EggSourceType.EVENT: + return this._eggDescriptor ?? i18next.t("egg:eventType"); default: console.warn("getEggTypeDescriptor case not defined. Returning default empty string"); return ""; diff --git a/src/data/move.ts b/src/data/move.ts index 1d1a788e7686..10c98e0a7f7f 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -813,10 +813,6 @@ export default class Move implements Localizable { power.value *= typeBoost.boostValue; } - if (source.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() && this.type === Type.GROUND && this.moveTarget === MoveTarget.ALL_NEAR_OTHERS) { - power.value /= 2; - } - applyMoveAttrs(VariablePowerAttr, source, target, this, power); source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power); @@ -3977,7 +3973,7 @@ 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.IntegerHolder); + 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); @@ -5248,7 +5244,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } - if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr) || target.isMax())) { + if (!this.user && move.category === MoveCategory.STATUS && (target.hasAbilityWithAttr(ForceSwitchOutImmunityAbAttr))) { return false; } @@ -6458,8 +6454,6 @@ const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !user. const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); -const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax(); - const failIfSingleBattle: MoveConditionFunc = (user, target, move) => user.scene.currentBattle.double; const failIfDampCondition: MoveConditionFunc = (user, target, move) => { @@ -6855,8 +6849,7 @@ export function initMoves() { new StatusMove(Moves.DISABLE, Type.NORMAL, 100, 20, -1, 0, 1) .attr(AddBattlerTagAttr, BattlerTagType.DISABLED, false, true) .condition((user, target, move) => target.getMoveHistory().reverse().find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual) !== undefined) - .ignoresSubstitute() - .condition(failOnMaxCondition), + .ignoresSubstitute(), new AttackMove(Moves.ACID, Type.POISON, MoveCategory.SPECIAL, 40, 100, 30, 10, 0, 1) .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .target(MoveTarget.ALL_NEAR_ENEMIES), @@ -6894,8 +6887,7 @@ export function initMoves() { .attr(RecoilAttr) .recklessMove(), new AttackMove(Moves.LOW_KICK, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, 0, 1) - .attr(WeightPowerAttr) - .condition(failOnMaxCondition), + .attr(WeightPowerAttr), new AttackMove(Moves.COUNTER, Type.FIGHTING, MoveCategory.PHYSICAL, -1, 100, 20, -1, -5, 1) .attr(CounterDamageAttr, (move: Move) => move.category === MoveCategory.PHYSICAL, 2) .target(MoveTarget.ATTACKER), @@ -6960,6 +6952,7 @@ export function initMoves() { .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) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FISSURE, Type.GROUND, MoveCategory.PHYSICAL, 200, 30, 5, -1, 0, 1) @@ -7353,6 +7346,7 @@ 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) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), @@ -8006,8 +8000,7 @@ export function initMoves() { .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.GRASS_KNOT, Type.GRASS, MoveCategory.SPECIAL, -1, 100, 20, -1, 0, 4) .attr(WeightPowerAttr) - .makesContact() - .condition(failOnMaxCondition), + .makesContact(), new AttackMove(Moves.CHATTER, Type.FLYING, MoveCategory.SPECIAL, 65, 100, 20, 100, 0, 4) .attr(ConfuseAttr) .soundBased(), @@ -8109,8 +8102,7 @@ export function initMoves() { new AttackMove(Moves.HEAVY_SLAM, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(MinimizeAccuracyAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .condition(failOnMaxCondition), + .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true), new AttackMove(Moves.SYNCHRONOISE, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 5) .target(MoveTarget.ALL_NEAR_OTHERS) .condition(unknownTypeCondition) @@ -8229,6 +8221,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) .makesContact(false) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FROST_BREATH, Type.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5) @@ -8262,8 +8255,7 @@ export function initMoves() { new AttackMove(Moves.HEAT_CRASH, Type.FIRE, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 5) .attr(MinimizeAccuracyAttr) .attr(CompareWeightPowerAttr) - .attr(HitsTagAttr, BattlerTagType.MINIMIZED, true) - .condition(failOnMaxCondition), + .attr(HitsTagAttr, 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) @@ -9119,7 +9111,7 @@ export function initMoves() { .condition(failIfDampCondition) .makesContact(false), new AttackMove(Moves.GRASSY_GLIDE, Type.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8) - .attr(IncrementMovePriorityAttr, (user, target, move) =>user.scene.arena.getTerrainType()===TerrainType.GRASSY&&user.isGrounded()), + .attr(IncrementMovePriorityAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.GRASSY && user.isGrounded()), new AttackMove(Moves.RISING_VOLTAGE, Type.ELECTRIC, MoveCategory.SPECIAL, 70, 100, 20, -1, 0, 8) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && target.isGrounded() ? 2 : 1), new AttackMove(Moves.TERRAIN_PULSE, Type.NORMAL, MoveCategory.SPECIAL, 50, 100, 10, -1, 0, 8) diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts new file mode 100644 index 000000000000..b66ca10c9f5b --- /dev/null +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -0,0 +1,186 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { randSeedInt } from "#app/utils"; +import i18next from "i18next"; +import { IEggOptions } from "#app/data/egg"; +import { EggSourceType } from "#enums/egg-source-types"; +import { EggTier } from "#enums/egg-type"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:aTrainersTest"; + +/** + * A Trainer's Test encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3816 | GitHub Issue #3816} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ATrainersTestEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.A_TRAINERS_TEST) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withSceneWaveRangeRequirement(100, 180) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Randomly pick from 1 of the 5 stat trainers to spawn + let trainerType: TrainerType; + let spriteKeys; + let trainerNameKey: string; + switch (randSeedInt(5)) { + default: + case 0: + trainerType = TrainerType.BUCK; + spriteKeys = getSpriteKeysFromSpecies(Species.CLAYDOL); + trainerNameKey = "buck"; + break; + case 1: + trainerType = TrainerType.CHERYL; + spriteKeys = getSpriteKeysFromSpecies(Species.BLISSEY); + trainerNameKey = "cheryl"; + break; + case 2: + trainerType = TrainerType.MARLEY; + spriteKeys = getSpriteKeysFromSpecies(Species.ARCANINE); + trainerNameKey = "marley"; + break; + case 3: + trainerType = TrainerType.MIRA; + spriteKeys = getSpriteKeysFromSpecies(Species.ALAKAZAM, false, 1); + trainerNameKey = "mira"; + break; + case 4: + trainerType = TrainerType.RILEY; + spriteKeys = getSpriteKeysFromSpecies(Species.LUCARIO, false, 1); + trainerNameKey = "riley"; + break; + } + + // Dialogue and tokens for trainer + encounter.dialogue.intro = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.intro_dialogue` + } + ]; + encounter.options[0].dialogue!.selected = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.accept` + } + ]; + encounter.options[1].dialogue!.selected = [ + { + speaker: `trainerNames:${trainerNameKey}`, + text: `${namespace}.${trainerNameKey}.decline` + } + ]; + + encounter.setDialogueToken("statTrainerName", i18next.t(`trainerNames:${trainerNameKey}`)); + const eggDescription = i18next.t(`${namespace}.title`) + ":\n" + i18next.t(`trainerNames:${trainerNameKey}`); + encounter.misc = { trainerType, trainerNameKey, trainerEggDescription: eggDescription }; + + // Trainer config + const trainerConfig = trainerConfigs[trainerType].clone(); + const trainerSpriteKey = trainerConfig.getSpriteKey(); + encounter.enemyPartyConfigs.push({ + levelAdditiveMultiplier: 1, + trainerConfig: trainerConfig + }); + + encounter.spriteConfigs = [ + { + spriteKey: spriteKeys.spriteKey, + fileRoot: spriteKeys.fileRoot, + hasShadow: true, + repeat: true, + isPokemon: true, + x: 22, + y: -2, + yShadow: -2 + }, + { + spriteKey: trainerSpriteKey, + fileRoot: "trainer", + hasShadow: true, + disableAnimation: true, + x: -24, + y: 4, + yShadow: 4 + } + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withIntroDialogue() + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip` + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Battle the stat trainer for an Egg and great rewards + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + await transitionMysteryEncounterIntroVisuals(scene); + + const eggOptions: IEggOptions = { + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.ULTRA + }; + 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); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Full heal party + scene.unshiftPhase(new PartyHealPhase(scene, true)); + + const eggOptions: IEggOptions = { + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eggDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.GREAT + }; + encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.rare`)); + setEncounterRewards(scene, { fillRemaining: false, rerollMultiplier: -1 }, [eggOptions]); + leaveEncounterWithoutBattle(scene); + } + ) + .withOutroDialogue([ + { + 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 new file mode 100644 index 000000000000..a9a273c6ec4e --- /dev/null +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -0,0 +1,521 @@ +import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import { BerryModifierType, 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 MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { PersistentModifierRequirement } from "../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"; +import { BerryModifier } from "#app/modifier/modifier"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { randInt } from "#app/utils"; +import { BattlerIndex } from "#app/battle"; +import { applyModifierTypeToPlayerPokemon, catchPokemon, getHighestLevelPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { PokeballType } from "#app/data/pokeball"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { BerryType } from "#enums/berry-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:absoluteAvarice"; + +/** + * Absolute Avarice encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3805 | GitHub Issue #3805} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AbsoluteAvariceEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.ABSOLUTE_AVARICE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Must have at least 4 berries to spawn + .withIntroSpriteConfigs([ + { + // This sprite has the shadow + spriteKey: "", + fileRoot: "", + species: Species.GREEDENT, + hasShadow: true, + alpha: 0.001, + repeat: true, + x: -5 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.GREEDENT, + hasShadow: false, + repeat: true, + x: -5 + }, + { + spriteKey: "lum_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: -14, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "salac_berry", + fileRoot: "items", + isItem: true, + x: 2, + y: 4, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "lansat_berry", + fileRoot: "items", + isItem: true, + x: 32, + y: 5, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "liechi_berry", + fileRoot: "items", + isItem: true, + x: 6, + y: -5, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "sitrus_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: 8, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "enigma_berry", + fileRoot: "items", + isItem: true, + x: 26, + y: -4, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "leppa_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -27, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "petaya_berry", + fileRoot: "items", + isItem: true, + x: 30, + y: -17, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "ganlon_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -11, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "apicot_berry", + fileRoot: "items", + isItem: true, + x: 14, + y: -2, + hidden: true, + disableAnimation: true + }, + { + spriteKey: "starf_berry", + fileRoot: "items", + isItem: true, + x: 18, + y: 9, + hidden: true, + disableAnimation: true + }, + ]) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withOnVisualsStart((scene: BattleScene) => { + doGreedentSpriteSteal(scene); + doBerrySpritePile(scene); + + return true; + }) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); + scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); + + // Get all player berry items, remove from party, and store reference + const berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + + // Sort berries by party member ID to more easily re-add later if necessary + const berryItemsMap = new Map(); + scene.getParty().forEach(pokemon => { + const pokemonBerries = berryItems.filter(b => b.pokemonId === pokemon.id); + if (pokemonBerries?.length > 0) { + berryItemsMap.set(pokemon.id, pokemonBerries); + } + }); + + encounter.misc = { berryItemsMap }; + + // Generates copies of the stolen berries to put on the Greedent + const bossModifierConfigs: HeldModifierConfig[] = []; + berryItems.forEach(berryMod => { + // Can't define stack count on a ModifierType, have to just create separate instances for each stack + // Overflow berries will be "lost" on the boss, but it's un-catchable anyway + for (let i = 0; i < berryMod.stackCount; i++) { + const modifierType = generateModifierType(scene, modifierTypes.BERRY, [berryMod.berryType]) as PokemonHeldItemModifierType; + bossModifierConfigs.push({ modifier: modifierType }); + } + + scene.removeModifier(berryMod); + }); + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GREEDENT), + isBoss: true, + bossSegments: 3, + moveSet: [Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF], + modifierConfigs: bossModifierConfigs, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + encounter.setDialogueToken("greedentName", getPokemonSpecies(Species.GREEDENT).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + // Provides 1x Reviver Seed to each party member at end of battle + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); + const givePartyPokemonReviverSeeds = () => { + const party = scene.getParty(); + party.forEach(p => { + if (revSeed) { + const seedModifier = revSeed.newModifier(p); + if (seedModifier) { + encounter.setDialogueToken("foodReward", seedModifier.type.name); + } + scene.addModifier(seedModifier, false, false, false, true); + } + }); + queueEncounterMessage(scene, `${namespace}.option.1.food_stash`); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, givePartyPokemonReviverSeeds); + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.STUFF_CHEEKS), + ignorePp: true + }); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const berryMap = encounter.misc.berryItemsMap; + + // Returns 2/5 of the berries stolen from each Pokemon + const party = scene.getParty(); + party.forEach(pokemon => { + const stolenBerries: BerryModifier[] = berryMap.get(pokemon.id); + const berryTypesAsArray: BerryType[] = []; + stolenBerries?.forEach(bMod => berryTypesAsArray.push(...new Array(bMod.stackCount).fill(bMod.berryType))); + const returnedBerryCount = Math.floor((berryTypesAsArray.length ?? 0) * 2 / 5); + + if (returnedBerryCount > 0) { + for (let i = 0; i < returnedBerryCount; i++) { + // Shuffle remaining berry types and pop + Phaser.Math.RND.shuffle(berryTypesAsArray); + const randBerryType = berryTypesAsArray.pop(); + + const berryModType = generateModifierType(scene, modifierTypes.BERRY, [randBerryType]) as BerryModifierType; + applyModifierTypeToPlayerPokemon(scene, pokemon, berryModType); + } + } + }); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Animate berries being eaten + doGreedentEatBerries(scene); + doBerrySpritePile(scene, true); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Let it have the food + // Greedent joins the team, level equal to 2 below highest party member + const level = getHighestLevelPlayerPokemon(scene).level - 2; + const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false); + greedent.moveset = [new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF)]; + greedent.passive = true; + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await catchPokemon(scene, greedent, null, PokeballType.POKEBALL, false); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); + +function doGreedentSpriteSteal(scene: BattleScene) { + const shakeDelay = 50; + const slideDelay = 500; + + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); + + scene.playSound("battle_anims/Follow Me"); + scene.tweens.chain({ + targets: greedentSprites, + tweens: [ + { // Slide Greedent diagonally + duration: slideDelay, + ease: "Cubic.easeOut", + y: "+=75", + x: "-=65", + scale: 1.1 + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Shake + duration: shakeDelay, + ease: "Cubic.easeOut", + yoyo: true, + x: (randInt(2) > 0 ? "-=" : "+=") + 5, + y: (randInt(2) > 0 ? "-=" : "+=") + 5, + }, + { // Slide Greedent diagonally + duration: slideDelay, + ease: "Cubic.easeOut", + y: "-=75", + x: "+=65", + scale: 1 + }, + { // Bounce at the end + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=20", + loop: 1, + } + ] + }); +} + +function doGreedentEatBerries(scene: BattleScene) { + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); + let index = 1; + scene.tweens.add({ + targets: greedentSprites, + duration: 150, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=8", + loop: 5, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + } + index++; + } + }); +} + +/** + * + * @param scene + * @param isEat Default false. Will "create" pile when false, and remove pile when true. + */ +function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) { + const berryAddDelay = 150; + let animationOrder = ["starf", "sitrus", "lansat", "salac", "apicot", "enigma", "liechi", "ganlon", "lum", "petaya", "leppa"]; + if (isEat) { + animationOrder = animationOrder.reverse(); + } + const encounter = scene.currentBattle.mysteryEncounter!; + animationOrder.forEach((berry, i) => { + const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey?.includes(berry)); + let sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite; + const sprites = encounter.introVisuals?.getSpriteAtIndex(introVisualsIndex); + if (sprites) { + sprite = sprites[0]; + tintSprite = sprites[1]; + } + scene.time.delayedCall(berryAddDelay * i + 400, () => { + if (sprite) { + sprite.setVisible(!isEat); + } + if (tintSprite) { + tintSprite.setVisible(!isEat); + } + + // Animate Petaya berry falling off the pile + if (berry === "petaya" && sprite && tintSprite && !isEat) { + scene.time.delayedCall(200, () => { + doBerryBounce(scene, [sprite, tintSprite], 30, 500); + }); + } + }); + }); +} + +function doBerryBounce(scene: BattleScene, berrySprites: Phaser.GameObjects.Sprite[], yd: number, baseBounceDuration: number) { + let bouncePower = 1; + let bounceYOffset = yd; + + const doBounce = () => { + scene.tweens.add({ + targets: berrySprites, + y: "+=" + bounceYOffset, + x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" }, + duration: bouncePower * baseBounceDuration, + ease: "Cubic.easeIn", + onComplete: () => { + bouncePower = bouncePower > 0.01 ? bouncePower * 0.5 : 0; + + if (bouncePower) { + bounceYOffset = bounceYOffset * bouncePower; + + scene.tweens.add({ + targets: berrySprites, + y: "-=" + bounceYOffset, + x: { value: "+=" + (bouncePower * bouncePower * 10), ease: "Linear" }, + duration: bouncePower * baseBounceDuration, + ease: "Cubic.easeOut", + onComplete: () => doBounce() + }); + } + } + }); + }; + + doBounce(); +} 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 new file mode 100644 index 000000000000..9f38b5a4deaa --- /dev/null +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -0,0 +1,165 @@ +import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +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 { 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"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:offerYouCantRefuse"; + +/** + * An Offer You Can't Refuse encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3808 | GitHub Issue #3808} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AnOfferYouCantRefuseEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party + .withIntroSpriteConfigs([ + { + spriteKey: Species.LIEPARD.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 0, + y: -4, + yShadow: -4 + }, + { + spriteKey: "rich_kid_m", + fileRoot: "trainer", + hasShadow: true, + x: 2, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = getHighestStatTotalPlayerPokemon(scene, false); + const price = scene.getWaveMoneyAmount(10); + + encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("price", price.toString()); + + // Store pokemon and price + encounter.misc = { + pokemon: pokemon, + price: price + }; + + // If player meets the combo OR requirements for option 2, populate the token + const opt2Req = encounter.options[1].primaryPokemonRequirements[0]; + if (opt2Req.meetsRequirement(scene)) { + const abilityToken = encounter.dialogueTokens["option2PrimaryAbility"]; + const moveToken = encounter.dialogueTokens["option2PrimaryMove"]; + if (abilityToken) { + encounter.setDialogueToken("moveOrAbility", abilityToken); + } else if (moveToken) { + encounter.setDialogueToken("moveOrAbility", moveToken); + } + } + + encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + speaker: `${namespace}.speaker`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Update money and remove pokemon from party + updatePlayerMoney(scene, encounter.misc.price); + scene.removePokemonFromPlayerParty(encounter.misc.pokemon); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player a Shiny charm + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + new MoveRequirement(EXTORTION_MOVES), + new AbilityRequirement(EXTORTION_ABILITIES)) + ) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Extort the rich kid for money + const encounter = scene.currentBattle.mysteryEncounter!; + // Update money and remove pokemon from party + updatePlayerMoney(scene, encounter.misc.price); + + setEncounterExp(scene, encounter.options[1].primaryPokemon!.id, getPokemonSpecies(Species.LIEPARD).baseExp, true); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts new file mode 100644 index 000000000000..7e6914cabddd --- /dev/null +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -0,0 +1,273 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, generateModifierType, generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { + BerryModifierType, + getPartyLuckValue, + ModifierPoolType, + ModifierTypeOption, modifierTypes, + regenerateModifierPoolThresholds, +} from "#app/modifier/modifier-type"; +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 { 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 PokemonData from "#app/system/pokemon-data"; +import { BerryModifier } from "#app/modifier/modifier"; +import i18next from "#app/plugins/i18n"; +import { BerryType } from "#enums/berry-type"; +import { PERMANENT_STATS, Stat } from "#enums/stat"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:berriesAbound"; + +/** + * Berries Abound encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3810 | GitHub Issue #3810} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BerriesAboundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BERRIES_ABOUND) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + 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 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, + dataSource: new PokemonData(bossPokemon), + isBoss: true + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate the number of extra berries that player receives + // 10-40: 2, 40-120: 4, 120-160: 5, 160-180: 7 + const numBerries = + scene.currentBattle.waveIndex > 160 ? 7 + : scene.currentBattle.waveIndex > 120 ? 5 + : scene.currentBattle.waveIndex > 40 ? 4 : 2; + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + encounter.misc = { numBerries }; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); + encounter.spriteConfigs = [ + { + spriteKey: "berry_bush", + fileRoot: "mystery-encounters", + x: 25, + y: -6, + yShadow: -7, + disableAnimation: true, + hasShadow: true + }, + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + isPokemon: true + } + ]; + + // Get fastest party pokemon for option 2 + const fastestPokemon = getHighestStatPlayerPokemon(scene, PERMANENT_STATS[Stat.SPD], true); + encounter.misc.fastestPokemon = fastestPokemon; + encounter.misc.enemySpeed = bossPokemon.getStat(Stat.SPD); + encounter.setDialogueToken("fastestPokemon", fastestPokemon.getNameToRender()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + const numBerries = encounter.misc.numBerries; + + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + const mod = generateModifierTypeOption(scene, modifierTypes.BERRY); + if (mod) { + shopOptions.push(mod); + } + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick race for berries + const encounter = scene.currentBattle.mysteryEncounter!; + const fastestPokemon = encounter.misc.fastestPokemon; + const enemySpeed = encounter.misc.enemySpeed; + const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1); + const numBerries = encounter.misc.numBerries; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + const mod = generateModifierTypeOption(scene, modifierTypes.BERRY); + if (mod) { + shopOptions.push(mod); + } + } + + if (speedDiff < 1) { + // Caught and attacked by boss, gets +1 to all stats at start of fight + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const config = scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; + config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.2.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + }; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); + await showEncounterText(scene, `${namespace}.option.2.selected_bad`); + await initBattleWithEnemyConfig(scene, config); + return; + } else { + // Gains 1 berry for every 10% faster the player's pokemon is than the enemy, up to a max of numBerries, minimum of 2 + const numBerriesGrabbed = Math.max(Math.min(Math.round((speedDiff - 1)/0.08), numBerries), 2); + encounter.setDialogueToken("numBerries", String(numBerriesGrabbed)); + const doFasterBerryRewards = async () => { + const berryText = numBerriesGrabbed + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it (trying to give to fastest first) + for (let i = 0; i < numBerriesGrabbed; i++) { + await tryGiveBerry(scene, fastestPokemon); + } + }; + + setEncounterExp(scene, fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doFasterBerryRewards); + await showEncounterText(scene, `${namespace}.option.2.selected`); + leaveEncounterWithoutBattle(scene); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) { + const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; + const berry = generateModifierType(scene, modifierTypes.BERRY, [berryType]) as BerryModifierType; + + const party = scene.getParty(); + + // Will try to apply to prioritized pokemon first, then do normal application method if it fails + if (prioritizedPokemon) { + const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier + && m.pokemonId === prioritizedPokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier; + + if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, prioritizedPokemon, berry); + return; + } + } + + // Iterate over the party until berry was successfully given + for (const pokemon of party) { + const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier + && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier; + + if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, berry); + return; + } + } +} diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts new file mode 100644 index 000000000000..7fdaec35dc38 --- /dev/null +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -0,0 +1,670 @@ +import { + EnemyPartyConfig, generateModifierType, + generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, + selectOptionThenPokemon, + selectPokemonForOption, + setEncounterRewards, + transitionMysteryEncounterIntroVisuals, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, + TrainerSlot, +} from "#app/data/trainer-config"; +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 { isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; +import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { getEncounterText, showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { Moves } from "#enums/moves"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { + AttackTypeBoosterHeldItemTypeRequirement, + CombinationPokemonRequirement, + HeldItemRequirement, + TypeRequirement +} from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { Type } from "#app/data/type"; +import { AttackTypeBoosterModifierType, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import { + AttackTypeBoosterModifier, + BypassSpeedChanceModifier, + ContactHeldItemTransferChanceModifier, + PokemonHeldItemModifier +} from "#app/modifier/modifier"; +import i18next from "i18next"; +import MoveInfoOverlay from "#app/ui/move-info-overlay"; +import { allMoves } from "#app/data/move"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:bugTypeSuperfan"; + +const POOL_1_POKEMON = [ + Species.PARASECT, + Species.VENOMOTH, + Species.LEDIAN, + Species.ARIADOS, + Species.YANMA, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.MASQUERAIN, + Species.NINJASK, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ANORITH, + Species.KRICKETUNE, + Species.WORMADAM, + Species.MOTHIM, + Species.SKORUPI, + Species.JOLTIK, + Species.LARVESTA, + Species.VIVILLON, + Species.CHARJABUG, + Species.RIBOMBEE, + Species.SPIDOPS, + Species.LOKIX +]; + +const POOL_2_POKEMON = [ + Species.SCYTHER, + Species.PINSIR, + Species.HERACROSS, + Species.FORRETRESS, + Species.SCIZOR, + Species.SHUCKLE, + Species.SHEDINJA, + Species.ARMALDO, + Species.VESPIQUEN, + Species.DRAPION, + Species.YANMEGA, + Species.LEAVANNY, + Species.SCOLIPEDE, + Species.CRUSTLE, + Species.ESCAVALIER, + Species.ACCELGOR, + Species.GALVANTULA, + Species.VIKAVOLT, + Species.ARAQUANID, + Species.ORBEETLE, + Species.CENTISKORCH, + Species.FROSMOTH, + Species.KLEAVOR, +]; + +const POOL_3_POKEMON: { species: Species, formIndex?: number }[] = [ + { + species: Species.PINSIR, + formIndex: 1 + }, + { + species: Species.SCIZOR, + formIndex: 1 + }, + { + species: Species.HERACROSS, + formIndex: 1 + }, + { + species: Species.ORBEETLE, + formIndex: 1 + }, + { + species: Species.CENTISKORCH, + formIndex: 1 + }, + { + species: Species.DURANT, + }, + { + species: Species.VOLCARONA, + }, + { + species: Species.GOLISOPOD, + }, +]; + +const POOL_4_POKEMON = [ + Species.GENESECT, + Species.SLITHER_WING, + Species.BUZZWOLE, + Species.PHEROMOSA +]; + +const PHYSICAL_TUTOR_MOVES = [ + Moves.MEGAHORN, + Moves.X_SCISSOR, + Moves.ATTACK_ORDER, + Moves.PIN_MISSILE, + Moves.FIRST_IMPRESSION +]; + +const SPECIAL_TUTOR_MOVES = [ + Moves.SILVER_WIND, + Moves.BUG_BUZZ, + Moves.SIGNAL_BEAM, + Moves.POLLEN_PUFF +]; + +const STATUS_TUTOR_MOVES = [ + Moves.STRING_SHOT, + Moves.STICKY_WEB, + Moves.SILK_TRAP, + Moves.RAGE_POWDER, + Moves.HEAL_ORDER +]; + +const MISC_TUTOR_MOVES = [ + Moves.BUG_BITE, + Moves.LEECH_LIFE, + Moves.DEFEND_ORDER, + Moves.QUIVER_DANCE, + Moves.TAIL_GLOW, + Moves.INFESTATION, + Moves.U_TURN +]; + +/** + * Bug Type Superfan encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3810 | GitHub Issue #3810} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BugTypeSuperfanEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team + new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1), + new TypeRequirement(Type.BUG, false, 1) + )) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Calculates what trainers are available for battle in the encounter + + // Bug type superfan trainer config + const config = getTrainerConfigForWave(scene.currentBattle.waveIndex); + const spriteKey = config.getSpriteKey(); + encounter.enemyPartyConfigs.push({ + trainerConfig: config, + female: true, + }); + + encounter.spriteConfigs = [ + { + spriteKey: spriteKey, + fileRoot: "trainer", + hasShadow: true, + }, + ]; + + const requiredItems = [ + generateModifierType(scene, modifierTypes.QUICK_CLAW), + generateModifierType(scene, modifierTypes.GRIP_CLAW), + generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.BUG]), + ]; + + const requiredItemString = requiredItems.map(m => m?.name ?? "unknown").join("/"); + encounter.setDialogueToken("requiredBugItems", requiredItemString); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Select battle the bug trainer + const encounter = scene.currentBattle.mysteryEncounter!; + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + // Init the moves available for tutor + const moveTutorOptions: PokemonMove[] = []; + moveTutorOptions.push(new PokemonMove(PHYSICAL_TUTOR_MOVES[randSeedInt(PHYSICAL_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(SPECIAL_TUTOR_MOVES[randSeedInt(SPECIAL_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(STATUS_TUTOR_MOVES[randSeedInt(STATUS_TUTOR_MOVES.length)])); + moveTutorOptions.push(new PokemonMove(MISC_TUTOR_MOVES[randSeedInt(MISC_TUTOR_MOVES.length)])); + encounter.misc = { + moveTutorOptions + }; + + // Assigns callback that teaches move before continuing to rewards + encounter.onRewards = doBugTypeMoveTutor; + + setEncounterRewards(scene, { fillRemaining: true }); + await transitionMysteryEncounterIntroVisuals(scene, true, true); + await initBattleWithEnemyConfig(scene, config); + } + ) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new TypeRequirement(Type.BUG, false, 1)) // Must have 1 Bug type on team + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip` + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Player shows off their bug types + const encounter = scene.currentBattle.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()); + + if (numBugTypes < 2) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SUPER_LURE, modifierTypes.GREAT_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_0_to_1`, + }, + ]; + } else if (numBugTypes < 4) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.QUICK_CLAW, modifierTypes.MAX_LURE, modifierTypes.ULTRA_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_2_to_3`, + }, + ]; + } else if (numBugTypes < 6) { + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.GRIP_CLAW, modifierTypes.MAX_LURE, modifierTypes.ROGUE_BALL], fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_4_to_5`, + }, + ]; + } else { + // If player has any evolution/form change items that are valid for their party, will spawn one of those items in addition to a Master Ball + const modifierOptions: ModifierTypeOption[] = [generateModifierTypeOption(scene, modifierTypes.MASTER_BALL)!, generateModifierTypeOption(scene, modifierTypes.MAX_LURE)!]; + const specialOptions: ModifierTypeOption[] = []; + + const nonRareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.EVOLUTION_ITEM); + if (nonRareEvolutionModifier) { + specialOptions.push(nonRareEvolutionModifier); + } + const rareEvolutionModifier = generateModifierTypeOption(scene, modifierTypes.RARE_EVOLUTION_ITEM); + if (rareEvolutionModifier) { + specialOptions.push(rareEvolutionModifier); + } + const formChangeModifier = generateModifierTypeOption(scene, modifierTypes.FORM_CHANGE_ITEM); + if (formChangeModifier) { + specialOptions.push(formChangeModifier); + } + if (specialOptions.length > 0) { + modifierOptions.push(specialOptions[randSeedInt(specialOptions.length)]); + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifierOptions, fillRemaining: false }); + encounter.selectedOption!.dialogue!.selected = [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_6`, + }, + ]; + } + }) + .withOptionPhase(async (scene: BattleScene) => { + // Player shows off their bug types + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + // Meets one or both of the below reqs + new HeldItemRequirement(["BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier"], 1), + new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1) + )) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_dialogue`, + }, + ], + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter(item => { + return item instanceof BypassSpeedChanceModifier || + item instanceof ContactHeldItemTransferChanceModifier || + (item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG); + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("selectedItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has valid item, it can be selected + const hasValidItem = pokemon.getHeldItems().some(item => { + return item instanceof BypassSpeedChanceModifier || + item instanceof ContactHeldItemTransferChanceModifier || + (item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG); + }); + if (!hasValidItem) { + return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + scene.updateModifiers(true, true); + + const bugNet = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; + bugNet.type.tier = ModifierTier.ROGUE; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [bugNet], guaranteedModifierTypeFuncs: [modifierTypes.REVIVER_SEED], fillRemaining: false }); + leaveEncounterWithoutBattle(scene, true); + }) + .build()) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +function getTrainerConfigForWave(waveIndex: number) { + // Bug type superfan trainer config + const config = trainerConfigs[TrainerType.BUG_TYPE_SUPERFAN].clone(); + config.name = i18next.t("trainerNames:bug_type_superfan"); + + const pool3Copy = POOL_3_POKEMON.slice(0); + randSeedShuffle(pool3Copy); + const pool3Mon = pool3Copy.pop()!; + + if (waveIndex < 30) { + // Use default template (2 AVG) + config + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)); + } else if (waveIndex < 50) { + config + .setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 70) { + config + .setPartyTemplates(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 100) { + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_1_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)); + } else if (waveIndex < 120) { + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })); + } else if (waveIndex < 140) { + randSeedShuffle(pool3Copy); + const pool3Mon2 = pool3Copy.pop()!; + config + .setPartyTemplates(new TrainerPartyTemplate(5, PartyMemberStrength.AVERAGE)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([pool3Mon2.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon2.formIndex)) { + p.formIndex = pool3Mon2.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })); + } else if (waveIndex < 160) { + config + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(1, PartyMemberStrength.STRONG))) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc(POOL_2_POKEMON, TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_4_POKEMON, TrainerSlot.TRAINER, true)); + } else { + config + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(4, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(1, PartyMemberStrength.STRONG))) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BEEDRILL ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.BUTTERFREE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.formIndex = 1; + p.generateAndPopulateMoveset(); + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([pool3Mon.species], TrainerSlot.TRAINER, true, p => { + if (!isNullOrUndefined(pool3Mon.formIndex)) { + p.formIndex = pool3Mon.formIndex!; + p.generateAndPopulateMoveset(); + p.generateName(); + } + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc(POOL_4_POKEMON, TrainerSlot.TRAINER, true)); + } + + return config; +} + +function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSlot = TrainerSlot.TRAINER, ignoreEvolution: boolean = false, postProcess?: (enemyPokemon: EnemyPokemon) => void) { + return (scene: BattleScene, level: number, strength: PartyMemberStrength) => { + let species = Utils.randSeedItem(speciesPool); + if (!ignoreEvolution) { + species = getPokemonSpecies(species).getTrainerSpeciesForLevel(level, true, strength); + } + return scene.addEnemyPokemon(getPokemonSpecies(species), level, trainerSlot, undefined, undefined, postProcess); + }; +} + +function doBugTypeMoveTutor(scene: BattleScene): Promise { + return new Promise(async resolve => { + const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; + await showEncounterDialogue(scene, `${namespace}.battle_won`, `${namespace}.speaker`); + + const overlayScale = 1; + const moveInfoOverlay = new MoveInfoOverlay(scene, { + delayVisibility: false, + scale: overlayScale, + onSide: true, + right: true, + x: 1, + y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, + width: (scene.game.canvas.width / 6) - 2, + }); + scene.ui.add(moveInfoOverlay); + + const optionSelectItems = moveOptions.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + return true; + }, + onHover: () => { + moveInfoOverlay.active = true; + moveInfoOverlay.show(allMoves[move.moveId]); + }, + }; + return option; + }); + + const onHoverOverCancel = () => { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + }; + + const result = await selectOptionThenPokemon(scene, optionSelectItems, `${namespace}.teach_move_prompt`, undefined, onHoverOverCancel); + // let forceExit = !!result; + if (!result) { + moveInfoOverlay.active = false; + moveInfoOverlay.setVisible(false); + } + + // TODO: add menu to confirm player doesn't want to teach a move + // while (!result && !forceExit) { + // // Didn't teach a move, ask the player to confirm they don't want to teach a move + // await showEncounterDialogue(scene, `${namespace}.confirm_no_teach`, `${namespace}.speaker`); + // const confirm = await new Promise(confirmResolve => { + // scene.ui.setMode(Mode.CONFIRM, () => confirmResolve(true), () => confirmResolve(false)); + // }); + // scene.ui.clearText(); + // await scene.ui.setMode(Mode.MESSAGE); + // if (confirm) { + // // No teach, break out of loop + // forceExit = true; + // } else { + // // Re-show learn menu + // result = await selectOptionThenPokemon(scene, optionSelectItems, `${namespace}.teach_move_prompt`, undefined, onHoverOverCancel); + // if (!result) { + // moveInfoOverlay.active = false; + // moveInfoOverlay.setVisible(false); + // } + // } + // } + + // Option select complete, handle if they are learning a move + if (result && result.selectedOptionIndex < moveOptions.length) { + scene.unshiftPhase(new LearnMovePhase(scene, result.selectedPokemonIndex, moveOptions[result.selectedOptionIndex].moveId)); + } + + // Complete battle and go to rewards + resolve(); + }); +} diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts new file mode 100644 index 000000000000..061d2a33e8a5 --- /dev/null +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -0,0 +1,496 @@ +import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import { TrainerType } from "#enums/trainer-type"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Abilities } from "#enums/abilities"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Type } from "#app/data/type"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { randSeedInt, randSeedShuffle } from "#app/utils"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { Mode } from "#app/ui/ui"; +import i18next from "i18next"; +import { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { Ability } from "#app/data/ability"; +import { BerryModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { BattlerIndex } from "#app/battle"; +import { Moves } from "#enums/moves"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, GameModes } from "#app/game-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:clowningAround"; + +const RANDOM_ABILITY_POOL = [ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER +]; + +/** + * Clowning Around encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3807 | GitHub Issue #3807} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ClowningAroundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withDisabledGameModes(GameModes.CHALLENGE) + .withSceneWaveRangeRequirement(80, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withAnimations(EncounterAnim.SMOKESCREEN) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: Species.MR_MIME.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: -25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: Species.BLACEPHALON.toString(), + fileRoot: "pokemon/exp", + hasShadow: true, + repeat: true, + x: 25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: "harlequin", + fileRoot: "trainer", + hasShadow: true, + x: 0, + y: 2, + yShadow: 2 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker` + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const clownTrainerType = TrainerType.HARLEQUIN; + const clownConfig = trainerConfigs[clownTrainerType].clone(); + const clownPartyTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)); + clownConfig.setPartyTemplates(clownPartyTemplate); + clownConfig.setDoubleOnly(); + // @ts-ignore + clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists + + // Generate random ability for Blacephalon from pool + const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)]; + encounter.setDialogueToken("ability", new Ability(ability, 3).name); + encounter.misc = { ability }; + + encounter.enemyPartyConfigs.push({ + trainerConfig: clownConfig, + pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon + { + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }, + { // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter + species: getPokemonSpecies(Species.BLACEPHALON), + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ ability: ability, types: [randSeedInt(18), randSeedInt(18)] }), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }, + ], + doubleBattle: true + }); + + // Load animations/sfx for start of fight moves + loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]); + + encounter.setDialogueToken("blacephalonName", getPokemonSpecies(Species.BLACEPHALON).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn battle + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { fillRemaining: true }); + + // TODO: when Magic Room and Wonder Room are implemented, add those to start of battle + encounter.startOfBattleEffects.push( + { // Mr. Mime copies the Blacephalon's random ability + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY_2], + move: new PokemonMove(Moves.ROLE_PLAY), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER_2], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }); + + await transitionMysteryEncounterIntroVisuals(scene); + await initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene): Promise => { + // After the battle, offer the player the opportunity to permanently swap ability + const abilityWasSwapped = await handleSwapAbility(scene); + if (abilityWasSwapped) { + await showEncounterText(scene, `${namespace}.option.1.ability_gained`); + } + + // Play animations once ability swap is complete + // Trainer sprite that is shown at end of battle is not the same as mystery encounter intro visuals + scene.tweens.add({ + targets: scene.currentBattle.trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 250 + }); + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + return true; + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + text: `${namespace}.option.2.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Swap player's items on pokemon with the most items + // Item comparisons look at whichever Pokemon has the greatest number of TRANSFERABLE, non-berry items + // So Vitamins, form change items, etc. are not included + const encounter = scene.currentBattle.mysteryEncounter!; + + const party = scene.getParty(); + let mostHeldItemsPokemon = party[0]; + let count = mostHeldItemsPokemon.getHeldItems() + .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + + party.forEach(pokemon => { + const nextCount = pokemon.getHeldItems() + .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + if (nextCount > count) { + mostHeldItemsPokemon = pokemon; + count = nextCount; + } + }); + + encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); + + const items = mostHeldItemsPokemon.getHeldItems(); + + // Shuffles Berries (if they have any) + let numBerries = 0; + items.filter(m => m instanceof BerryModifier) + .forEach(m => { + numBerries += m.stackCount; + scene.removeModifier(m); + }); + + generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries"); + + // 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)) + .forEach(m => { + const type = m.type.withTierFromPool(); + const tier = type.tier ?? ModifierTier.ULTRA; + if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { + numRogue += m.stackCount; + scene.removeModifier(m); + } else if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { + numUltra += m.stackCount; + scene.removeModifier(m); + } + }); + + generateItemsOfTier(scene, mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); + generateItemsOfTier(scene, mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene, true, true, 200); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + text: `${namespace}.option.3.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Randomize the second type of all player's pokemon + // If the pokemon does not normally have a second type, it will gain 1 + for (const pokemon of scene.getParty()) { + const originalTypes = pokemon.getTypes(false, false, true); + + // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type + // Makes the "randomness" of the shuffle slightly less punishing + let priorityTypes = pokemon.moveset + .filter(move => move && !originalTypes.includes(move.getMove().type) && move.getMove().category !== MoveCategory.STATUS) + .map(move => move!.getMove().type); + if (priorityTypes?.length > 0) { + priorityTypes = [...new Set(priorityTypes)]; + randSeedShuffle(priorityTypes); + } + + const newTypes = [originalTypes[0]]; + let secondType: Type | null = null; + while (secondType === null || secondType === newTypes[0] || originalTypes.includes(secondType)) { + if (priorityTypes.length > 0) { + secondType = priorityTypes.pop() ?? null; + } else { + secondType = randSeedInt(18) as Type; + } + } + newTypes.push(secondType); + if (!pokemon.mysteryEncounterPokemonData) { + pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.mysteryEncounterPokemonData.types = newTypes; + } + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene, true, true, 200); + }) + .build() + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +async function handleSwapAbility(scene: BattleScene) { + return new Promise(async resolve => { + await showEncounterDialogue(scene, `${namespace}.option.1.apply_ability_dialogue`, `${namespace}.speaker`); + await showEncounterText(scene, `${namespace}.option.1.apply_ability_message`); + + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }); +} + +function displayYesNoOptions(scene: BattleScene, resolve) { + showEncounterText(scene, `${namespace}.option.1.ability_prompt`, null, 500, false); + const fullOptions = [ + { + label: i18next.t("menu:yes"), + handler: () => { + onYesAbilitySwap(scene, resolve); + return true; + } + }, + { + label: i18next.t("menu:no"), + handler: () => { + resolve(false); + return true; + } + } + ]; + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0 + }; + scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true); +} + +function onYesAbilitySwap(scene: BattleScene, resolve) { + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Do ability swap + const encounter = scene.currentBattle.mysteryEncounter!; + if (!pokemon.mysteryEncounterPokemonData) { + pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability; + encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); + scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); + }; + + const onPokemonNotSelected = () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }; + + selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected); +} + +function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: number, tier: ModifierTier | "Berries") { + // These pools have to be defined at runtime so that modifierTypes exist + // Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon + // This is to prevent "over-generating" a random item of a certain type during item swaps + const ultraPool = [ + [modifierTypes.REVIVER_SEED, 1], + [modifierTypes.GOLDEN_PUNCH, 5], + [modifierTypes.ATTACK_TYPE_BOOSTER, 99], + [modifierTypes.QUICK_CLAW, 3], + [modifierTypes.WIDE_LENS, 3] + ]; + + const roguePool = [ + [modifierTypes.LEFTOVERS, 4], + [modifierTypes.SHELL_BELL, 4], + [modifierTypes.SOUL_DEW, 10], + [modifierTypes.SOOTHE_BELL, 3], + [modifierTypes.SCOPE_LENS, 1], + [modifierTypes.BATON, 1], + [modifierTypes.FOCUS_BAND, 5], + [modifierTypes.KINGS_ROCK, 3], + [modifierTypes.GRIP_CLAW, 5] + ]; + + const berryPool = [ + [BerryType.APICOT, 3], + [BerryType.ENIGMA, 2], + [BerryType.GANLON, 3], + [BerryType.LANSAT, 3], + [BerryType.LEPPA, 2], + [BerryType.LIECHI, 3], + [BerryType.LUM, 2], + [BerryType.PETAYA, 3], + [BerryType.SALAC, 2], + [BerryType.SITRUS, 2], + [BerryType.STARF, 3] + ]; + + let pool: any[]; + if (tier === "Berries") { + pool = berryPool; + } else { + pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool; + } + + for (let i = 0; i < numItems; i++) { + const randIndex = randSeedInt(pool.length); + const newItemType = pool[randIndex]; + let newMod; + if (tier === "Berries") { + newMod = generateModifierType(scene, modifierTypes.BERRY, [newItemType[0]]) as PokemonHeldItemModifierType; + } else { + newMod = generateModifierType(scene, newItemType[0]) as PokemonHeldItemModifierType; + } + applyModifierTypeToPlayerPokemon(scene, pokemon, newMod); + // Decrement max stacks and remove from pool if at max + newItemType[1]--; + if (newItemType[1] <= 0) { + pool.splice(randIndex, 1); + } + } +} diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts new file mode 100644 index 000000000000..046e2b2f8768 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -0,0 +1,325 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { TrainerSlot } from "#app/data/trainer-config"; +import PokemonData from "#app/system/pokemon-data"; +import { Biome } from "#enums/biome"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +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 { PokeballType } from "#enums/pokeball"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:dancingLessons"; + +// Fire form +const BAILE_STYLE_BIOMES = [ + Biome.VOLCANO, + Biome.BEACH, + Biome.ISLAND, + Biome.WASTELAND, + Biome.MOUNTAIN, + Biome.BADLANDS, + Biome.DESERT +]; + +// Electric form +const POM_POM_STYLE_BIOMES = [ + Biome.CONSTRUCTION_SITE, + Biome.POWER_PLANT, + Biome.FACTORY, + Biome.LABORATORY, + Biome.SLUM, + Biome.METROPOLIS, + Biome.DOJO +]; + +// Psychic form +const PAU_STYLE_BIOMES = [ + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.MEADOW, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.FOREST +]; + +// Ghost form +const SENSU_STYLE_BIOMES = [ + Biome.RUINS, + Biome.SWAMP, + Biome.CAVE, + Biome.ABYSS, + Biome.GRAVEYARD, + Biome.LAKE, + Biome.TEMPLE +]; + +/** + * Dancing Lessons encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3823 | GitHub Issue #3823} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DancingLessonsEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DANCING_LESSONS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // Uses a real Pokemon sprite instead of ME Intro Visuals + .withAnimations(EncounterAnim.DANCE) + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withOnVisualsStart((scene: BattleScene) => { + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]); + danceAnim.play(scene); + + return true; + }) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + 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 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) { + enemyPokemon.moveset.push(new PokemonMove(Moves.REVELATION_DANCE)); + } else { + enemyPokemon.moveset[0] = new PokemonMove(Moves.REVELATION_DANCE); + } + } + + // Set the form index based on the biome + // Defaults to Baile style if somehow nothing matches + const currentBiome = scene.arena.biomeType; + if (BAILE_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 0; + } else if (POM_POM_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 1; + } else if (PAU_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 2; + } else if (SENSU_STYLE_BIOMES.includes(currentBiome)) { + enemyPokemon.formIndex = 3; + } else { + enemyPokemon.formIndex = 0; + } + + const oricorioData = new PokemonData(enemyPokemon); + const oricorio = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false, oricorioData); + + // Adds a real Pokemon sprite to the field (required for the animation) + scene.getEnemyParty().forEach(enemyPokemon => { + scene.field.remove(enemyPokemon, true); + }); + scene.currentBattle.enemyParty = [oricorio]; + scene.field.add(oricorio); + // Spawns on offscreen field + oricorio.x -= 300; + encounter.loadAssets.push(oricorio.loadAssets()); + + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [{ + species: species, + dataSource: oricorioData, + isBoss: true, + // Gets +1 to all stats except SPD on battle start + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF], 1)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + encounter.misc = { + oricorioData + }; + + encounter.setDialogueToken("oricorioName", getPokemonSpecies(Species.ORICORIO).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.REVELATION_DANCE), + ignorePp: true + }); + + await hideOricorioPokemon(scene); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.BATON], fillRemaining: true }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Learn its Dance + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + scene.unshiftPhase(new LearnMovePhase(scene, scene.getParty().indexOf(pokemon), Moves.REVELATION_DANCE)); + + // Play animation again to "learn" the dance + const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon()); + danceAnim.play(scene); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Learn its Dance + hideOricorioPokemon(scene); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(DANCING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Open menu for selecting pokemon with a Dancing move + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for nature selection + return pokemon.moveset + .filter(move => move && DANCING_MOVES.includes(move.getMove().id)) + .map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and second option selected + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("selectedMove", move.getName()); + encounter.misc.selectedMove = move; + + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that have a Dancing move can be selected + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Show the Oricorio a dance, and recruit it + const encounter = scene.currentBattle.mysteryEncounter!; + const oricorio = encounter.misc.oricorioData.toPokemon(scene); + oricorio.passive = true; + + // Ensure the Oricorio's moveset gains the Dance move the player used + const move = encounter.misc.selectedMove?.getMove().id; + if (!oricorio.moveset.some(m => m.getMove().id === move)) { + if (oricorio.moveset.length < 4) { + oricorio.moveset.push(new PokemonMove(move)); + } else { + oricorio.moveset[3] = new PokemonMove(move); + } + } + + hideOricorioPokemon(scene); + await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); + +function hideOricorioPokemon(scene: BattleScene) { + return new Promise(resolve => { + const oricorioSprite = scene.getEnemyParty()[0]; + scene.tweens.add({ + targets: oricorioSprite, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(oricorioSprite, true); + resolve(); + } + }); + }); +} diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts new file mode 100644 index 000000000000..212ff6ed1bb1 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -0,0 +1,197 @@ +import { Type } from "#app/data/type"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +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 { 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"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** 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 */ +const excludedBosses = [ + Species.NECROZMA, + Species.COSMOG, + Species.COSMOEM, + Species.SOLGALEO, + Species.LUNALA, + Species.ETERNATUS, + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.KORAIDON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.MIRAIDON, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN, + Species.MEW, + Species.CELEBI, + Species.DEOXYS, + Species.JIRACHI, + Species.PHIONE, + Species.MANAPHY, + Species.ARCEUS, + Species.VICTINI, + Species.MELTAN, + Species.PECHARUNT, +]; + +/** + * Dark Deal encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3806 | GitHub Issue #3806} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DarkDealEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DARK_DEAL) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withIntroSpriteConfigs([ + { + spriteKey: "mad_scientist_m", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "dark_deal_porygon", + fileRoot: "mystery-encounters", + hasShadow: true, + repeat: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party + .withCatchAllowed(true) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected_dialogue`, + }, + { + text: `${namespace}.option.1.selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens + // Will never return last battle able mon and instead pick fainted/unable to battle + const removedPokemon = getRandomPlayerPokemon(scene, false, true); + // Get all the pokemon's held items + const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + scene.removePokemonFromPlayerParty(removedPokemon); + + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); + + // Store removed pokemon types + encounter.misc = { + removedTypes: removedPokemon.getTypes(), + modifiers + }; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player 5 Rogue Balls + const encounter = scene.currentBattle.mysteryEncounter!; + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); + + // Start encounter with random legendary (7-10 starter strength) that has level additive + const bossTypes: Type[] = encounter.misc.removedTypes; + const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers; + // 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]; + const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + modifierConfigs: bossModifiers.map(m => { + return { + modifier: m + }; + }) + }; + if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { + pokemonConfig.formIndex = 0; + } + const config: EnemyPartyConfig = { + pokemonConfigs: [pokemonConfig], + }; + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro` + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts new file mode 100644 index 000000000000..ed9344d3c959 --- /dev/null +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -0,0 +1,309 @@ +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +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 MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../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"; +import { HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import i18next from "#app/plugins/i18n"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:delibirdy"; + +/** Berries only */ +const OPTION_2_ALLOWED_MODIFIERS = ["BerryModifier", "PokemonInstantReviveModifier"]; + +/** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ +const OPTION_3_DISALLOWED_MODIFIERS = [ + "BerryModifier", + "PokemonInstantReviveModifier", + "TerastallizeModifier", + "PokemonBaseStatModifier", + "PokemonBaseStatTotalModifier" +]; + +/** + * Delibird-y encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3804 | GitHub Issue #3804} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +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 + .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) + )) + .withIntroSpriteConfigs([ + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + startFrame: 38, + scale: 0.94 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + scale: 1.06 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.DELIBIRD, + hasShadow: true, + repeat: true, + startFrame: 65, + x: 1, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + } + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("delibirdName", getPokemonSpecies(Species.DELIBIRD).getName()); + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 2) // Must have money to spawn + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player an Ability Charm + // Check if the player has max stacks of that item already + const existing = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that can gain benefits are above 1/3rd HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + if (modifier.type.name.includes("Berry")) { + // Check if the player has max stacks of that Candy Jar already + const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } + } else { + // Check if the player has max stacks of that Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that can gain benefits are above 1/3rd HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Check if the player has max stacks of Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts new file mode 100644 index 000000000000..e35ca08b6a0d --- /dev/null +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -0,0 +1,164 @@ +import { + leaveEncounterWithoutBattle, + setEncounterRewards, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { ModifierTypeFunc, modifierTypes } from "#app/modifier/modifier-type"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:departmentStoreSale"; + +/** + * Department Store Sale encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3797 | GitHub Issue #3797} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DepartmentStoreSaleEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[0], 100) + .withIntroSpriteConfigs([ + { + spriteKey: "b2w2_lady", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -20, + }, + { + spriteKey: "", + fileRoot: "", + species: Species.FURFROU, + hasShadow: true, + repeat: true, + x: 30, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + }, + async (scene: BattleScene) => { + // Choose TMs + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 4) { + // 2/2/1 weight on TM rarity + const roll = randSeedInt(5); + if (roll < 2) { + modifiers.push(modifierTypes.TM_COMMON); + } else if (roll < 4) { + modifiers.push(modifierTypes.TM_GREAT); + } else { + modifiers.push(modifierTypes.TM_ULTRA); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }, + async (scene: BattleScene) => { + // Choose Vitamins + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 3) { + // 2/1 weight on base stat booster vs PP Up + const roll = randSeedInt(3); + if (roll === 0) { + modifiers.push(modifierTypes.PP_UP); + } else { + modifiers.push(modifierTypes.BASE_STAT_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + }, + async (scene: BattleScene) => { + // Choose X Items + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 5) { + // 4/1 weight on base stat booster vs Dire Hit + const roll = randSeedInt(5); + if (roll === 0) { + modifiers.push(modifierTypes.DIRE_HIT); + } else { + modifiers.push(modifierTypes.TEMP_STAT_STAGE_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + }, + async (scene: BattleScene) => { + // Choose Pokeballs + const modifiers: ModifierTypeFunc[] = []; + let i = 0; + while (i < 4) { + // 10/30/20/5 weight on pokeballs + const roll = randSeedInt(65); + if (roll < 10) { + modifiers.push(modifierTypes.POKEBALL); + } else if (roll < 40) { + modifiers.push(modifierTypes.GREAT_BALL); + } else if (roll < 60) { + modifiers.push(modifierTypes.ULTRA_BALL); + } else { + modifiers.push(modifierTypes.ROGUE_BALL); + } + i++; + } + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false, }); + leaveEncounterWithoutBattle(scene); + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts new file mode 100644 index 000000000000..e0101d60a2ab --- /dev/null +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -0,0 +1,235 @@ +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Stat } from "#enums/stat"; +import i18next from "i18next"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fieldTrip"; + +/** + * Field Trip encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3794 | GitHub Issue #3794} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieldTripEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIELD_TRIP) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "preschooler_m", + fileRoot: "trainer", + hasShadow: true, + }, + { + spriteKey: "teacher", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "preschooler_f", + fileRoot: "trainer", + hasShadow: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.physical`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.PHYSICAL); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ATK])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.DEF])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.special`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.SPECIAL); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPATK])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPDEF])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + label: move.getName(), + handler: () => { + // Pokemon and move selected + encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.status`)); + pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.STATUS); + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.ACC])!, + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_STAGE_BOOSTER, [Stat.SPD])!, + generateModifierTypeOption(scene, modifierTypes.GREAT_BALL)!, + generateModifierTypeOption(scene, modifierTypes.IV_SCANNER)!, + generateModifierTypeOption(scene, modifierTypes.RARER_CANDY)!, + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .build(); + +function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move: PokemonMove, correctMoveCategory: MoveCategory) { + const encounter = scene.currentBattle.mysteryEncounter!; + const correctMove = move.getMove().category === correctMoveCategory; + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); + encounter.setDialogueToken("move", move.getName()); + if (!correctMove) { + encounter.selectedOption!.dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + { + text: `${namespace}.incorrect`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.incorrect_exp`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + } else { + encounter.selectedOption!.dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + { + text: `${namespace}.correct`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.correct_exp`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); + } + encounter.misc = { + correctMove: correctMove, + }; +} diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts new file mode 100644 index 000000000000..1861abcc7a48 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -0,0 +1,255 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { Species } from "#enums/species"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Gender } from "#app/data/gender"; +import { Type } from "#app/data/type"; +import { BattlerIndex } from "#app/battle"; +import { PokemonMove } from "#app/field/pokemon"; +import { Moves } from "#enums/moves"; +import { EncounterBattleAnim } from "#app/data/battle-anims"; +import { WeatherType } from "#app/data/weather"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { StatusEffect } from "#app/data/status-effect"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fieryFallout"; + +/** + * Damage percentage taken when suffering the heat. + * Can be a number between `0` - `100`. + * The higher the more damage taken (100% = instant KO). + */ +const DAMAGE_PERCENTAGE: number = 20; + +/** + * Fiery Fallout encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3814 | GitHub Issue #3814} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieryFalloutEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIERY_FALLOUT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(40, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withCatchAllowed(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withAnimations(EncounterAnim.MAGMA_BG, EncounterAnim.MAGMA_SPOUT) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mons + const volcaronaSpecies = getPokemonSpecies(Species.VOLCARONA); + const config: EnemyPartyConfig = { + pokemonConfigs: [ + { + species: volcaronaSpecies, + isBoss: false, + gender: Gender.MALE + }, + { + species: volcaronaSpecies, + isBoss: false, + gender: Gender.FEMALE + } + ], + doubleBattle: true, + disableSwitch: true + }; + encounter.enemyPartyConfigs = [config]; + + // Load hidden Volcarona sprites + encounter.spriteConfigs = [ + { + spriteKey: "", + fileRoot: "", + species: Species.VOLCARONA, + repeat: true, + hidden: true, + hasShadow: true, + x: -20, + startFrame: 20 + }, + { + spriteKey: "", + fileRoot: "", + species: Species.VOLCARONA, + repeat: true, + hidden: true, + hasShadow: true, + x: 20 + }, + ]; + + // Load animations/sfx for Volcarona moves + loadCustomMovesForEncounter(scene, [Moves.FIRE_SPIN, Moves.QUIVER_DANCE]); + + scene.arena.trySetWeather(WeatherType.SUNNY, true); + + encounter.setDialogueToken("volcaronaName", getPokemonSpecies(Species.VOLCARONA).getName()); + + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.MAGMA_BG, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 200, 70, 2, 3); + const animation = new EncounterBattleAnim(EncounterAnim.MAGMA_SPOUT, scene.getPlayerPokemon()!, scene.getPlayerPokemon()); + animation.playWithoutTargets(scene, 80, 100, 2); + scene.time.delayedCall(600, () => { + animation.playWithoutTargets(scene, -20, 100, 2); + }); + scene.time.delayedCall(1200, () => { + animation.playWithoutTargets(scene, 140, 150, 2); + }); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); + + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.FIRE_SPIN), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER_2], + move: new PokemonMove(Moves.FIRE_SPIN), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.QUIVER_DANCE), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.ENEMY_2], + move: new PokemonMove(Moves.QUIVER_DANCE), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Damage non-fire types and burn 1 random non-fire type member + const encounter = scene.currentBattle.mysteryEncounter!; + const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); + + for (const pkm of nonFireTypes) { + const percentage = DAMAGE_PERCENTAGE / 100; + const damage = Math.floor(pkm.getMaxHp() * percentage); + applyDamageToPokemon(scene, pkm, damage); + } + + // Burn random member + const burnable = nonFireTypes.filter(p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status!.effect) || p.status?.effect === StatusEffect.BURN); + if (burnable?.length > 0) { + const roll = randSeedInt(burnable.length); + const chosenPokemon = burnable[roll]; + if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { + // Burn applied + encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}.option.2.target_burned`); + } + } + + // No rewards + leaveEncounterWithoutBattle(scene, true); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically + .withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + transitionMysteryEncounterIntroVisuals(scene, false, false, 2000); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Fire types help calm the Volcarona + const encounter = scene.currentBattle.mysteryEncounter!; + transitionMysteryEncounterIntroVisuals(scene); + setEncounterRewards(scene, + { fillRemaining: true }, + undefined, + () => { + giveLeadPokemonCharcoal(scene); + }); + + const primary = encounter.options[2].primaryPokemon!; + const secondary = encounter.options[2].secondaryPokemon![0]; + + setEncounterExp(scene, [primary.id, secondary.id], getPokemonSpecies(Species.VOLCARONA).baseExp * 2); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); + +function giveLeadPokemonCharcoal(scene: BattleScene) { + // Give first party pokemon Charcoal for free at end of battle + const leadPokemon = scene.getParty()?.[0]; + if (leadPokemon) { + const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]) as AttackTypeBoosterModifierType; + applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); + scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); + queueEncounterMessage(scene, `${namespace}.found_charcoal`); + } +} diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts new file mode 100644 index 000000000000..c163a2fc194c --- /dev/null +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -0,0 +1,186 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { + getPartyLuckValue, + 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 { MoveRequirement } from "../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 PokemonData from "#app/system/pokemon-data"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { randSeedInt } from "#app/utils"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fightOrFlight"; + +/** + * Fight or Flight encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3795 | GitHub Issue #3795} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FightOrFlightEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + 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 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, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + 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)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate item + // Waves 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER + const tier = + scene.currentBattle.waveIndex > 160 + ? ModifierTier.MASTER + : scene.currentBattle.waveIndex > 120 + ? ModifierTier.ROGUE + : scene.currentBattle.waveIndex > 40 + ? ModifierTier.ULTRA + : ModifierTier.GREAT; + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + let item: ModifierTypeOption | null = null; + // TMs and Candy Jar excluded from possible rewards as they're too swingy in value for a singular item reward + while (!item || item.type.id.includes("TM_") || item.type.id === "CANDY_JAR") { + item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0]; + } + encounter.setDialogueToken("itemName", item.type.name); + encounter.misc = item; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); + encounter.spriteConfigs = [ + { + spriteKey: item.type.iconImage, + fileRoot: "items", + hasShadow: false, + x: 35, + y: -5, + scale: 0.75, + isItem: true, + disableAnimation: true + }, + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + isPokemon: true + }, + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + // Pokemon will randomly boost 1 stat by 2 stages + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick steal + const encounter = scene.currentBattle.mysteryEncounter!; + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + + // Use primaryPokemon to execute the thievery + const primaryPokemon = encounter.options[1].primaryPokemon!; + setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs![0].species.baseExp); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts new file mode 100644 index 000000000000..a544657e47ca --- /dev/null +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -0,0 +1,425 @@ +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 { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +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"; +import { Species } from "#enums/species"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { PlayerGender } from "#enums/player-gender"; +import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; +import { addPokeballOpenParticles } from "#app/field/anims"; +import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { Nature } from "#enums/nature"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:funAndGames"; + +/** + * Fun and Games! encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3819 | GitHub Issue #3819} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FunAndGamesEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FUN_AND_GAMES) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play + .withAutoHideIntroVisuals(false) + // Allows using move without a visible enemy pokemon + .withBattleAnimationsWithoutTargets(true) + // The Wobbuffet won't use moves + .withSkipEnemyBattleTurns(true) + // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu + .withSkipToFightInput(true) + .withIntroSpriteConfigs([ + { + spriteKey: "carnival_game", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 0, + y: 6, + }, + { + spriteKey: "carnival_wobbuffet", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -28, + y: 6, + yShadow: 6 + }, + { + spriteKey: "carnival_man", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 40, + y: 6, + yShadow: 6 + }, + ]) + .withIntroDialogue([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + scene.loadBgm("mystery_encounter_fun_and_games", "mystery_encounter_fun_and_games.mp3"); + encounter.setDialogueToken("wobbuffetName", getPokemonSpecies(Species.WOBBUFFET).getName()); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Change the bgm + scene.fadeOutBgm(2000, false); + scene.time.delayedCall(2000, () => { + scene.playBgm("mystery_encounter_fun_and_games"); + }); + + return true; + }) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Select Pokemon for minigame + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + // Only Pokemon that are not KOed/legal can be selected + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start minigame + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.misc.turnsRemaining = 3; + + // Update money + const moneyCost = (encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; + updatePlayerMoney(scene, -moneyCost, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:paid_money", { amount: moneyCost })); + + // Handlers for battle events + encounter.onTurnStart = handleNextTurn; // triggered during TurnInitPhase + encounter.doContinueEncounter = handleLoseMinigame; // triggered during MysteryEncounterRewardsPhase, post VictoryPhase if the player KOs Wobbuffet + + hideShowmanIntroSprite(scene); + await summonPlayerPokemon(scene); + await showWobbuffetHealthBar(scene); + + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + transitionMysteryEncounterIntroVisuals(scene, true, true); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function summonPlayerPokemon(scene: BattleScene) { + return new Promise(async resolve => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const playerPokemon = encounter.misc.playerPokemon; + // Swaps the chosen Pokemon and the first player's lead Pokemon in the party + const party = scene.getParty(); + const chosenIndex = party.indexOf(playerPokemon); + if (chosenIndex !== 0) { + [party[chosenIndex], party[0]] = [party[chosenIndex], party[chosenIndex]]; + } + + // Do trainer summon animation + let playerAnimationPromise: Promise | undefined; + scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(playerPokemon) })); + scene.pbTray.hide(); + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(562, () => { + scene.trainer.setFrame("2"); + scene.time.delayedCall(64, () => { + scene.trainer.setFrame("3"); + }); + }); + scene.tweens.add({ + targets: scene.trainer, + x: -36, + duration: 1000, + onComplete: () => scene.trainer.setVisible(false) + }); + scene.time.delayedCall(750, () => { + playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon); + }); + + // Also loads Wobbuffet data + const enemySpecies = getPokemonSpecies(Species.WOBBUFFET); + scene.currentBattle.enemyParty = []; + const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false); + wobbuffet.ivs = [0, 0, 0, 0, 0, 0]; + wobbuffet.setNature(Nature.MILD); + wobbuffet.setAlpha(0); + wobbuffet.setVisible(false); + wobbuffet.calculateStats(); + scene.currentBattle.enemyParty[0] = wobbuffet; + scene.gameData.setPokemonSeen(wobbuffet, true); + await wobbuffet.loadAssets(); + const id = setInterval(checkPlayerAnimationPromise, 500); + async function checkPlayerAnimationPromise() { + if (playerAnimationPromise) { + clearInterval(id); + await playerAnimationPromise; + resolve(); + } + } + }); +} + +function handleLoseMinigame(scene: BattleScene) { + return new Promise(async resolve => { + // Check Wobbuffet is still alive + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet || wobbuffet.isFainted(true) || wobbuffet.hp === 0) { + // Player loses + // End the battle + if (wobbuffet) { + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + } + transitionMysteryEncounterIntroVisuals(scene, true, true); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, true); + await showEncounterText(scene, `${namespace}.ko`); + const reviveCost = scene.getWaveMoneyAmount(1.5); + updatePlayerMoney(scene, -reviveCost, true, false); + } + + resolve(); + }); +} + +function handleNextTurn(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet) { + // Should never be triggered, just handling the edge case + handleLoseMinigame(scene); + return true; + } + if (encounter.misc.turnsRemaining <= 0) { + // Check Wobbuffet's health for the actual result + const healthRatio = wobbuffet.hp / wobbuffet.getMaxHp(); + let resultMessageKey: string; + let isHealPhase = false; + if (healthRatio < 0.03) { + // Grand prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.best_result`; + } else if (healthRatio < 0.15) { + // 2nd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.great_result`; + } else if (healthRatio < 0.33) { + // 3rd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.good_result`; + } else { + // No prize + isHealPhase = true; + resultMessageKey = `${namespace}.bad_result`; + } + + // End the battle + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, isHealPhase); + // Must end the TurnInit phase prematurely so battle phases aren't added to queue + queueEncounterMessage(scene, `${namespace}.end_game`); + queueEncounterMessage(scene, resultMessageKey); + + // Skip remainder of TurnInitPhase + return true; + } else { + if (encounter.misc.turnsRemaining < 3) { + // Display charging messages on turns that aren't the initial turn + queueEncounterMessage(scene, `${namespace}.charging_continue`); + } + queueEncounterMessage(scene, `${namespace}.turn_remaining_${encounter.misc.turnsRemaining}`); + encounter.misc.turnsRemaining--; + } + + // Don't skip remainder of TurnInitPhase + return false; +} + +async function showWobbuffetHealthBar(scene: BattleScene) { + const wobbuffet = scene.getEnemyPokemon()!; + + scene.add.existing(wobbuffet); + scene.field.add(wobbuffet); + + const playerPokemon = scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + scene.field.moveBelow(wobbuffet, playerPokemon); + } + // Show health bar and trigger cry + wobbuffet.showInfo(); + scene.time.delayedCall(1000, () => { + wobbuffet.cry(); + }); + wobbuffet.resetSummonData(); + + // Track the HP change across turns + scene.currentBattle.mysteryEncounter!.misc.wobbuffetHealth = wobbuffet.hp; +} + +function summonPlayerPokemonAnimation(scene: BattleScene, pokemon: PlayerPokemon): Promise { + return new Promise(resolve => { + const pokeball = scene.addFieldSprite(36, 80, "pb", getPokeballAtlasKey(pokemon.pokeball)); + pokeball.setVisible(false); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + pokemon.setFieldPosition(FieldPosition.CENTER, 0); + + const fpOffset = pokemon.getFieldPositionOffset(); + + pokeball.setVisible(true); + + scene.tweens.add({ + targets: pokeball, + duration: 650, + x: 100 + fpOffset[0] + }); + + scene.tweens.add({ + targets: pokeball, + duration: 150, + ease: "Cubic.easeOut", + y: 70 + fpOffset[1], + onComplete: () => { + scene.tweens.add({ + targets: pokeball, + duration: 500, + ease: "Cubic.easeIn", + angle: 1440, + y: 132 + fpOffset[1], + onComplete: () => { + scene.playSound("se/pb_rel"); + pokeball.destroy(); + scene.add.existing(pokemon); + scene.field.add(pokemon); + addPokeballOpenParticles(scene, pokemon.x, pokemon.y - 16, pokemon.pokeball); + scene.updateModifiers(true); + scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.5); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + scene.updateFieldScale(); + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + scene.time.delayedCall(1000, () => { + if (pokemon.isShiny()) { + scene.unshiftPhase(new ShinySparklePhase(scene, pokemon.getBattlerIndex())); + } + + pokemon.resetTurnData(); + + scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); + scene.pushPhase(new PostSummonPhase(scene, pokemon.getBattlerIndex())); + resolve(); + }); + } + }); + } + }); + } + }); + }); +} + +function hideShowmanIntroSprite(scene: BattleScene) { + const carnivalGame = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(0)[0]; + const wobbuffet = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1)[0]; + const showMan = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(2)[0]; + + // Hide the showman + scene.tweens.add({ + targets: showMan, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750 + }); + + // Slide the Wobbuffet and Game over slightly + scene.tweens.add({ + targets: [wobbuffet, carnivalGame], + x: "+=16", + ease: "Sine.easeInOut", + duration: 750 + }); +} diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts new file mode 100644 index 000000000000..1c5a1f009d94 --- /dev/null +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -0,0 +1,830 @@ +import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { TrainerSlot, } from "#app/data/trainer-config"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; +import { getTypeRgb } from "#app/data/type"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import PokemonData from "#app/system/pokemon-data"; +import i18next from "i18next"; +import { Gender, getGenderSymbol } from "#app/data/gender"; +import { getNatureName } from "#app/data/nature"; +import { getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:globalTradeSystem"; + +const LEGENDARY_TRADE_POOLS = { + 1: [Species.RATTATA, Species.PIDGEY, Species.WEEDLE], + 2: [Species.SENTRET, Species.HOOTHOOT, Species.LEDYBA], + 3: [Species.POOCHYENA, Species.ZIGZAGOON, Species.TAILLOW], + 4: [Species.BIDOOF, Species.STARLY, Species.KRICKETOT], + 5: [Species.PATRAT, Species.PURRLOIN, Species.PIDOVE], + 6: [Species.BUNNELBY, Species.LITLEO, Species.SCATTERBUG], + 7: [Species.PIKIPEK, Species.YUNGOOS, Species.ROCKRUFF], + 8: [Species.SKWOVET, Species.WOOLOO, Species.ROOKIDEE], + 9: [Species.LECHONK, Species.FIDOUGH, Species.TAROUNTULA] +}; + +/** Exclude Paradox mons as they aren't considered legendary/mythical */ +const EXCLUDED_TRADE_SPECIES = [ + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN +]; + +/** + * Global Trade System encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3812 | GitHub Issue #3812} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const GlobalTradeSystemEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.GLOBAL_TRADE_SYSTEM) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "global_trade_system", + fileRoot: "mystery-encounters", + hasShadow: true, + disableAnimation: true, + x: 3, + y: 5, + yShadow: 1 + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Load bgm + let bgmKey: string; + if (scene.musicPreference === 0) { + bgmKey = "mystery_encounter_gen_5_gts"; + scene.loadBgm(bgmKey, `${bgmKey}.mp3`); + } else { + // Mixed option + bgmKey = "mystery_encounter_gen_6_gts"; + scene.loadBgm(bgmKey, `${bgmKey}.mp3`); + } + + // Load possible trade options + // Maps current party member's id to 3 EnemyPokemon objects + // None of the trade options can be the same species + const tradeOptionsMap: Map = getPokemonTradeOptions(scene); + encounter.misc = { + tradeOptionsMap, + bgmKey + }; + + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Change the bgm + scene.fadeOutBgm(1500, false); + scene.time.delayedCall(1500, () => { + scene.playBgm(scene.currentBattle.mysteryEncounter!.misc.bgmKey); + }); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.option.1.trade_options_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get the trade species options for the selected pokemon + const tradeOptionsMap: Map = encounter.misc.tradeOptionsMap; + const tradeOptions = tradeOptionsMap.get(pokemon.id); + if (!tradeOptions) { + return []; + } + + return tradeOptions.map((tradePokemon: EnemyPokemon) => { + const option: OptionSelectItem = { + label: tradePokemon.getNameToRender(), + handler: () => { + // Pokemon trade selected + encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("received", tradePokemon.getNameToRender()); + encounter.misc.tradedPokemon = pokemon; + encounter.misc.receivedPokemon = tradePokemon; + return true; + }, + onHover: () => { + const formName = tradePokemon.species.forms?.[pokemon.formIndex]?.formName; + const line1 = i18next.t("pokemonInfoContainer:ability") + " " + tradePokemon.getAbility().name + (tradePokemon.getGender() !== Gender.GENDERLESS ? " | " + i18next.t("pokemonInfoContainer:gender") + " " + getGenderSymbol(tradePokemon.getGender()) : ""); + const line2 = i18next.t("pokemonInfoContainer:nature") + " " + getNatureName(tradePokemon.getNature()) + (formName ? " | " + i18next.t("pokemonInfoContainer:form") + " " + formName : ""); + showEncounterText(scene, `${line1}\n${line2}`, 0, 0, false); + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; + const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + const modifiers = tradedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + + // Remove the original party member from party + scene.removePokemonFromPlayerParty(tradedPokemon, false); + + // Set data properly, then generate the new Pokemon's assets + receivedPokemonData.passive = tradedPokemon.passive; + // Pokeball to Ultra ball, randomly + receivedPokemonData.pokeball = randInt(4) as PokeballType; + const dataSource = new PokemonData(receivedPokemonData); + const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); + scene.getParty().push(newPlayerPokemon); + await newPlayerPokemon.loadAssets(); + + for (const mod of modifiers) { + mod.pokemonId = newPlayerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + + // Show the trade animation + await showTradeBackground(scene); + await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon); + await showEncounterText(scene, `${namespace}.trade_received`, null, 0, true, 4000); + scene.playBgm(encounter.misc.bgmKey); + await hideTradeBackground(scene); + tradedPokemon.destroy(); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + 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 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) { + // 512/65536 -> 1/128 + tradePokemon.trySetShinySeed(512, true); + } + + // Extra HA roll at base 1/64 odds (boosted by events and charms) + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(64); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("received", tradePokemon.getNameToRender()); + encounter.misc.tradedPokemon = pokemon; + encounter.misc.receivedPokemon = tradePokemon; + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon; + const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon; + const modifiers = tradedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier)); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + + // Remove the original party member from party + scene.removePokemonFromPlayerParty(tradedPokemon, false); + + // Set data properly, then generate the new Pokemon's assets + receivedPokemonData.passive = tradedPokemon.passive; + receivedPokemonData.pokeball = randInt(4) as PokeballType; + const dataSource = new PokemonData(receivedPokemonData); + const newPlayerPokemon = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource); + scene.getParty().push(newPlayerPokemon); + await newPlayerPokemon.loadAssets(); + + for (const mod of modifiers) { + mod.pokemonId = newPlayerPokemon.id; + scene.addModifier(mod, true, false, false, true); + } + + // Show the trade animation + 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); + await hideTradeBackground(scene); + tradedPokemon.destroy(); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.trade_options_prompt`, + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return it.isTransferrable; + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc.chosenModifier = modifier; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that can gain benefits are above 1/3rd HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon has items to trade + const meetsReqs = pokemon.getHeldItems().filter((it) => { + return it.isTransferrable; + }).length > 0; + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.option.3.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const modifier = encounter.misc.chosenModifier; + + // Check tier of the traded item, the received item will be one tier up + const type = modifier.type.withTierFromPool(); + let tier = type.tier ?? ModifierTier.GREAT; + // Eggs and White Herb are not in the pool + if (type.id === "WHITE_HERB") { + tier = ModifierTier.GREAT; + } else if (type.id === "LUCKY_EGG") { + tier = ModifierTier.ULTRA; + } else if (type.id === "GOLDEN_EGG") { + tier = ModifierTier.ROGUE; + } + // Increment tier by 1 + if (tier < ModifierTier.MASTER) { + tier++; + } + + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + let item: ModifierTypeOption | null = null; + // TMs excluded from possible rewards + while (!item || item.type.id.includes("TM_")) { + item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0]; + } + + encounter.setDialogueToken("itemName", item.type.name); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); + + // Remove the chosen modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + scene.updateModifiers(true, true); + + // Generate a trainer name + const traderName = generateRandomTraderName(); + encounter.setDialogueToken("tradeTrainerName", traderName.trim()); + await showEncounterText(scene, `${namespace}.item_trade_selected`); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +function getPokemonTradeOptions(scene: BattleScene): Map { + const tradeOptionsMap: Map = new Map(); + // Starts by filtering out any current party members as valid resulting species + const alreadyUsedSpecies: PokemonSpecies[] = scene.getParty().map(p => p.species); + + scene.getParty().forEach(pokemon => { + // If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools + if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { + const generation = pokemon.species.generation; + const tradeOptions: EnemyPokemon[] = LEGENDARY_TRADE_POOLS[generation].map(s => { + const pokemonSpecies = getPokemonSpecies(s); + return new EnemyPokemon(scene, pokemonSpecies, 5, TrainerSlot.NONE, false); + }); + tradeOptionsMap.set(pokemon.id, tradeOptions); + } else { + const originalBst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0); + + const tradeOptions: PokemonSpecies[] = []; + for (let i = 0; i < 3; i++) { + const speciesTradeOption = generateTradeOption(alreadyUsedSpecies, originalBst); + alreadyUsedSpecies.push(speciesTradeOption); + tradeOptions.push(speciesTradeOption); + } + + // Add trade options to map + tradeOptionsMap.set(pokemon.id, tradeOptions.map(s => { + return new EnemyPokemon(scene, s, pokemon.level, TrainerSlot.NONE, false); + })); + } + }); + + return tradeOptionsMap; +} + +function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: number): PokemonSpecies { + let newSpecies: PokemonSpecies | undefined; + while (isNullOrUndefined(newSpecies)) { + let bstCap = 9999; + let bstMin = 0; + if (originalBst) { + bstCap = originalBst + 100; + bstMin = originalBst - 100; + } + + // Get all non-legendary species that fall within the Bst range requirements + let validSpecies = allSpecies + .filter(s => { + const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical; + const speciesBst = s.getBaseStatTotal(); + const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap; + return !isLegendaryOrMythical && bstInRange && !EXCLUDED_TRADE_SPECIES.includes(s.speciesId); + }); + + // There must be at least 20 species available before it will choose one + if (validSpecies?.length > 20) { + validSpecies = randSeedShuffle(validSpecies); + newSpecies = validSpecies.pop(); + while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies!)) { + newSpecies = validSpecies.pop(); + } + } else { + // Expands search range until at least 20 are in the pool + bstMin -= 10; + bstCap += 10; + } + } + + return newSpecies!; +} + +function showTradeBackground(scene: BattleScene) { + return new Promise(resolve => { + const tradeContainer = scene.add.container(0, -scene.game.canvas.height / 6); + tradeContainer.setName("Trade Background"); + + const flyByStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0); + flyByStaticBg.setName("Black Background"); + flyByStaticBg.setOrigin(0, 0); + flyByStaticBg.setVisible(false); + tradeContainer.add(flyByStaticBg); + + const tradeBaseBg = scene.add.image(0, 0, "default_bg"); + tradeBaseBg.setName("Trade Background Image"); + tradeBaseBg.setOrigin(0, 0); + tradeContainer.add(tradeBaseBg); + + scene.fieldUI.add(tradeContainer); + scene.fieldUI.bringToTop(tradeContainer); + tradeContainer.setVisible(true); + tradeContainer.alpha = 0; + + scene.tweens.add({ + targets: tradeContainer, + alpha: 1, + duration: 500, + ease: "Sine.easeInOut", + onComplete: () => { + resolve(); + } + }); + }); +} + +function hideTradeBackground(scene: BattleScene) { + return new Promise(resolve => { + const transformationContainer = scene.fieldUI.getByName("Trade Background"); + + scene.tweens.add({ + targets: transformationContainer, + alpha: 0, + duration: 1000, + ease: "Sine.easeInOut", + onComplete: () => { + scene.fieldUI.remove(transformationContainer, true); + resolve(); + } + }); + }); +} + +/** + * Initiates an "evolution-like" animation to transform a previousPokemon (presumably from the player's party) into a new one, not necessarily an evolution species. + * @param scene + * @param tradedPokemon + * @param receivedPokemon + */ +function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon, receivedPokemon: PlayerPokemon) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + + let tradedPokemonSprite: Phaser.GameObjects.Sprite; + let tradedPokemonTintSprite: Phaser.GameObjects.Sprite; + let receivedPokemonSprite: Phaser.GameObjects.Sprite; + let receivedPokemonTintSprite: Phaser.GameObjects.Sprite; + + const getPokemonSprite = () => { + const ret = scene.addPokemonSprite(tradedPokemon, tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pkmn__sub"); + ret.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + return ret; + }; + + tradeContainer.add((tradedPokemonSprite = getPokemonSprite())); + tradeContainer.add((tradedPokemonTintSprite = getPokemonSprite())); + tradeContainer.add((receivedPokemonSprite = getPokemonSprite())); + tradeContainer.add((receivedPokemonTintSprite = getPokemonSprite())); + + tradedPokemonSprite.setAlpha(0); + tradedPokemonTintSprite.setAlpha(0); + tradedPokemonTintSprite.setTintFill(getPokeballTintColor(tradedPokemon.pokeball)); + receivedPokemonSprite.setVisible(false); + receivedPokemonTintSprite.setVisible(false); + receivedPokemonTintSprite.setTintFill(getPokeballTintColor(receivedPokemon.pokeball)); + + [ tradedPokemonSprite, tradedPokemonTintSprite ].map(sprite => { + sprite.play(tradedPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", tradedPokemon.shiny); + sprite.setPipelineData("variant", tradedPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (tradedPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = tradedPokemon.getSprite().pipelineData[k]; + }); + }); + + [ receivedPokemonSprite, receivedPokemonTintSprite ].map(sprite => { + sprite.play(receivedPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", receivedPokemon.shiny); + sprite.setPipelineData("variant", receivedPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (receivedPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = receivedPokemon.getSprite().pipelineData[k]; + }); + }); + + // Traded pokemon pokeball + const tradedPbAtlasKey = getPokeballAtlasKey(tradedPokemon.pokeball); + const tradedPokeball: Phaser.GameObjects.Sprite = scene.add.sprite(tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pb", tradedPbAtlasKey); + tradedPokeball.setVisible(false); + tradeContainer.add(tradedPokeball); + + // Received pokemon pokeball + const receivedPbAtlasKey = getPokeballAtlasKey(receivedPokemon.pokeball); + const receivedPokeball: Phaser.GameObjects.Sprite = scene.add.sprite(tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pb", receivedPbAtlasKey); + receivedPokeball.setVisible(false); + tradeContainer.add(receivedPokeball); + + scene.tweens.add({ + targets: tradedPokemonSprite, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 500, + onComplete: async () => { + scene.fadeOutBgm(1000, false); + await showEncounterText(scene, `${namespace}.pokemon_trade_selected`); + tradedPokemon.cry(); + scene.playBgm("evolution"); + await showEncounterText(scene, `${namespace}.pokemon_trade_goodbye`); + + tradedPokeball.setAlpha(0); + tradedPokeball.setVisible(true); + scene.tweens.add({ + targets: tradedPokeball, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 250, + onComplete: () => { + tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`); + scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_open`)); + scene.playSound("se/pb_rel"); + tradedPokemonTintSprite.setVisible(true); + + // TODO: need to add particles to fieldUI instead of field + // addPokeballOpenParticles(scene, tradedPokemon.x, tradedPokemon.y, tradedPokemon.pokeball); + + scene.tweens.add({ + targets: [tradedPokemonTintSprite, tradedPokemonSprite], + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + onComplete: () => { + tradedPokemonSprite.setVisible(false); + tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`); + tradedPokemonTintSprite.setVisible(false); + scene.playSound("se/pb_catch"); + scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}`)); + + scene.tweens.add({ + targets: tradedPokeball, + y: "+=10", + duration: 200, + delay: 250, + ease: "Cubic.easeIn", + onComplete: () => { + scene.playSound("se/pb_bounce_1"); + + scene.tweens.add({ + targets: tradedPokeball, + y: "-=100", + duration: 200, + delay: 1000, + ease: "Cubic.easeInOut", + onStart: () => { + scene.playSound("se/pb_throw"); + }, + onComplete: async () => { + await doPokemonTradeFlyBySequence(scene, tradedPokemonSprite, receivedPokemonSprite); + await doTradeReceivedSequence(scene, receivedPokemon, receivedPokemonSprite, receivedPokemonTintSprite, receivedPokeball, receivedPbAtlasKey); + resolve(); + } + }); + } + }); + } + }); + } + }); + } + }); + }); +} + +function doPokemonTradeFlyBySequence(scene: BattleScene, tradedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonSprite: Phaser.GameObjects.Sprite) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + const flyByStaticBg = tradeContainer.getByName("Black Background") as Phaser.GameObjects.Rectangle; + flyByStaticBg.setVisible(true); + tradeContainer.bringToTop(tradedPokemonSprite); + tradeContainer.bringToTop(receivedPokemonSprite); + + tradedPokemonSprite.x = tradeBaseBg.displayWidth / 4; + tradedPokemonSprite.y = 200; + tradedPokemonSprite.scale = 1; + tradedPokemonSprite.setVisible(true); + receivedPokemonSprite.x = tradeBaseBg.displayWidth * 3 / 4; + receivedPokemonSprite.y = -200; + receivedPokemonSprite.scale = 1; + receivedPokemonSprite.setVisible(true); + + const FADE_DELAY = 300; + const ANIM_DELAY = 750; + const BASE_ANIM_DURATION = 1000; + + // Fade out trade background + scene.tweens.add({ + targets: tradeBaseBg, + alpha: 0, + ease: "Cubic.easeInOut", + duration: FADE_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: [receivedPokemonSprite, tradedPokemonSprite], + y: tradeBaseBg.displayWidth / 2 - 100, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 3, + onComplete: () => { + scene.tweens.add({ + targets: receivedPokemonSprite, + x: tradeBaseBg.displayWidth / 4, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION / 2, + delay: ANIM_DELAY + }); + scene.tweens.add({ + targets: tradedPokemonSprite, + x: tradeBaseBg.displayWidth * 3 / 4, + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION / 2, + delay: ANIM_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: receivedPokemonSprite, + y: "+=200", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 2, + delay: ANIM_DELAY, + }); + scene.tweens.add({ + targets: tradedPokemonSprite, + y: "-=200", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION * 2, + delay: ANIM_DELAY, + onComplete: () => { + scene.tweens.add({ + targets: tradeBaseBg, + alpha: 1, + ease: "Cubic.easeInOut", + duration: FADE_DELAY, + onComplete: () => { + resolve(); + } + }); + } + }); + } + }); + } + }); + } + }); + }); +} + +function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPokemon, receivedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonTintSprite: Phaser.GameObjects.Sprite, receivedPokeballSprite: Phaser.GameObjects.Sprite, receivedPbAtlasKey: string) { + return new Promise(resolve => { + const tradeContainer = scene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container; + const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image; + + receivedPokemonSprite.setVisible(false); + receivedPokemonSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokemonSprite.y = tradeBaseBg.displayHeight / 2; + receivedPokemonTintSprite.setVisible(false); + receivedPokemonTintSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokemonTintSprite.y = tradeBaseBg.displayHeight / 2; + + receivedPokeballSprite.setVisible(true); + receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2; + receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100; + + const BASE_ANIM_DURATION = 1000; + + // Pokeball falls to the screen + scene.playSound("se/pb_throw"); + scene.tweens.add({ + targets: receivedPokeballSprite, + y: "+=100", + ease: "Cubic.easeInOut", + duration: BASE_ANIM_DURATION, + onComplete: () => { + scene.playSound("se/pb_bounce_1"); + scene.time.delayedCall(100, () => scene.playSound("se/pb_bounce_1")); + + scene.time.delayedCall(2000, () => { + scene.playSound("se/pb_rel"); + scene.fadeOutBgm(500, false); + receivedPokemon.cry(); + receivedPokemonTintSprite.scale = 0.25; + receivedPokemonTintSprite.alpha = 1; + receivedPokemonSprite.setVisible(true); + receivedPokemonSprite.scale = 0.25; + receivedPokemonTintSprite.alpha = 1; + receivedPokemonTintSprite.setVisible(true); + receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_opening`); + scene.time.delayedCall(17, () => receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_open`)); + scene.tweens.add({ + targets: receivedPokemonSprite, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + scene.tweens.add({ + targets: receivedPokemonTintSprite, + duration: 250, + ease: "Sine.easeOut", + scale: 1, + alpha: 0, + onComplete: () => { + receivedPokeballSprite.destroy(); + scene.time.delayedCall(2000, () => resolve()); + } + }); + }); + } + }); + }); +} + +function generateRandomTraderName() { + const length = Object.keys(trainerNamePools).length; + // +1 avoids TrainerType.UNKNOWN + let trainerTypePool = trainerNamePools[randInt(length) + 1]; + while (!trainerTypePool) { + trainerTypePool = trainerNamePools[randInt(length) + 1]; + } + // Some trainers have 2 gendered pools, some do not + const genderedPool = trainerTypePool[randInt(trainerTypePool.length)]; + const trainerNameString = genderedPool instanceof Array ? genderedPool[randInt(genderedPool.length)] : genderedPool; + // Some names have an '&' symbol and need to be trimmed to a single name instead of a double name + const trainerNames = trainerNameString.split(" & "); + return trainerNames[randInt(trainerNames.length)]; +} diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts new file mode 100644 index 000000000000..16568d8cb7d8 --- /dev/null +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -0,0 +1,143 @@ +import { getPokemonSpecies } from "#app/data/pokemon-species"; +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 { 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"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; +import {PokemonMove} from "#app/field/pokemon"; + +const OPTION_1_REQUIRED_MOVE = Moves.SURF; +const OPTION_2_REQUIRED_MOVE = Moves.FLY; +/** + * Damage percentage taken when wandering aimlessly. + * Can be a number between `0` - `100`. + * The higher the more damage taken (100% = instant KO). + */ +const DAMAGE_PERCENTAGE: number = 25; +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:lostAtSea"; + +/** + * Lost at sea encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3793 | GitHub Issue #3793} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "buoy", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 20, + y: 3, + }, + ]) + .withIntroDialogue([{ text: `${namespace}.intro` }]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + encounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE)); + encounter.setDialogueToken("option1RequiredMove", new PokemonMove(OPTION_1_REQUIRED_MOVE).getName()); + encounter.setDialogueToken("option2RequiredMove", new PokemonMove(OPTION_2_REQUIRED_MOVE).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + // Option 1: Use a (non fainted) pokemon that can learn Surf to guide you back/ + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPokemonCanLearnMoveRequirement(OPTION_1_REQUIRED_MOVE) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + disabledButtonLabel: `${namespace}.option.1.label_disabled`, + buttonTooltip: `${namespace}.option.1.tooltip`, + disabledButtonTooltip: `${namespace}.option.1.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => handlePokemonGuidingYouPhase(scene)) + .build() + ) + .withOption( + //Option 2: Use a (non fainted) pokemon that can learn fly to guide you back. + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPokemonCanLearnMoveRequirement(OPTION_2_REQUIRED_MOVE) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + disabledButtonLabel: `${namespace}.option.2.label_disabled`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => handlePokemonGuidingYouPhase(scene)) + .build() + ) + .withSimpleOption( + // Option 3: Wander aimlessly + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const allowedPokemon = scene.getParty().filter((p) => p.isAllowedInBattle()); + + for (const pkm of allowedPokemon) { + const percentage = DAMAGE_PERCENTAGE / 100; + const damage = Math.floor(pkm.getMaxHp() * percentage); + applyDamageToPokemon(scene, pkm, damage); + } + + leaveEncounterWithoutBattle(scene); + + return true; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +/** + * Generic handler for using a guiding pokemon to guide you back. + * + * @param scene Battle scene + */ +async function handlePokemonGuidingYouPhase(scene: BattleScene) { + const laprasSpecies = getPokemonSpecies(Species.LAPRAS); + const { mysteryEncounter } = scene.currentBattle; + + if (mysteryEncounter?.selectedOption?.primaryPokemon?.id) { + setEncounterExp(scene, mysteryEncounter.selectedOption.primaryPokemon.id, laprasSpecies.baseExp, true); + } else { + console.warn("Lost at sea: No guide pokemon found but pokemon guides player. huh!?"); + } + + leaveEncounterWithoutBattle(scene); + return true; +} diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts new file mode 100644 index 000000000000..71a44bd68521 --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -0,0 +1,214 @@ +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + setEncounterRewards, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, + trainerPartyTemplates, +} from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:mysteriousChallengers"; + +/** + * Mysterious Challengers encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3801 | GitHub Issue #3801} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChallengersEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Calculates what trainers are available for battle in the encounter + + // Normal difficulty trainer is randomly pulled from biome + const normalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + const normalConfig = trainerConfigs[normalTrainerType].clone(); + let female = false; + if (normalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const normalSpriteKey = normalConfig.getSpriteKey(female, normalConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: normalConfig, + female: female, + }); + + // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config + // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 + const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); + const hardTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate( + Math.min(Math.ceil(scene.currentBattle.waveIndex / 20), 5), + PartyMemberStrength.AVERAGE, + false, + true + ) + ); + const hardConfig = trainerConfigs[hardTrainerType].clone(); + hardConfig.setPartyTemplates(hardTemplate); + female = false; + if (hardConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: hardConfig, + levelAdditiveMultiplier: 1, + female: female, + }); + + // Brutal trainer is pulled from pool of boss trainers (gym leaders) for the biome + // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons + const brutalTrainerType = scene.arena.randomTrainerType( + scene.currentBattle.waveIndex, + true + ); + const e4Template = trainerPartyTemplates.ELITE_FOUR; + const brutalConfig = trainerConfigs[brutalTrainerType].clone(); + brutalConfig.title = trainerConfigs[brutalTrainerType].title; + brutalConfig.setPartyTemplates(e4Template); + // @ts-ignore + brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func + female = false; + if (brutalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); + encounter.enemyPartyConfigs.push({ + trainerConfig: brutalConfig, + levelAdditiveMultiplier: 1.5, + female: female, + }); + + encounter.spriteConfigs = [ + { + spriteKey: normalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: hardSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: brutalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn standard trainer battle with memory mushroom reward + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 10); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn hard fight + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 100); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Spawn brutal fight + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; + + // To avoid player level snowballing from picking this option + encounter.expMultiplier = 0.9; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 1000); + return ret; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts new file mode 100644 index 000000000000..26e846ed8744 --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -0,0 +1,204 @@ +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { Moves } from "#enums/moves"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:mysteriousChest"; + +const RAND_LENGTH = 100; +const COMMON_REWARDS_WEIGHT = 20; // 20% +const ULTRA_REWARDS_WEIGHT = 50; // 30% +const ROGUE_REWARDS_WEIGHT = 60; // 10% +const MASTER_REWARDS_WEIGHT = 65; // 5% + +/** + * Mysterious Chest encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3796 | GitHub Issue #3796} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChestEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withIntroSpriteConfigs([ + { + spriteKey: "chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 8, + yShadow: 6, + alpha: 1, + disableAnimation: true, // Re-enabled after option select + }, + { + spriteKey: "chest_red", + fileRoot: "mystery-encounters", + hasShadow: false, + y: 8, + yShadow: 6, + alpha: 0, + disableAnimation: true, // Re-enabled after option select + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 0.5, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GIMMIGHOUL), + formIndex: 0, + isBoss: true, + moveSet: [Moves.NASTY_PLOT, Moves.SHADOW_BALL, Moves.POWER_GEM, Moves.THIEF] + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + encounter.setDialogueToken("gimmighoulName", getPokemonSpecies(Species.GIMMIGHOUL).getName()); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play animation + const encounter = scene.currentBattle.mysteryEncounter!; + const introVisuals = encounter.introVisuals!; + + // Determine roll first + const roll = randSeedInt(RAND_LENGTH); + encounter.misc = { + roll + }; + + if (roll >= MASTER_REWARDS_WEIGHT) { + // Chest is springing trap, change to red chest sprite + const blueChestSprites = introVisuals.getSpriteAtIndex(0); + const redChestSprites = introVisuals.getSpriteAtIndex(1); + redChestSprites[0].setAlpha(1); + blueChestSprites[0].setAlpha(0.001); + } + introVisuals.spriteConfigs[0].disableAnimation = false; + introVisuals.spriteConfigs[1].disableAnimation = false; + introVisuals.playAnim(); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Open the chest + const encounter = scene.currentBattle.mysteryEncounter!; + const roll = encounter.misc.roll; + if (roll < COMMON_REWARDS_WEIGHT) { + // Choose between 2 COMMON / 2 GREAT tier items (20%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.COMMON, + ModifierTier.COMMON, + ModifierTier.GREAT, + ModifierTier.GREAT, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.normal`); + leaveEncounterWithoutBattle(scene); + } else if (roll < ULTRA_REWARDS_WEIGHT) { + // Choose between 3 ULTRA tier items (30%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.good`); + leaveEncounterWithoutBattle(scene); + } else if (roll < ROGUE_REWARDS_WEIGHT) { + // Choose between 2 ROGUE tier items (10%) + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE] }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.great`); + leaveEncounterWithoutBattle(scene); + } else if (roll < MASTER_REWARDS_WEIGHT) { + // Choose 1 MASTER tier item (5%) + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER] }); + // Display result message then proceed to rewards + queueEncounterMessage(scene, `${namespace}.option.1.amazing`); + leaveEncounterWithoutBattle(scene); + } else { + // Your highest level unfainted Pokemon gets OHKO. Start battle against a Gimmighoul (35%) + const highestLevelPokemon = getHighestLevelPlayerPokemon( + scene, + true + ); + koPlayerPokemon(scene, highestLevelPokemon); + // Handle game over edge case + const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); + if (allowedPokemon.length === 0) { + // If there are no longer any legal pokemon in the party, game over. + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + // Show which Pokemon was KOed, then start battle against Gimmighoul + encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); + await showEncounterText(scene, `${namespace}.option.1.bad`); + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts new file mode 100644 index 000000000000..2abbb53c333b --- /dev/null +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -0,0 +1,340 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Stat } from "#enums/stat"; +import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { getEncounterText, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "i18next"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:partTimer"; + +/** + * Part Timer encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3813 | GitHub Issue #3813} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const PartTimerEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.PART_TIMER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "warehouse_crate", + fileRoot: "mystery-encounters", + hasShadow: false, + y: 6, + x: 15 + }, + { + spriteKey: "worker_f", + fileRoot: "trainer", + hasShadow: true, + x: -18, + y: 4 + } + ]) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withOnInit((scene: BattleScene) => { + // Load sfx + scene.loadSe("PRSFX- Horn Drill1", "battle_anims", "PRSFX- Horn Drill1.wav"); + scene.loadSe("PRSFX- Horn Drill3", "battle_anims", "PRSFX- Horn Drill3.wav"); + scene.loadSe("PRSFX- Guillotine2", "battle_anims", "PRSFX- Guillotine2.wav"); + scene.loadSe("PRSFX- Heavy Slam2", "battle_anims", "PRSFX- Heavy Slam2.wav"); + + scene.loadSe("PRSFX- Agility", "battle_anims", "PRSFX- Agility.wav"); + scene.loadSe("PRSFX- Extremespeed1", "battle_anims", "PRSFX- Extremespeed1.wav"); + scene.loadSe("PRSFX- Accelerock1", "battle_anims", "PRSFX- Accelerock1.wav"); + + scene.loadSe("PRSFX- Captivate", "battle_anims", "PRSFX- Captivate.wav"); + scene.loadSe("PRSFX- Attract2", "battle_anims", "PRSFX- Attract2.wav"); + scene.loadSe("PRSFX- Aurora Veil2", "battle_anims", "PRSFX- Aurora Veil2.wav"); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + + // Calculate the "baseline" stat value (90 base stat, 16 IVs, neutral nature, same level as pokemon) to compare + // Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4. + // Calculation from Pokemon.calculateStats + const baselineValue = Math.floor(((2 * 90 + 16) * pokemon.level) * 0.01) + 5; + const percentDiff = (pokemon.getStat(Stat.SPD) - baselineValue) / baselineValue; + const moneyMultiplier = Math.min(Math.max(2.5 * (1+ percentDiff), 1), 4); + + encounter.misc = { + moneyMultiplier + }; + + // Reduce all PP to 2 (if they started at greater than 2) + pokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, pokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doDeliverySfx(scene); + }; + + // Only Pokemon non-KOd pokemon can be selected + const selectableFilter = (pokemon: Pokemon) => { + if (!pokemon.isAllowedInBattle()) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick Deliveries + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; + + // Give money and do dialogue + if (moneyMultiplier > 2.5) { + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + } else { + await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`); + } + const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + + // Calculate the "baseline" stat value (75 base stat, 16 IVs, neutral nature, same level as pokemon) to compare + // Resulting money is 2.5 * (% difference from baseline), with minimum of 1 and maximum of 4. + // Calculation from Pokemon.calculateStats + const baselineHp = Math.floor(((2 * 75 + 16) * pokemon.level) * 0.01) + pokemon.level + 10; + const baselineAtkDef = Math.floor(((2 * 75 + 16) * pokemon.level) * 0.01) + 5; + const baselineValue = baselineHp + 1.5 * (baselineAtkDef * 2); + const strongestValue = pokemon.getStat(Stat.HP) + 1.5 * (pokemon.getStat(Stat.ATK) + pokemon.getStat(Stat.DEF)); + const percentDiff = (strongestValue - baselineValue) / baselineValue; + const moneyMultiplier = Math.min(Math.max(2.5 * (1 + percentDiff), 1), 4); + + encounter.misc = { + moneyMultiplier + }; + + // Reduce all PP to 2 (if they started at greater than 2) + pokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, pokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doStrongWorkSfx(scene); + }; + + // Only Pokemon non-KOd pokemon can be selected + const selectableFilter = (pokemon: Pokemon) => { + if (!pokemon.isAllowedInBattle()) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick Move Warehouse items + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; + + // Give money and do dialogue + if (moneyMultiplier > 2.5) { + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + } else { + await showEncounterDialogue(scene, `${namespace}.job_complete_bad`, `${namespace}.speaker`); + } + const moneyChange = scene.getWaveMoneyAmount(moneyMultiplier); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option3PrimaryName and option3PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const selectedPokemon = encounter.selectedOption?.primaryPokemon!; + encounter.setDialogueToken("selectedPokemon", selectedPokemon.getNameToRender()); + + // Reduce all PP to 2 (if they started at greater than 2) + selectedPokemon.moveset.forEach(move => { + if (move) { + const newPpUsed = move.getMovePp() - 2; + move.ppUsed = move.ppUsed < newPpUsed ? newPpUsed : move.ppUsed; + } + }); + + setEncounterExp(scene, selectedPokemon.id, 100); + + // Hide intro visuals + transitionMysteryEncounterIntroVisuals(scene, true, false); + // Play sfx for "working" + doSalesSfx(scene); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Assist with Sales + // Bring visuals back in + await transitionMysteryEncounterIntroVisuals(scene, false, false); + + // Give money and do dialogue + await showEncounterDialogue(scene, `${namespace}.job_complete_good`, `${namespace}.speaker`); + const moneyChange = scene.getWaveMoneyAmount(2.5); + updatePlayerMoney(scene, moneyChange, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:receive_money", { amount: moneyChange })); + await showEncounterText(scene, `${namespace}.pokemon_tired`); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOutroDialogue([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.outro`, + } + ]) + .build(); + +function doStrongWorkSfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Horn Drill1"); + scene.playSound("battle_anims/PRSFX- Horn Drill1"); + + scene.time.delayedCall(1000, () => { + scene.playSound("battle_anims/PRSFX- Guillotine2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Heavy Slam2"); + }); + + scene.time.delayedCall(2500, () => { + scene.playSound("battle_anims/PRSFX- Guillotine2"); + }); +} + +function doDeliverySfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Accelerock1"); + + scene.time.delayedCall(1500, () => { + scene.playSound("battle_anims/PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2250, () => { + scene.playSound("battle_anims/PRSFX- Agility"); + }); +} + +function doSalesSfx(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Captivate"); + + scene.time.delayedCall(1500, () => { + scene.playSound("battle_anims/PRSFX- Attract2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("battle_anims/PRSFX- Aurora Veil2"); + }); + + scene.time.delayedCall(3000, () => { + scene.playSound("battle_anims/PRSFX- Attract2"); + }); +} diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts new file mode 100644 index 000000000000..49abf98cf49e --- /dev/null +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -0,0 +1,518 @@ +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 MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { PokeballType } from "#app/data/pokeball"; +import { PlayerGender } from "#enums/player-gender"; +import { IntegerHolder, randSeedInt } from "#app/utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterText, 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 { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:safariZone"; + +const TRAINER_THROW_ANIMATION_TIMES = [512, 184, 768]; + +const SAFARI_MONEY_MULTIPLIER = 2.75; + +/** + * Safari Zone encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3800 | GitHub Issue #3800} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SafariZoneEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(0, SAFARI_MONEY_MULTIPLIER)) // Cost equal to 1 Max Revive + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "safari_zone", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 4, + y: 6 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, SAFARI_MONEY_MULTIPLIER)) // Cost equal to 1 Max Revive + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start safari encounter + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.continuousEncounter = true; + encounter.misc = { + safariPokemonRemaining: 3 + }; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + // Load bait/mud assets + scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims", "PRSFX- Sludge Bomb2.wav"); + scene.loadSe("PRSFX- Taunt2", "battle_anims", "PRSFX- Taunt2.wav"); + scene.loadAtlas("bait", "mystery-encounters"); + scene.loadAtlas("mud", "mystery-encounters"); + // Clear enemy party + scene.currentBattle.enemyParty = []; + await transitionMysteryEncounterIntroVisuals(scene); + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, hideDescription: true }); + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +/** + * SAFARI ZONE MINIGAME OPTIONS + * + * Catch and flee rate stages are calculated in the same way stat changes are (they range from -6/+6) + * https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone + * + * Catch Rate calculation: + * catchRate = speciesCatchRate [1 to 255] * catchStageMultiplier [2/8 to 8/2] * ballCatchRate [1.5] + * + * Flee calculation: + * The harder a species is to catch, the higher its flee rate is + * (Caps at 50% base chance to flee for the hardest to catch Pokemon, before factoring in flee stage) + * fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2] + * Flee chance = fleeRate / 255 + */ +const safariZoneGameOptions: MysteryEncounterOption[] = [ + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.1.label`, + buttonTooltip: `${namespace}.safari.1.tooltip`, + selected: [ + { + text: `${namespace}.safari.1.selected`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw a ball option + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + const catchResult = await throwPokeball(scene, pokemon); + + if (catchResult) { + // You caught pokemon + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 0, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + } else { + // Pokemon catch failed, end turn + await doEndTurn(scene, 0); + } + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.2.label`, + buttonTooltip: `${namespace}.safari.2.tooltip`, + selected: [ + { + text: `${namespace}.safari.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw bait option + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; + await throwBait(scene, pokemon); + + // 100% chance to increase catch stage +2 + tryChangeCatchStage(scene, 2); + // 80% chance to increase flee stage +1 + const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); + if (!fleeChangeResult) { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.busy_eating`) ?? "", null, 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.eating`) ?? "", null, 1000, false); + } + + await doEndTurn(scene, 1); + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.3.label`, + buttonTooltip: `${namespace}.safari.3.tooltip`, + selected: [ + { + text: `${namespace}.safari.3.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw mud option + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; + await throwMud(scene, pokemon); + // 100% chance to decrease flee stage -2 + tryChangeFleeStage(scene, -2); + // 80% chance to decrease catch stage -1 + const catchChangeResult = tryChangeCatchStage(scene, -1, 8); + if (!catchChangeResult) { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.beside_itself_angry`) ?? "", null, 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.angry`) ?? "", null, 1000, false ); + } + + await doEndTurn(scene, 2); + return true; + }) + .build(), + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.safari.4.label`, + buttonTooltip: `${namespace}.safari.4.tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Flee option + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + await doPlayerFlee(scene, pokemon); + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 3, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + return true; + }) + .build() +]; + +async function summonSafariPokemon(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + // Message pokemon remaining + encounter.setDialogueToken("remainingCount", encounter.misc.safariPokemonRemaining); + scene.queueMessage(getEncounterText(scene, `${namespace}.safari.remaining_count`) ?? "", null, true); + + // Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken + // Safari pokemon roll twice on shiny and HA chances, but are otherwise normal + let enemySpecies; + let pokemon; + scene.executeWithSeedOffset(() => { + enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); + const level = scene.currentBattle.getLevelForWave(); + enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode)); + pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false); + + // Roll shiny twice + if (!pokemon.shiny) { + pokemon.trySetShinySeed(); + } + + // Roll HA twice + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + pokemon.calculateStats(); + + scene.currentBattle.enemyParty.unshift(pokemon); + }, scene.currentBattle.waveIndex * 1000 + encounter.misc.safariPokemonRemaining); + + scene.gameData.setPokemonSeen(pokemon, true); + await pokemon.loadAssets(); + + // Reset safari catch and flee rates + encounter.misc.catchStage = 0; + encounter.misc.fleeStage = 0; + encounter.misc.pokemon = pokemon; + encounter.misc.safariPokemonRemaining -= 1; + + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + + encounter.setDialogueToken("pokemonName", getPokemonNameWithAffix(pokemon)); + showEncounterText(scene, getEncounterText(scene, "battle:singleWildAppeared") ?? "", null, 1500, false) + .then(() => { + const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); + } + }); +} + +function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const baseCatchRate = pokemon.species.catchRate; + // Catch stage ranges from -6 to +6 (like stat boost stages) + const safariCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage; + // Catch modifier ranges from 2/8 (-6 stage) to 8/2 (+6) + const safariModifier = (2 + Math.min(Math.max(safariCatchStage, 0), 6)) / (2 - Math.max(Math.min(safariCatchStage, 0), -6)); + // Catch rate same as safari ball + const pokeballMultiplier = 1.5; + const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier); + const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate))); + return trainerThrowPokeball(scene, pokemon, PokeballType.POKEBALL, ballTwitchRate); +} + +async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "bait", "0001.png"); + bait.setOrigin(0.5, 0.625); + scene.field.add(bait); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: bait, + x: { value: 210 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + + let index = 1; + scene.time.delayedCall(768, () => { + scene.tweens.add({ + targets: pokemon, + duration: 150, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 5, + loop: 6, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + bait.setFrame("0002.png"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("battle_anims/PRSFX- Bug Bite"); + } + if (index === 4) { + bait.setFrame("0003.png"); + } + index++; + }, + onComplete: () => { + scene.time.delayedCall(256, () => { + bait.destroy(); + resolve(true); + }); + } + }); + }); + } + }); + }); + }); +} + +async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 35, "mud", "0001.png"); + mud.setOrigin(0.5, 0.625); + scene.field.add(mud); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Mud throw and splat + scene.tweens.add({ + targets: mud, + x: { value: 230 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Mud frame 2 + scene.playSound("battle_anims/PRSFX- Sludge Bomb2"); + mud.setFrame("0002.png"); + // Mud splat + scene.time.delayedCall(200, () => { + mud.setFrame("0003.png"); + scene.time.delayedCall(400, () => { + mud.setFrame("0004.png"); + }); + }); + + // Fade mud then angry animation + scene.tweens.add({ + targets: mud, + alpha: 0, + ease: "Cubic.easeIn", + duration: 1000, + onComplete: () => { + mud.destroy(); + scene.tweens.add({ + targets: pokemon, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 20, + loop: 1, + onStart: () => { + scene.playSound("battle_anims/PRSFX- Taunt2"); + }, + onLoop: () => { + scene.playSound("battle_anims/PRSFX- Taunt2"); + }, + onComplete: () => { + resolve(true); + } + }); + } + }); + } + }); + }); + }); +} + +function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { + const speciesCatchRate = pokemon.species.catchRate; + const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6)); + const fleeRate = (255 * 255 - speciesCatchRate * speciesCatchRate) / 255 / 2 * fleeModifier; + console.log("Flee rate: " + fleeRate); + const roll = randSeedInt(256); + console.log("Roll: " + roll); + return roll < fleeRate; +} + +function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + return false; + } + const currentFleeStage = scene.currentBattle.mysteryEncounter!.misc.fleeStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); + return true; +} + +function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + return false; + } + const currentCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); + return true; +} + +async function doEndTurn(scene: BattleScene, cursorIndex: number) { + // First cleanup and destroy old Pokemon objects that were left in the enemyParty + // They are left in enemyParty temporarily so that VictoryPhase properly handles EXP + const party = scene.getEnemyParty(); + if (party.length > 1) { + for (let i = 1; i < party.length; i++) { + party[i].destroy(); + } + scene.currentBattle.enemyParty = party.slice(0, 1); + } + + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + const isFlee = isPokemonFlee(pokemon, encounter.misc.fleeStage); + if (isFlee) { + // Pokemon flees! + await doPokemonFlee(scene, pokemon); + // Check how many safari pokemon left + if (encounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } else { + // End safari mode + encounter.continuousEncounter = false; + leaveEncounterWithoutBattle(scene, true); + } + } else { + scene.queueMessage(getEncounterText(scene, `${namespace}.safari.watching`) ?? "", 0, null, 1000); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } +} diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts new file mode 100644 index 000000000000..8ee4782def55 --- /dev/null +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -0,0 +1,227 @@ +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { modifierTypes } from "#app/modifier/modifier-type"; +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 { 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"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Nature } from "#enums/nature"; +import { getNatureName } from "#app/data/nature"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:shadyVitaminDealer"; + +/** + * Shady Vitamin Dealer encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3798 | GitHub Issue #3798} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +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 + .withPrimaryPokemonHealthRatioRequirement([0.5, 1]) // At least 1 Pokemon must have above half HP + .withIntroSpriteConfigs([ + { + spriteKey: Species.KROOKODILE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 12, + y: -5, + yShadow: -5 + }, + { + spriteKey: "b2w2_veteran_m", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -12, + y: 3, + yShadow: 3 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 1.5) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Update money + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + // Calculate modifiers and dialogue tokens + const modifiers = [ + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + ]; + encounter.setDialogueToken("boost1", modifiers[0].name); + encounter.setDialogueToken("boost2", modifiers[1].name); + encounter.misc = { + chosenPokemon: pokemon, + modifiers: modifiers, + }; + }; + + // Only Pokemon that can gain benefits are above half HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Choose Cheap Option + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; + + for (const modType of modifiers) { + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); + } + + leaveEncounterWithoutBattle(scene); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Damage and status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon as PlayerPokemon; + + // Pokemon takes half max HP damage and nature is randomized (does not update dex) + applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 2)); + + const currentNature = chosenPokemon.nature; + let newNature = randSeedInt(25) as Nature; + while (newNature === currentNature) { + newNature = randSeedInt(25) as Nature; + } + + chosenPokemon.nature = newNature; + encounter.setDialogueToken("newNature", getNatureName(newNature)); + queueEncounterMessage(scene, `${namespace}.cheap_side_effects`); + setEncounterExp(scene, [chosenPokemon.id], 100); + chosenPokemon.updateInfo(); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 3.5) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Update money + updatePlayerMoney(scene, -(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); + // Calculate modifiers and dialogue tokens + const modifiers = [ + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER)!, + ]; + encounter.setDialogueToken("boost1", modifiers[0].name); + encounter.setDialogueToken("boost2", modifiers[1].name); + encounter.misc = { + chosenPokemon: pokemon, + modifiers: modifiers, + }; + }; + + // Only Pokemon that can gain benefits are unfainted + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon is unfainted it can be selected + const meetsReqs = !pokemon.isFainted(true); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Choose Expensive Option + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; + + for (const modType of modifiers) { + await applyModifierTypeToPlayerPokemon(scene, chosenPokemon, modType); + } + + leaveEncounterWithoutBattle(scene); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter!; + const chosenPokemon = encounter.misc.chosenPokemon; + + queueEncounterMessage(scene, `${namespace}.no_bad_effects`); + setEncounterExp(scene, [chosenPokemon.id], 100); + + chosenPokemon.updateInfo(); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + speaker: `${namespace}.speaker` + } + ] + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts new file mode 100644 index 000000000000..b9f08b12ffd9 --- /dev/null +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -0,0 +1,151 @@ +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +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 { 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 { 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 { 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"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:slumberingSnorlax"; + +/** + * Sleeping Snorlax encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3815 | GitHub Issue #3815} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SlumberingSnorlaxEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SLUMBERING_SNORLAX) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: Species.SNORLAX.toString(), + fileRoot: "pokemon", + hasShadow: true, + tint: 0.25, + scale: 1.5, + repeat: true, + y: 5, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + console.log(encounter); + + // Calculate boss mon + const bossSpecies = getPokemonSpecies(Species.SNORLAX); + const pokemonConfig: EnemyPokemonConfig = { + 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] + }; + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 0.5, + pokemonConfigs: [pokemonConfig], + }; + encounter.enemyPartyConfigs = [config]; + + // Load animations/sfx for Snorlax fight start moves + loadCustomMovesForEncounter(scene, [Moves.SNORE]); + + encounter.setDialogueToken("snorlaxName", getPokemonSpecies(Species.SNORLAX).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true}); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.SNORE), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.SNORE), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Fall asleep waiting for Snorlax + // Full heal party + scene.unshiftPhase(new PartyHealPhase(scene, true)); + queueEncounterMessage(scene, `${namespace}.option.2.rest_result`); + leaveEncounterWithoutBattle(scene); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Steal the Snorlax's Leftovers + const instance = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false }); + // Snorlax exp to Pokemon that did the stealing + setEncounterExp(scene, instance.primaryPokemon!.id, getPokemonSpecies(Species.SNORLAX).baseExp); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts new file mode 100644 index 000000000000..bf976458fddc --- /dev/null +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -0,0 +1,237 @@ +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 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"; +import PokemonData from "#app/system/pokemon-data"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Biome } from "#enums/biome"; +import { getBiomeKey } from "#app/field/arena"; +import { Type } from "#app/data/type"; +import { getPartyLuckValue, modifierTypes } from "#app/modifier/modifier-type"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { BattlerTagType } from "#enums/battler-tag-type"; +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"; + +/** 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 MACHINE_INTERFACING_TYPES = [Type.ELECTRIC, Type.STEEL]; + +/** + * Teleporting Hijinks encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3817 | GitHub Issue #3817} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TeleportingHijinksEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TELEPORTING_HIJINKS) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new WaveModulusRequirement([1, 2, 3], 10)) // Must be in first 3 waves after boss wave + .withSceneRequirement(new MoneyRequirement(undefined, MONEY_COST_MULTIPLIER)) // Must be able to pay teleport cost + .withAutoHideIntroVisuals(false) + .withCatchAllowed(true) + .withIntroSpriteConfigs([ + { + spriteKey: "teleporter", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 4, + yShadow: 1 + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const price = scene.getWaveMoneyAmount(MONEY_COST_MULTIPLIER); + encounter.setDialogueToken("price", price.toString()); + encounter.misc = { + price + }; + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(undefined, MONEY_COST_MULTIPLIER) // Must be able to pay teleport cost + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + } + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Update money + updatePlayerMoney(scene, -scene.currentBattle.mysteryEncounter!.misc.price, true, false); + }) + .withOptionPhase(async (scene: BattleScene) => { + const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); + setEncounterRewards(scene, { fillRemaining: true }); + await initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPokemonTypeRequirement(MACHINE_INTERFACING_TYPES, true, 1) // Must have Steel or Electric type + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); + setEncounterRewards(scene, { fillRemaining: true }); + setEncounterExp(scene, scene.currentBattle.mysteryEncounter!.selectedOption!.primaryPokemon!.id, 100); + await initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Inspect the Machine + 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 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 = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + }], + }; + + const magnet = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.STEEL])!; + const metalCoat = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.ELECTRIC])!; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [magnet, metalCoat], fillRemaining: true }); + transitionMysteryEncounterIntroVisuals(scene, true, true); + await initBattleWithEnemyConfig(scene, config); + } + ) + .build(); + +async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate new biome (cannot be current biome) + const filteredBiomes = BIOME_CANDIDATES.filter(b => scene.arena.biomeType !== b); + const newBiome = filteredBiomes[randSeedInt(filteredBiomes.length)]; + + // Show dialogue and transition biome + await showEncounterText(scene, `${namespace}.transport`); + await Promise.all([animateBiomeChange(scene, newBiome), transitionMysteryEncounterIntroVisuals(scene)]); + scene.playBgm(); + 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 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 = { + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.boss_enraged`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + } + }], + }; + + return config; +} + +async function animateBiomeChange(scene: BattleScene, nextBiome: Biome) { + return new Promise(resolve => { + scene.tweens.add({ + targets: [scene.arenaEnemy, scene.lastEnemyTrainer], + x: "+=300", + duration: 2000, + onComplete: () => { + scene.newArena(nextBiome); + + const biomeKey = getBiomeKey(nextBiome); + const bgTexture = `${biomeKey}_bg`; + scene.arenaBgTransition.setTexture(bgTexture); + scene.arenaBgTransition.setAlpha(0); + scene.arenaBgTransition.setVisible(true); + scene.arenaPlayerTransition.setBiome(nextBiome); + scene.arenaPlayerTransition.setAlpha(0); + scene.arenaPlayerTransition.setVisible(true); + + scene.tweens.add({ + targets: [scene.arenaPlayer, scene.arenaBgTransition, scene.arenaPlayerTransition], + duration: 1000, + ease: "Sine.easeInOut", + alpha: (target: any) => target === scene.arenaPlayer ? 0 : 1, + onComplete: () => { + scene.arenaBg.setTexture(bgTexture); + scene.arenaPlayer.setBiome(nextBiome); + scene.arenaPlayer.setAlpha(1); + scene.arenaEnemy.setBiome(nextBiome); + scene.arenaEnemy.setAlpha(1); + scene.arenaNextEnemy.setBiome(nextBiome); + scene.arenaBgTransition.setVisible(false); + scene.arenaPlayerTransition.setVisible(false); + if (scene.lastEnemyTrainer) { + scene.lastEnemyTrainer.destroy(); + } + + resolve(); + + scene.tweens.add({ + targets: scene.arenaEnemy, + x: "-=300", + }); + } + }); + } + }); + }); +} diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts new file mode 100644 index 000000000000..16b0c421bd43 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -0,0 +1,159 @@ +import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { 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"; +import { PokeballType } from "#app/data/pokeball"; +import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +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"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:pokemonSalesman"; + +const MAX_POKEMON_PRICE_MULTIPLIER = 6; + +/** + * Pokemon Salesman encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3799 | GitHub Issue #3799} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ThePokemonSalesmanEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_POKEMON_SALESMAN) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withSceneRequirement(new MoneyRequirement(undefined, MAX_POKEMON_PRICE_MULTIPLIER)) // Some costs may not be as significant, this is the max you'd pay + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "pokemon_salesman", + fileRoot: "mystery-encounters", + hasShadow: true + } + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + let species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); + const tries = 0; + + // Reroll any species that don't have HAs + while (isNullOrUndefined(species.abilityHidden) && tries < 5) { + species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); + } + + let pokemon: PlayerPokemon; + if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) { + // If no HA mon found or you roll 1%, give shiny Magikarp + species = getPokemonSpecies(Species.MAGIKARP); + const hiddenIndex = species.ability2 ? 2 : 1; + pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex, undefined, true); + } else { + const hiddenIndex = species.ability2 ? 2 : 1; + pokemon = new PlayerPokemon(scene, species, 5, hiddenIndex, species.formIndex); + } + pokemon.generateAndPopulateMoveset(); + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon); + encounter.spriteConfigs.push({ + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + repeat: true, + isPokemon: true + }); + + const starterTier = speciesStarters[species.speciesId]; + // Prices decrease by starter tier less than 5, but only reduces cost by half at max + let priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER * (Math.max(starterTier, 2.5) / 5); + if (pokemon.shiny) { + // Always max price for shiny (flip HA back to normal), and add special messaging + priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER; + pokemon.abilityIndex = 0; + encounter.dialogue.encounterOptionsDialogue!.description = `${namespace}.description_shiny`; + encounter.options[0].dialogue!.buttonTooltip = `${namespace}.option.1.tooltip_shiny`; + } + const price = scene.getWaveMoneyAmount(priceMultiplier); + encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender()); + encounter.setDialogueToken("price", price.toString()); + encounter.misc = { + price: price, + pokemon: pokemon + }; + + pokemon.calculateStats(); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withHasDexProgress(true) + .withSceneMoneyRequirement(undefined, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2 + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected_message`, + } + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const price = encounter.misc.price; + const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon; + + // Update money + updatePlayerMoney(scene, -price, true, false); + + // Show dialogue + await showEncounterDialogue(scene, `${namespace}.option.1.selected_dialogue`, `${namespace}.speaker`); + await transitionMysteryEncounterIntroVisuals(scene); + + // "Catch" purchased pokemon + const data = new PokemonData(purchasedPokemon); + data.player = false; + await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true, true); + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts new file mode 100644 index 000000000000..047aa0d83f68 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -0,0 +1,199 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, loadCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Species } from "#enums/species"; +import { Nature } from "#app/data/nature"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { Stat } from "#enums/stat"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theStrongStuff"; + +// Halved for HP stat +const HIGH_BST_REDUCTION_VALUE = 15; +const BST_INCREASE_VALUE = 10; + +/** + * The Strong Stuff encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3803 | GitHub Issue #3803} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheStrongStuffEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(3, 6) // Must have at least 3 pokemon in party + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "berry_juice", + fileRoot: "items", + hasShadow: true, + isItem: true, + scale: 1.25, + x: -15, + y: 3, + disableAnimation: true + }, + { + spriteKey: Species.SHUCKLE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + scale: 1.25, + x: 20, + y: 10, + yShadow: 7 + }, + ]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2 + } + ], + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.2.stat_boost`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.DEF, Stat.SPDEF], 2)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + loadCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]); + + encounter.setDialogueToken("shuckleName", getPokemonSpecies(Species.SHUCKLE).getName()); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + // Do blackout and hide intro visuals during blackout + scene.time.delayedCall(750, () => { + transitionMysteryEncounterIntroVisuals(scene, true, true, 50); + }); + + // -15 to all base stats of highest BST (halved for HP), +10 to all base stats of rest of party (halved for HP) + // Sort party by bst + const sortedParty = scene.getParty().slice(0) + .sort((pokemon1, pokemon2) => { + const pokemon1Bst = pokemon1.calculateBaseStats().reduce((a, b) => a + b, 0); + const pokemon2Bst = pokemon2.calculateBaseStats().reduce((a, b) => a + b, 0); + return pokemon2Bst - pokemon1Bst; + }); + + sortedParty.forEach((pokemon, index) => { + if (index < 2) { + // -15 to the two highest BST mons + modifyPlayerPokemonBST(pokemon, -HIGH_BST_REDUCTION_VALUE); + encounter.setDialogueToken("highBstPokemon" + (index + 1), pokemon.getNameToRender()); + } else { + // +10 for the rest + modifyPlayerPokemonBST(pokemon, BST_INCREASE_VALUE); + } + }); + + encounter.setDialogueToken("reductionValue", HIGH_BST_REDUCTION_VALUE.toString()); + encounter.setDialogueToken("increaseValue", BST_INCREASE_VALUE.toString()); + await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, undefined, true); + + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true }); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.GASTRO_ACID), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.STEALTH_ROCK), + ignorePp: true + }); + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts new file mode 100644 index 000000000000..902aefcb4905 --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -0,0 +1,510 @@ +import { EnemyPartyConfig, generateModifierType, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { TrainerType } from "#enums/trainer-type"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { Nature } from "#enums/nature"; +import { Type } from "#app/data/type"; +import { BerryType } from "#enums/berry-type"; +import { Stat } from "#enums/stat"; +import { SpeciesFormChangeManualTrigger } from "#app/data/pokemon-forms"; +import { applyPostBattleInitAbAttrs, PostBattleInitAbAttr } from "#app/data/ability"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { ShowTrainerPhase } from "#app/phases/show-trainer-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import i18next from "i18next"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theWinstrateChallenge"; + +/** + * The Winstrate Challenge encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3821 | GitHub Issue #3821} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheWinstrateChallengeEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_WINSTRATE_CHALLENGE) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withSceneWaveRangeRequirement(100, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withIntroSpriteConfigs([ + { + spriteKey: "vito", + fileRoot: "trainer", + hasShadow: false, + x: 16, + y: -4 + }, + { + spriteKey: "vivi", + fileRoot: "trainer", + hasShadow: false, + x: -14, + y: -4 + }, + { + spriteKey: "victor", + fileRoot: "trainer", + hasShadow: true, + x: -32 + }, + { + spriteKey: "victoria", + fileRoot: "trainer", + hasShadow: true, + x: 40, + }, + { + spriteKey: "vicky", + fileRoot: "trainer", + hasShadow: true, + x: 3, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withAutoHideIntroVisuals(false) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Loaded back to front for pop() operations + encounter.enemyPartyConfigs.push(getVitoTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVickyTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getViviTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVictoriaTrainerConfig(scene)); + encounter.enemyPartyConfigs.push(getVictorTrainerConfig(scene)); + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Spawn 5 trainer battles back to back with Macho Brace in rewards + scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => { + return endTrainerBattleAndShowDialogue(scene); + }; + await transitionMysteryEncounterIntroVisuals(scene, true, false); + await spawnNextTrainerOrEndEncounter(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Refuse the challenge, they full heal the party and give the player a Rarer Candy + scene.unshiftPhase(new PartyHealPhase(scene, true)); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.RARER_CANDY], fillRemaining: false }); + leaveEncounterWithoutBattle(scene); + } + ) + .build(); + +async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + const nextConfig = encounter.enemyPartyConfigs.pop(); + if (!nextConfig) { + await transitionMysteryEncounterIntroVisuals(scene, false, false); + await showEncounterDialogue(scene, `${namespace}.victory`, `${namespace}.speaker`); + + // Give 10x Voucher + const newModifier = modifierTypes.VOUCHER_PREMIUM().newModifier(); + scene.addModifier(newModifier); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name })); + + await showEncounterDialogue(scene, `${namespace}.victory_2`, `${namespace}.speaker`); + scene.ui.clearText(); // Clears "Winstrate" title from screen as rewards get animated in + const machoBrace = generateModifierTypeOption(scene, modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE)!; + machoBrace.type.tier = ModifierTier.MASTER; + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [machoBrace], fillRemaining: false }); + encounter.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, false, MysteryEncounterMode.NO_BATTLE); + } else { + await initBattleWithEnemyConfig(scene, nextConfig); + } +} + +function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { + return new Promise(async resolve => { + if (scene.currentBattle.mysteryEncounter!.enemyPartyConfigs.length === 0) { + // Battle is over + const trainer = scene.currentBattle.trainer; + if (trainer) { + scene.tweens.add({ + targets: trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(trainer, true); + } + }); + } + + await spawnNextTrainerOrEndEncounter(scene); + resolve(); // Wait for all dialogue/post battle stuff to complete before resolving + } else { + scene.arena.resetArenaEffects(); + const playerField = scene.getPlayerField(); + playerField.forEach((_, p) => scene.unshiftPhase(new ReturnPhase(scene, p))); + + for (const pokemon of scene.getParty()) { + // Only trigger form change when Eiscue is in Noice form + // Hardcoded Eiscue for now in case it is fused with another pokemon + if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) { + scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger); + } + + pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + } + + scene.unshiftPhase(new ShowTrainerPhase(scene)); + // Hide the trainer and init next battle + const trainer = scene.currentBattle.trainer; + // Unassign previous trainer from battle so it isn't destroyed before animation completes + scene.currentBattle.trainer = null; + await spawnNextTrainerOrEndEncounter(scene); + if (trainer) { + scene.tweens.add({ + targets: trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(trainer, true); + resolve(); + } + }); + } + } + }); +} + +function getVictorTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICTOR, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SWELLOW), + isBoss: false, + abilityIndex: 0, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.BRAVE_BIRD, Moves.PROTECT, Moves.QUICK_ATTACK], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.FOCUS_BAND) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + }, + { + species: getPokemonSpecies(Species.OBSTAGOON), + isBoss: false, + abilityIndex: 1, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.OBSTRUCT, Moves.NIGHT_SLASH, Moves.FIRE_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + } + ] + }; +} + +function getVictoriaTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICTORIA, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.ROSERADE), + isBoss: false, + abilityIndex: 0, // Natural Cure + nature: Nature.CALM, + moveSet: [Moves.SYNTHESIS, Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.SLEEP_POWDER], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.GARDEVOIR), + isBoss: false, + formIndex: 1, + nature: Nature.TIMID, + moveSet: [Moves.PSYSHOCK, Moves.MOONBLAST, Moves.SHADOW_BALL, Moves.WILL_O_WISP], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.PSYCHIC]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FAIRY]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + } + ] + } + ] + }; +} + +function getViviTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VIVI, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SEAKING), + isBoss: false, + abilityIndex: 3, // Lightning Rod + nature: Nature.ADAMANT, + moveSet: [Moves.WATERFALL, Moves.MEGAHORN, Moves.KNOCK_OFF, Moves.REST], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, + stackCount: 4, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.BRELOOM), + isBoss: false, + abilityIndex: 1, // Poison Heal + nature: Nature.JOLLY, + moveSet: [Moves.SPORE, Moves.SWORDS_DANCE, Moves.SEED_BOMB, Moves.DRAIN_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, + stackCount: 4, + isTransferable: false + }, + { + modifier: generateModifierType(scene, modifierTypes.TOXIC_ORB) as PokemonHeldItemModifierType, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.CAMERUPT), + isBoss: false, + formIndex: 1, + nature: Nature.CALM, + moveSet: [Moves.EARTH_POWER, Moves.FIRE_BLAST, Moves.YAWN, Moves.PROTECT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 3, + isTransferable: false + }, + ] + } + ] + }; +} + +function getVickyTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VICKY, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.MEDICHAM), + isBoss: false, + formIndex: 1, + nature: Nature.IMPISH, + moveSet: [Moves.AXE_KICK, Moves.ICE_PUNCH, Moves.ZEN_HEADBUTT, Moves.BULLET_PUNCH], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType, + isTransferable: false + } + ] + } + ] + }; +} + +function getVitoTrainerConfig(scene: BattleScene): EnemyPartyConfig { + return { + trainerType: TrainerType.VITO, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.HISUI_ELECTRODE), + isBoss: false, + abilityIndex: 0, // Soundproof + nature: Nature.MODEST, + moveSet: [Moves.THUNDERBOLT, Moves.GIGA_DRAIN, Moves.FOUL_PLAY, Moves.THUNDER_WAVE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.SWALOT), + isBoss: false, + abilityIndex: 2, // Gluttony + nature: Nature.QUIET, + moveSet: [Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.ICE_BEAM, Moves.EARTHQUAKE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LEPPA]) as PokemonHeldItemModifierType, + stackCount: 2, + } + ] + }, + { + species: getPokemonSpecies(Species.DODRIO), + isBoss: false, + abilityIndex: 2, // Tangled Feet + nature: Nature.JOLLY, + moveSet: [Moves.DRILL_PECK, Moves.QUICK_ATTACK, Moves.THRASH, Moves.KNOCK_OFF], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.KINGS_ROCK) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + } + ] + }, + { + species: getPokemonSpecies(Species.ALAKAZAM), + isBoss: false, + formIndex: 1, + nature: Nature.BOLD, + moveSet: [Moves.PSYCHIC, Moves.SHADOW_BALL, Moves.FOCUS_BLAST, Moves.THUNDERBOLT], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.WIDE_LENS) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + }, + { + species: getPokemonSpecies(Species.DARMANITAN), + isBoss: false, + abilityIndex: 0, // Sheer Force + nature: Nature.IMPISH, + moveSet: [Moves.EARTHQUAKE, Moves.U_TURN, Moves.FLARE_BLITZ, Moves.ROCK_SLIDE], + modifierConfigs: [ + { + modifier: generateModifierType(scene, modifierTypes.QUICK_CLAW) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + ] + } + ] + }; +} diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts new file mode 100644 index 000000000000..6c0f1706fa5f --- /dev/null +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -0,0 +1,440 @@ +import { Ability, allAbilities } from "#app/data/ability"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { AbilityAttr } from "#app/system/game-data"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +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 { 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"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import i18next from "i18next"; +import { getStatKey } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:trainingSession"; + +/** + * Training Session encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3802 | GitHub Issue #3802} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrainingSessionEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRAINING_SESSION) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: "training_gear", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 6, + x: 5, + yShadow: -2 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn light training session with chosen pokemon + // Every 50 waves, add +1 boss segment, capping at 5 + const segments = Math.min( + 2 + Math.floor(scene.currentBattle.waveIndex / 50), + 5 + ); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig( + scene, + playerPokemon, + segments, + modifiers + ); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + encounter.setDialogueToken("stat1", "-"); + encounter.setDialogueToken("stat2", "-"); + // Add the pokemon back to party with IV boost + const ivIndexes: any[] = []; + playerPokemon.ivs.forEach((iv, index) => { + if (iv < 31) { + ivIndexes.push({ iv: iv, index: index }); + } + }); + + // Improves 2 random non-maxed IVs + // +10 if IV is < 10, +5 if between 10-20, and +3 if > 20 + // A 0-4 starting IV will cap in 6 encounters (assuming you always rolled that IV) + // 5-14 starting IV caps in 5 encounters + // 15-19 starting IV caps in 4 encounters + // 20-24 starting IV caps in 3 encounters + // 25-27 starting IV caps in 2 encounters + let improvedCount = 0; + while (ivIndexes.length > 0 && improvedCount < 2) { + randSeedShuffle(ivIndexes); + const ivToChange = ivIndexes.pop(); + let newVal = ivToChange.iv; + if (improvedCount === 0) { + encounter.setDialogueToken( + "stat1", + i18next.t(getStatKey(ivToChange.index)) ?? "" + ); + } else { + encounter.setDialogueToken( + "stat2", + i18next.t(getStatKey(ivToChange.index)) ?? "" + ); + } + + // Corrects required encounter breakpoints to be continuous for all IV values + if (ivToChange.iv <= 21 && ivToChange.iv - (1 % 5) === 0) { + newVal += 1; + } + + newVal += ivToChange.iv <= 10 ? 10 : ivToChange.iv <= 20 ? 5 : 3; + newVal = Math.min(newVal, 31); + playerPokemon.ivs[ivToChange.index] = newVal; + improvedCount++; + } + + if (improvedCount > 0) { + playerPokemon.calculateStats(); + scene.gameData.updateSpeciesDexIvs( + playerPokemon.species.getRootSpeciesId(true), + playerPokemon.ivs + ); + scene.gameData.setPokemonCaught(playerPokemon, false); + } + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + queueEncounterMessage(scene, `${namespace}.option.1.finished`); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and Nature + const encounter = scene.currentBattle.mysteryEncounter!; + const natures = new Array(25).fill(null).map((val, i) => i as Nature); + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for nature selection + return natures.map((nature: Nature) => { + const option: OptionSelectItem = { + label: getNatureName(nature, true, true, true, scene.uiTheme), + handler: () => { + // Pokemon and second option selected + encounter.setDialogueToken("nature", getNatureName(nature)); + encounter.misc = { + playerPokemon: pokemon, + chosenNature: nature, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // 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 modifiers = new ModifiersHolder(); + const config = getEnemyConfig( + scene, + playerPokemon, + segments, + modifiers + ); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + queueEncounterMessage(scene, `${namespace}.option.2.finished`); + // Add the pokemon back to party with Nature change + playerPokemon.setNature(encounter.misc.chosenNature); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and modifiers 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); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and ability to learn + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for ability selection + const speciesForm = !!pokemon.getFusionSpeciesForm() + ? pokemon.getFusionSpeciesForm() + : pokemon.getSpeciesForm(); + const abilityCount = speciesForm.getAbilityCount(); + const abilities: Ability[] = new Array(abilityCount) + .fill(null) + .map((val, i) => allAbilities[speciesForm.getAbility(i)]); + + const optionSelectItems: OptionSelectItem[] = []; + abilities.forEach((ability: Ability, index) => { + if (!optionSelectItems.some(o => o.label === ability.name)) { + const option: OptionSelectItem = { + label: ability.name, + handler: () => { + // Pokemon and ability selected + encounter.setDialogueToken("ability", ability.name); + encounter.misc = { + playerPokemon: pokemon, + abilityIndex: index, + }; + return true; + }, + onHover: () => { + showEncounterText(scene, ability.description, 0, 0, false); + }, + }; + optionSelectItems.push(option); + } + }); + + return optionSelectItems; + }; + + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn hard training session with chosen pokemon + // Every 30 waves, add +1 boss segment, capping at 6 + // Also starts with +1 to all stats + const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 30), 6); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); + config.pokemonConfigs![0].tags = [ + BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, + ]; + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + queueEncounterMessage(scene, `${namespace}.option.3.finished`); + // Add the pokemon back to party with ability change + const abilityIndex = encounter.misc.abilityIndex; + if (!!playerPokemon.getFusionSpeciesForm()) { + playerPokemon.fusionAbilityIndex = abilityIndex; + if (!isNullOrUndefined(playerPokemon.fusionSpecies?.speciesId) && speciesStarters.hasOwnProperty(playerPokemon.fusionSpecies!.speciesId)) { + scene.gameData.starterData[playerPokemon.fusionSpecies!.speciesId] + .abilityAttr |= + abilityIndex !== 1 || playerPokemon.fusionSpecies!.ability2 + ? Math.pow(2, playerPokemon.fusionAbilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } else { + playerPokemon.abilityIndex = abilityIndex; + if ( + speciesStarters.hasOwnProperty(playerPokemon.species.speciesId) + ) { + scene.gameData.starterData[ + playerPokemon.species.speciesId + ].abilityAttr |= + abilityIndex !== 1 || playerPokemon.species.ability2 + ? Math.pow(2, playerPokemon.abilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } + + playerPokemon.getAbility(); + playerPokemon.calculateStats(); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + }; + + setEncounterRewards(scene, { fillRemaining: true }, undefined, onBeforeRewardsPhase); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { + playerPokemon.resetSummonData(); + + // Passes modifiers by reference + modifiers.value = playerPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + const modifierConfigs = modifiers.value.map((mod) => { + return { + modifier: mod + }; + }) as HeldModifierConfig[]; + + const data = new PokemonData(playerPokemon); + return { + pokemonConfigs: [ + { + species: playerPokemon.species, + isBoss: true, + bossSegments: segments, + formIndex: playerPokemon.formIndex, + level: playerPokemon.level, + dataSource: data, + modifierConfigs: modifierConfigs, + }, + ], + }; +} + +class ModifiersHolder { + public value: PokemonHeldItemModifier[] = []; + + constructor() {} +} diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts new file mode 100644 index 000000000000..ec6291f2a8cd --- /dev/null +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -0,0 +1,222 @@ +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Species } from "#enums/species"; +import { HitHealModifier, PokemonHeldItemModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "#app/plugins/i18n"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { PokemonMove } from "#app/field/pokemon"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:trashToTreasure"; + +const SOUND_EFFECT_WAIT_TIME = 700; + +/** + * Trash to Treasure encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3809 | GitHub Issue #3809} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrashToTreasureEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRASH_TO_TREASURE) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(60, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) + .withMaxAllowedEncounters(1) + .withIntroSpriteConfigs([ + { + spriteKey: Species.GARBODOR.toString() + "-gigantamax", + fileRoot: "pokemon", + hasShadow: false, + disableAnimation: true, + scale: 1.5, + y: 8, + tint: 0.4 + } + ]) + .withAutoHideIntroVisuals(false) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + const bossSpecies = getPokemonSpecies(Species.GARBODOR); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + formIndex: 1, // Gmax + bossSegmentModifier: 1, // +1 Segment from normal + moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH] + }; + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [pokemonConfig], + disableSwitch: true + }; + encounter.enemyPartyConfigs = [config]; + + // Load animations/sfx for Garbodor fight start moves + loadCustomMovesForEncounter(scene, [Moves.TOXIC, Moves.AMNESIA]); + + scene.loadSe("PRSFX- Dig2", "battle_anims", "PRSFX- Dig2.wav"); + scene.loadSe("PRSFX- Venom Drench", "battle_anims", "PRSFX- Venom Drench.wav"); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play Dig2 and then Venom Drench sfx + doGarbageDig(scene); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Gain 2 Leftovers and 2 Shell Bell + transitionMysteryEncounterIntroVisuals(scene); + await tryApplyDigRewardItems(scene); + + // Give the player the Black Sludge curse + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.MYSTERY_ENCOUNTER_BLACK_SLUDGE)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Investigate garbage, battle Gmax Garbodor + scene.setFieldScale(0.75); + await showEncounterText(scene, `${namespace}.option.2.selected_2`); + transitionMysteryEncounterIntroVisuals(scene); + + const encounter = scene.currentBattle.mysteryEncounter!; + + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.TOXIC), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY], + move: new PokemonMove(Moves.AMNESIA), + ignorePp: true + }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + }) + .build() + ) + .build(); + +async function tryApplyDigRewardItems(scene: BattleScene) { + const shellBell = generateModifierType(scene, modifierTypes.SHELL_BELL) as PokemonHeldItemModifierType; + const leftovers = generateModifierType(scene, modifierTypes.LEFTOVERS) as PokemonHeldItemModifierType; + + const party = scene.getParty(); + + // Iterate over the party until an item was successfully given + // First leftovers + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; + + if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, leftovers); + break; + } + } + + // Second leftovers + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingLeftovers = heldItems.find(m => m instanceof TurnHealModifier) as TurnHealModifier; + + if (!existingLeftovers || existingLeftovers.getStackCount() < existingLeftovers.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, leftovers); + break; + } + } + + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + leftovers.name }), null, undefined, true); + + // First Shell bell + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier; + + if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, shellBell); + break; + } + } + + // Second Shell bell + for (const pokemon of party) { + const heldItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; + const existingShellBell = heldItems.find(m => m instanceof HitHealModifier) as HitHealModifier; + + if (!existingShellBell || existingShellBell.getStackCount() < existingShellBell.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, shellBell); + break; + } + } + + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: "2 " + shellBell.name }), null, undefined, true); +} + +async function doGarbageDig(scene: BattleScene) { + scene.playSound("battle_anims/PRSFX- Dig2"); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => { + scene.playSound("battle_anims/PRSFX- Dig2"); + scene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2 }); + }); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME * 2, () => { + scene.playSound("battle_anims/PRSFX- Dig2"); + }); +} diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts new file mode 100644 index 000000000000..f9148b87f9bd --- /dev/null +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -0,0 +1,268 @@ +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { catchPokemon, getHighestLevelPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { SelfStatusMove } from "#app/data/move"; +import { PokeballType } from "#enums/pokeball"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { BerryModifier } from "#app/modifier/modifier"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:uncommonBreed"; + +/** + * Uncommon Breed encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3811 | GitHub Issue #3811} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const UncommonBreedEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.UNCOMMON_BREED) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter!; + + // Calculate boss mon + // Level equal to 2 below highest party member + const level = getHighestLevelPlayerPokemon(scene).level - 2; + const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); + const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); + const speciesRootForm = pokemon.species.getRootSpeciesId(); + + // Pokemon will always have one of its egg moves in its moveset + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + const eggMoveIndex = randSeedInt(4); + const randomEggMove: Moves = eggMoves[eggMoveIndex]; + encounter.misc = { + eggMove: randomEggMove + }; + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[0] = new PokemonMove(randomEggMove); + } + } + + encounter.misc.pokemon = pokemon; + + const config: EnemyPartyConfig = { + pokemonConfigs: [{ + level: level, + species: species, + dataSource: new PokemonData(pokemon), + isBoss: false, + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon); + encounter.spriteConfigs = [ + { + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + x: -5, + repeat: true, + isPokemon: true + }, + ]; + + encounter.setDialogueToken("enemyPokemon", pokemon.getNameToRender()); + scene.loadSe("PRSFX- Spotlight2", "battle_anims", "PRSFX- Spotlight2.wav"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Animate the pokemon + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemonSprite = encounter.introVisuals!.getSprites(); + + scene.tweens.add({ // Bounce at the end + targets: pokemonSprite, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: "-=20", + loop: 1, + }); + + scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2")); + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter!; + + const eggMove = encounter.misc.eggMove; + if (!isNullOrUndefined(eggMove)) { + // Check what type of move the egg move is to determine target + const pokemonMove = new PokemonMove(eggMove); + const move = pokemonMove.getMove(); + const target = move instanceof SelfStatusMove ? BattlerIndex.ENEMY : BattlerIndex.PLAYER; + + encounter.startOfBattleEffects.push( + { + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [target], + move: pokemonMove, + ignorePp: true + }); + } + + setEncounterRewards(scene, { fillRemaining: true }); + await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); + } + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give it some food + + // Remove 4 random berries from player's party + // Get all player berry items, remove from party, and store reference + const berryItems: BerryModifier[]= scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + for (let i = 0; i < 4; i++) { + const index = randSeedInt(berryItems.length); + const randBerry = berryItems[index]; + randBerry.stackCount--; + if (randBerry.stackCount === 0) { + scene.removeModifier(randBerry); + berryItems.splice(index, 1); + } + } + await scene.updateModifiers(true, true); + + // Pokemon joins the team, with 2 egg moves + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + + // Give 1 additional egg move + const previousEggMove = encounter.misc.eggMove; + const speciesRootForm = pokemon.species.getRootSpeciesId(); + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + let randomEggMove: Moves = eggMoves[randSeedInt(4)]; + while (randomEggMove === previousEggMove) { + randomEggMove = eggMoves[randSeedInt(4)]; + } + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[1] = new PokemonMove(randomEggMove); + } + } + + await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false); + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }) + .withOptionPhase(async (scene: BattleScene) => { + // Attract the pokemon with a move + // Pokemon joins the team, with 2 egg moves and IVs rolled an additional time + const encounter = scene.currentBattle.mysteryEncounter!; + const pokemon = encounter.misc.pokemon; + + // Give 1 additional egg move + const previousEggMove = encounter.misc.eggMove; + const speciesRootForm = pokemon.species.getRootSpeciesId(); + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves: Moves[] = speciesEggMoves[speciesRootForm]; + let randomEggMove: Moves = eggMoves[randSeedInt(4)]; + while (randomEggMove === previousEggMove) { + randomEggMove = eggMoves[randSeedInt(4)]; + } + if (pokemon.moveset.length < 4) { + pokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + pokemon.moveset[1] = new PokemonMove(randomEggMove); + } + } + + // Roll IVs a second time + pokemon.ivs = pokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false); + if (encounter.selectedOption?.primaryPokemon?.id) { + setEncounterExp(scene, encounter.selectedOption.primaryPokemon.id, pokemon.getExpValue(), false); + } + setEncounterRewards(scene, { fillRemaining: true }); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts new file mode 100644 index 000000000000..476cc98f503e --- /dev/null +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -0,0 +1,542 @@ +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 { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; +import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; +import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { achvs } from "#app/system/achv"; +import { 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 { modifierTypes } from "#app/modifier/modifier-type"; +import i18next from "#app/plugins/i18n"; +import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; +import { getLevelTotalExp } from "#app/data/exp"; +import { Stat } from "#enums/stat"; +import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, GameModes } from "#app/game-mode"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:weirdDream"; + +/** Exclude Ultra Beasts, Paradox, Eternatus, and all legendary/mythical/trio pokemon that are below 570 BST */ +const EXCLUDED_TRANSFORMATION_SPECIES = [ + Species.ETERNATUS, + /** UBs */ + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + /** Paradox */ + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN, + /** These are banned so they don't appear in the < 570 BST pool */ + Species.COSMOG, + Species.MELTAN, + Species.KUBFU, + Species.COSMOEM, + Species.POIPOLE, + Species.TERAPAGOS, + Species.TYPE_NULL, + Species.CALYREX, + Species.NAGANADEL, + Species.URSHIFU, + Species.OGERPON, + Species.OKIDOGI, + Species.MUNKIDORI, + Species.FEZANDIPITI, +]; + +const SUPER_LEGENDARY_BST_THRESHOLD = 600; +const NON_LEGENDARY_BST_THRESHOLD = 570; +const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450; + +/** + * Value ranges of the resulting species BST transformations after adding values to original species + * 2 Pokemon in the party use this range + */ +const HIGH_BST_TRANSFORM_BASE_VALUES: [number, number] = [90, 110]; +/** + * Value ranges of the resulting species BST transformations after adding values to original species + * All remaining Pokemon in the party use this range + */ +const STANDARD_BST_TRANSFORM_BASE_VALUES: [number, number] = [40, 50]; + +/** + * Weird Dream encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3822 | GitHub Issue #3822} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const WeirdDreamEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withDisabledGameModes(GameModes.CHALLENGE) + .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withIntroSpriteConfigs([ + { + spriteKey: "weird_dream_woman", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 11, + yShadow: 6, + x: 4 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Change the bgm + scene.fadeOutBgm(3000, false); + scene.time.delayedCall(3000, () => { + scene.playBgm("mystery_encounter_weird_dream"); + }); + + return true; + }) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + } + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play the animation as the player goes through the dialogue + scene.time.delayedCall(1000, () => { + doShowDreamBackground(scene); + }); + + // Calculate all the newly transformed Pokemon and begin asset load + const teamTransformations = getTeamTransformations(scene); + const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); + scene.currentBattle.mysteryEncounter!.misc = { + teamTransformations, + loadAssets + }; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue + const cutsceneDialoguePromise = showEncounterText(scene, `${namespace}.option.1.cutscene`); + + // Change the entire player's party + // Wait for all new Pokemon assets to be loaded before showing transformation animations + await Promise.all(scene.currentBattle.mysteryEncounter!.misc.loadAssets); + const transformations = scene.currentBattle.mysteryEncounter!.misc.teamTransformations; + + // If there are 1-3 transformations, do them centered back to back + // Otherwise, the first 3 transformations are executed side-by-side, then any remaining 1-3 transformations occur in those same respective positions + if (transformations.length <= 3) { + for (const transformation of transformations) { + const pokemon1 = transformation.previousPokemon; + const pokemon2 = transformation.newPokemon; + + await doPokemonTransformationSequence(scene, pokemon1, pokemon2, TransformationScreenPosition.CENTER); + } + } else { + await doSideBySideTransformations(scene, transformations); + } + + // Make sure player has finished cutscene dialogue + await cutsceneDialoguePromise; + + doHideDreamBackground(scene); + await showEncounterText(scene, `${namespace}.option.1.dream_complete`); + + await doNewTeamPostProcess(scene, transformations); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT]}); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Reduce party levels by 20% + for (const pokemon of scene.getParty()) { + pokemon.level = Math.max(Math.ceil(0.8 * pokemon.level), 1); + pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); + pokemon.levelExp = 0; + + pokemon.calculateStats(); + pokemon.updateInfo(); + } + + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +interface PokemonTransformation { + previousPokemon: PlayerPokemon; + newSpecies: PokemonSpecies; + newPokemon: PlayerPokemon; + heldItems: PokemonHeldItemModifier[]; +} + +function getTeamTransformations(scene: BattleScene): PokemonTransformation[] { + const party = scene.getParty(); + // Removes all pokemon from the party + const alreadyUsedSpecies: PokemonSpecies[] = []; + const pokemonTransformations: PokemonTransformation[] = party.map(p => { + return { + previousPokemon: p + } as PokemonTransformation; + }); + + // Only 1 Pokemon can be transformed into BST higher than 600 + let hasPokemonInSuperLegendaryBstThreshold = false; + // Only 1 other Pokemon can be transformed into BST between 570-600 + let hasPokemonInLegendaryBstThreshold = false; + + // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference + // Then, roll the remainder of the party members at a +40 to +50 BST difference + const numPokemon = party.length; + for (let i = 0; i < numPokemon; i++) { + const removed = party[randSeedInt(party.length)]; + const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); + pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); + scene.removePokemonFromPlayerParty(removed, false); + + const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0); + let newBstRange: [number, number]; + if (i < 2) { + newBstRange = HIGH_BST_TRANSFORM_BASE_VALUES; + } else { + newBstRange = STANDARD_BST_TRANSFORM_BASE_VALUES; + } + + const newSpecies = getTransformedSpecies(bst, newBstRange, hasPokemonInSuperLegendaryBstThreshold, hasPokemonInLegendaryBstThreshold, alreadyUsedSpecies); + + const newSpeciesBst = newSpecies.getBaseStatTotal(); + if (newSpeciesBst > SUPER_LEGENDARY_BST_THRESHOLD) { + hasPokemonInSuperLegendaryBstThreshold = true; + } + if (newSpeciesBst <= SUPER_LEGENDARY_BST_THRESHOLD && newSpeciesBst >= NON_LEGENDARY_BST_THRESHOLD) { + hasPokemonInLegendaryBstThreshold = true; + } + + + pokemonTransformations[index].newSpecies = newSpecies; + alreadyUsedSpecies.push(newSpecies); + } + + for (const transformation of pokemonTransformations) { + const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); + const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined); + transformation.newPokemon = newPlayerPokemon; + scene.getParty().push(newPlayerPokemon); + } + + return pokemonTransformations; +} + +async function doNewTeamPostProcess(scene: BattleScene, transformations: PokemonTransformation[]) { + let atLeastOneNewStarter = false; + for (const transformation of transformations) { + const previousPokemon = transformation.previousPokemon; + const newPokemon = transformation.newPokemon; + const speciesRootForm = newPokemon.species.getRootSpeciesId(); + + // Roll HA a second time + if (newPokemon.species.abilityHidden) { + const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; + if (newPokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + newPokemon.abilityIndex = hiddenIndex; + } + } + } + + // Roll IVs a second time + newPokemon.ivs = newPokemon.ivs.map(iv => { + const newValue = randSeedInt(31); + return newValue > iv ? newValue : iv; + }); + + // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it + if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) { + if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (newPokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (newPokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (newPokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); + const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false); + if (newStarterUnlocked) { + atLeastOneNewStarter = true; + queueEncounterMessage(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() })); + } + } + + // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) + newPokemon.ivs = newPokemon.ivs.map((iv, index) => { + return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; + }); + + // For pokemon that the player owns (including ones just caught), gain a candy + if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { + scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); + } + + // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move of the new species + newPokemon.moveset = previousPokemon.moveset; + if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { + const eggMoves = speciesEggMoves[speciesRootForm]; + const eggMoveIndex = randSeedInt(4); + const randomEggMove = eggMoves[eggMoveIndex]; + if (newPokemon.moveset.length < 4) { + newPokemon.moveset.push(new PokemonMove(randomEggMove)); + } else { + newPokemon.moveset[randSeedInt(4)] = new PokemonMove(randomEggMove); + } + // For pokemon that the player owns (including ones just caught), unlock the egg move + if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { + await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), eggMoveIndex, true); + } + } + + // Randomize the second type of the pokemon + // If the pokemon does not normally have a second type, it will gain 1 + const newTypes = [newPokemon.getTypes()[0]]; + let newType = randSeedInt(18) as Type; + while (newType === newTypes[0]) { + newType = randSeedInt(18) as Type; + } + newTypes.push(newType); + if (!newPokemon.mysteryEncounterPokemonData) { + newPokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + } + newPokemon.mysteryEncounterPokemonData.types = newTypes; + + for (const item of transformation.heldItems) { + item.pokemonId = newPokemon.id; + scene.addModifier(item, false, false, false, true); + } + + // Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef + if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) { + const stats: Stat[] = [Stat.HP]; + const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0); + // Attack or SpAtk + stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); + // Def or SpDef + stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); + // const mod = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().newModifier(newPokemon, 20, stats); + const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().generateType(scene.getParty(), [20, stats]); + const modifier = modType?.newModifier(newPokemon); + if (modifier) { + scene.addModifier(modifier); + } + } + + // Enable passive if previous had it + newPokemon.passive = previousPokemon.passive; + + newPokemon.calculateStats(); + newPokemon.initBattleInfo(); + } + + // One random pokemon will get its passive unlocked + const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive); + if (passiveDisabledPokemon?.length > 0) { + passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)].passive = true; + } + + // If at least one new starter was unlocked, play 1 fanfare + if (atLeastOneNewStarter) { + scene.playSound("level_up_fanfare"); + } +} + +function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies { + let newSpecies: PokemonSpecies | undefined; + while (isNullOrUndefined(newSpecies)) { + const bstCap = originalBst + bstSearchRange[1]; + const bstMin = Math.max(originalBst + bstSearchRange[0], 0); + + // Get any/all species that fall within the Bst range requirements + let validSpecies = allSpecies + .filter(s => { + const speciesBst = s.getBaseStatTotal(); + const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap; + // Checks that a Pokemon has not already been added in the +600 or 570-600 slots; + const validBst = (!hasPokemonBstBetween570And600 || (speciesBst < NON_LEGENDARY_BST_THRESHOLD || speciesBst > SUPER_LEGENDARY_BST_THRESHOLD)) && + (!hasPokemonBstHigherThan600 || speciesBst <= SUPER_LEGENDARY_BST_THRESHOLD); + return bstInRange && validBst && !EXCLUDED_TRANSFORMATION_SPECIES.includes(s.speciesId); + }); + + // There must be at least 20 species available before it will choose one + if (validSpecies?.length > 20) { + validSpecies = randSeedShuffle(validSpecies); + newSpecies = validSpecies.pop(); + while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies!)) { + newSpecies = validSpecies.pop(); + } + } else { + // Expands search rand until a Pokemon is found + bstSearchRange[0] -= 10; + bstSearchRange[1] += 10; + } + } + + return newSpecies!; +} + +function doShowDreamBackground(scene: BattleScene) { + const transformationContainer = scene.add.container(0, -scene.game.canvas.height / 6); + transformationContainer.name = "Dream Background"; + + // In case it takes a bit for video to load + const transformationStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0); + transformationStaticBg.setName("Black Background"); + transformationStaticBg.setOrigin(0, 0); + transformationContainer.add(transformationStaticBg); + transformationStaticBg.setVisible(true); + + const transformationVideoBg: Phaser.GameObjects.Video = scene.add.video(0, 0, "evo_bg").stop(); + transformationVideoBg.setLoop(true); + transformationVideoBg.setOrigin(0, 0); + transformationVideoBg.setScale(0.4359673025); + transformationContainer.add(transformationVideoBg); + + scene.fieldUI.add(transformationContainer); + scene.fieldUI.bringToTop(transformationContainer); + transformationVideoBg.play(); + + transformationContainer.setVisible(true); + transformationContainer.alpha = 0; + + scene.tweens.add({ + targets: transformationContainer, + alpha: 1, + duration: 3000, + ease: "Sine.easeInOut" + }); +} + +function doHideDreamBackground(scene: BattleScene) { + const transformationContainer = scene.fieldUI.getByName("Dream Background"); + + scene.tweens.add({ + targets: transformationContainer, + alpha: 0, + duration: 3000, + ease: "Sine.easeInOut", + onComplete: () => { + scene.fieldUI.remove(transformationContainer, true); + } + }); +} + +function doSideBySideTransformations(scene: BattleScene, transformations: PokemonTransformation[]) { + return new Promise(resolve => { + const allTransformationPromises: Promise[] = []; + for (let i = 0; i < 3; i++) { + const delay = i * 4000; + scene.time.delayedCall(delay, () => { + const transformation = transformations[i]; + const pokemon1 = transformation.previousPokemon; + const pokemon2 = transformation.newPokemon; + const screenPosition = i as TransformationScreenPosition; + + const transformationPromise = doPokemonTransformationSequence(scene, pokemon1, pokemon2, screenPosition) + .then(() => { + if (transformations.length > i + 3) { + const nextTransformationAtPosition = transformations[i + 3]; + const nextPokemon1 = nextTransformationAtPosition.previousPokemon; + const nextPokemon2 = nextTransformationAtPosition.newPokemon; + + allTransformationPromises.push(doPokemonTransformationSequence(scene, nextPokemon1, nextPokemon2, screenPosition)); + } + }); + allTransformationPromises.push(transformationPromise); + }); + } + + // Wait for all transformations to be loaded into promise array + const id = setInterval(checkAllPromisesExist, 500); + async function checkAllPromisesExist() { + if (allTransformationPromises.length === transformations.length) { + clearInterval(id); + await Promise.all(allTransformationPromises); + resolve(); + } + } + }); +} diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts new file mode 100644 index 000000000000..e0ba8512d34d --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -0,0 +1,75 @@ +import { TextStyle } from "#app/ui/text"; + +export class TextDisplay { + speaker?: string; + text: string; + style?: TextStyle; +} + +export class OptionTextDisplay { + buttonLabel: string; + buttonTooltip?: string; + disabledButtonLabel?: string; + disabledButtonTooltip?: string; + secondOptionPrompt?: string; + selected?: TextDisplay[]; + style?: TextStyle; +} + +export class EncounterOptionsDialogue { + title?: string; + description?: string; + query?: string; + /** Options array with minimum 2 options */ + options?: [...OptionTextDisplay[]]; +} + +/** + * Example MysteryEncounterDialogue object: + * + { + intro: [ + { + text: "this is a rendered as a message window (no title display)" + }, + { + speaker: "John" + text: "this is a rendered as a dialogue window (title "John" is displayed above text)" + } + ], + encounterOptionsDialogue: { + title: "This is the title displayed at top of encounter description box", + description: "This is the description in the middle of encounter description box", + query: "This is an optional question displayed at the bottom of the description box (keep it short)", + options: [ + { + buttonLabel: "Option #1 button label (keep these short)", + selected: [ // Optional dialogue windows displayed when specific option is selected and before functional logic for the option is executed + { + text: "You chose option #1 message" + }, + { + speaker: "John" + text: "So, you've chosen option #1! It's time to d-d-d-duel!" + } + ] + }, + { + buttonLabel: "Option #2" + } + ], + }, + outro: [ + { + text: "This message will be displayed at the very end of the encounter (i.e. post battle, post reward, etc.)" + } + ], + } + * + */ +export default class MysteryEncounterDialogue { + intro?: TextDisplay[]; + encounterOptionsDialogue?: EncounterOptionsDialogue; + outro?: TextDisplay[]; +} + diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts new file mode 100644 index 000000000000..fb3daf53a8ba --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -0,0 +1,303 @@ +import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; +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 { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; + + +export type OptionPhaseCallback = (scene: BattleScene) => Promise; + +/** + * Used by {@linkcode MysteryEncounterOptionBuilder} class to define required/optional properties on the {@linkcode MysteryEncounterOption} class when building. + * + * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounterOption} construction. + * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounterOption} class itself. + */ +export interface IMysteryEncounterOption { + optionMode: MysteryEncounterOptionMode; + hasDexProgress: boolean; + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSecondaryRequirements: boolean; + + dialogue?: OptionTextDisplay; + + onPreOptionPhase?: OptionPhaseCallback; + onOptionPhase: OptionPhaseCallback; + onPostOptionPhase?: OptionPhaseCallback; +} + +export default class MysteryEncounterOption implements IMysteryEncounterOption { + optionMode: MysteryEncounterOptionMode; + hasDexProgress: boolean; + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + primaryPokemon?: PlayerPokemon; + secondaryPokemon?: PlayerPokemon[]; + excludePrimaryFromSecondaryRequirements: boolean; + + /** + * Dialogue object containing all the dialogue, messages, tooltips, etc. for this option + * Will be populated on {@linkcode MysteryEncounter} initialization + */ + dialogue?: OptionTextDisplay; + + /** Executes before any following dialogue or business logic from option. Usually this will be for calculating dialogueTokens or performing scene/data updates */ + onPreOptionPhase?: OptionPhaseCallback; + /** Business logic function for option */ + onOptionPhase: OptionPhaseCallback; + /** Executes after the encounter is over. Usually this will be for calculating dialogueTokens or performing data updates */ + onPostOptionPhase?: OptionPhaseCallback; + + constructor(option: IMysteryEncounterOption | null) { + if (!isNullOrUndefined(option)) { + Object.assign(this, option); + } + this.hasDexProgress = this.hasDexProgress ?? false; + this.requirements = this.requirements ?? []; + this.primaryPokemonRequirements = this.primaryPokemonRequirements ?? []; + this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ?? []; + } + + /** + * Returns true if option contains any {@linkcode EncounterRequirement}s, false otherwise. + */ + hasRequirements(): boolean { + return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0; + } + + /** + * Returns true if all {@linkcode EncounterRequirement}s for the option are met + * @param scene + */ + meetsRequirements(scene: BattleScene): boolean { + return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) + && this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) + && this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met + * @param scene + * @param pokemon + */ + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}. + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { + return true; + } + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.primaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + const queryParty = req.queryParty(scene.getParty()); + qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); + } else { + this.primaryPokemon = undefined; + return false; + } + } + + if (qualified.length === 0) { + return false; + } + + if (this.excludePrimaryFromSecondaryRequirements && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + const truePrimaryPool: PlayerPokemon[] = []; + const overlap: PlayerPokemon[] = []; + for (const qp of qualified) { + if (!this.secondaryPokemon.includes(qp)) { + truePrimaryPool.push(qp); + } else { + overlap.push(qp); + } + + } + if (truePrimaryPool.length > 0) { + // always choose from the non-overlapping pokemon first + this.primaryPokemon = truePrimaryPool[randSeedInt(truePrimaryPool.length)]; + return true; + } else { + // if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool + if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) { + this.primaryPokemon = overlap[randSeedInt(overlap.length)]; + this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon); + return true; + } + console.log("Mystery Encounter Edge Case: Requirement not met due to primay pokemon overlapping with support pokemon. There's no valid primary pokemon left."); + return false; + } + } else { + // Just pick the first qualifying Pokemon + this.primaryPokemon = qualified[0]; + return true; + } + } + + /** + * Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable). + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + meetsSupportingRequirementAndSupportingPokemonSelected(scene: BattleScene): boolean { + if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { + this.secondaryPokemon = []; + return true; + } + + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.secondaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + const queryParty = req.queryParty(scene.getParty()); + qualified = qualified.filter(pkmn => queryParty.includes(pkmn)); + } else { + this.secondaryPokemon = []; + return false; + } + } + this.secondaryPokemon = qualified; + return true; + } +} + +export class MysteryEncounterOptionBuilder implements Partial { + optionMode: MysteryEncounterOptionMode = MysteryEncounterOptionMode.DEFAULT; + requirements: EncounterSceneRequirement[] = []; + primaryPokemonRequirements: EncounterPokemonRequirement[] = []; + secondaryPokemonRequirements: EncounterPokemonRequirement[] = []; + excludePrimaryFromSecondaryRequirements: boolean = false; + isDisabledOnRequirementsNotMet: boolean = true; + hasDexProgress: boolean = false; + dialogue?: OptionTextDisplay; + + static newOptionWithMode(optionMode: MysteryEncounterOptionMode): MysteryEncounterOptionBuilder & Pick { + return Object.assign(new MysteryEncounterOptionBuilder(), { optionMode }); + } + + withHasDexProgress(hasDexProgress: boolean): this & Required> { + return Object.assign(this, { hasDexProgress: hasDexProgress }); + } + + /** + * Adds a {@linkcode EncounterSceneRequirement} to {@linkcode requirements} + * @param requirement + */ + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { + if (requirement instanceof EncounterPokemonRequirement) { + Error("Incorrectly added pokemon requirement as scene requirement."); + } + + this.requirements.push(requirement); + return Object.assign(this, { requirements: this.requirements }); + } + + withSceneMoneyRequirement(requiredMoney?: number, scalingMultiplier?: number) { + return this.withSceneRequirement(new MoneyRequirement(requiredMoney, scalingMultiplier)); + } + + /** + * Defines logic that runs immediately when an option is selected, but before the Encounter continues. + * Can determine whether or not the Encounter *should* continue. + * If there are scenarios where the Encounter should NOT continue, should return boolean instead of void. + * @param onPreOptionPhase + */ + withPreOptionPhase(onPreOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPreOptionPhase: onPreOptionPhase }); + } + + /** + * MUST be defined by every {@linkcode MysteryEncounterOption} + * @param onOptionPhase + */ + withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onOptionPhase: onOptionPhase }); + } + + withPostOptionPhase(onPostOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPostOptionPhase: onPostOptionPhase }); + } + + /** + * Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode primaryPokemonRequirements} + * @param requirement + */ + withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.primaryPokemonRequirements.push(requirement); + return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); + } + + /** + * Player is required to have certain type/s of pokemon in his party (with optional min number of pokemons with that type) + * + * @param type the required type/s + * @param excludeFainted whether to exclude fainted pokemon + * @param minNumberOfPokemon number of pokemons to have that type + * @param invertQuery + * @returns + */ + withPokemonTypeRequirement(type: Type | Type[], excludeFainted?: boolean, minNumberOfPokemon?: number, invertQuery?: boolean) { + return this.withPrimaryPokemonRequirement(new TypeRequirement(type, excludeFainted, minNumberOfPokemon, invertQuery)); + } + + /** + * Player is required to have a pokemon that can learn a certain move/moveset + * + * @param move the required move/moves + * @param options see {@linkcode CanLearnMoveRequirementOptions} + * @returns + */ + withPokemonCanLearnMoveRequirement(move: Moves | Moves[], options?: CanLearnMoveRequirementOptions) { + return this.withPrimaryPokemonRequirement(new CanLearnMoveRequirement(move, options)); + } + + /** + * Adds a {@linkcode EncounterPokemonRequirement} to {@linkcode secondaryPokemonRequirements} + * @param requirement + * @param excludePrimaryFromSecondaryRequirements + */ + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.secondaryPokemonRequirements.push(requirement); + this.excludePrimaryFromSecondaryRequirements = excludePrimaryFromSecondaryRequirements; + return Object.assign(this, { secondaryPokemonRequirements: this.secondaryPokemonRequirements }); + } + + /** + * Set the full dialogue object to the option. Will override anything already set + * + * @param dialogue see {@linkcode OptionTextDisplay} + * @returns + */ + withDialogue(dialogue: OptionTextDisplay) { + this.dialogue = dialogue; + return this; + } + + build(this: IMysteryEncounterOption) { + return new MysteryEncounterOption(this); + } +} diff --git a/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts new file mode 100644 index 000000000000..fc6ce313d415 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts @@ -0,0 +1,25 @@ +import { Abilities } from "#enums/abilities"; +import { Type } from "#app/data/type"; +import { isNullOrUndefined } from "#app/utils"; + +/** + * Data that can customize a Pokemon in non-standard ways from its Species + * Currently only used by Mystery Encounters, may need to be renamed if it becomes more widely used + */ +export class MysteryEncounterPokemonData { + public spriteScale: number; + public ability: Abilities | -1; + public passive: Abilities | -1; + public types: Type[]; + + constructor(data?: MysteryEncounterPokemonData | Partial) { + if (!isNullOrUndefined(data)) { + Object.assign(this, data); + } + + this.spriteScale = this.spriteScale ?? -1; + this.ability = this.ability ?? -1; + this.passive = this.passive ?? -1; + this.types = this.types ?? []; + } +} diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts new file mode 100644 index 000000000000..8dd6568e9295 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -0,0 +1,1022 @@ +import { PlayerPokemon } from "#app/field/pokemon"; +import BattleScene from "#app/battle-scene"; +import { isNullOrUndefined } from "#app/utils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { TimeOfDay } from "#enums/time-of-day"; +import { Nature } from "../nature"; +import { EvolutionItem, pokemonEvolutions } from "../pokemon-evolutions"; +import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeItemTrigger } from "../pokemon-forms"; +import { SpeciesFormKey } from "../pokemon-species"; +import { StatusEffect } from "../status-effect"; +import { Type } from "../type"; +import { WeatherType } from "../weather"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { AttackTypeBoosterModifier } from "#app/modifier/modifier"; +import { AttackTypeBoosterModifierType } from "#app/modifier/modifier-type"; + +export interface EncounterRequirement { + meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met + getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export abstract class EncounterSceneRequirement implements EncounterRequirement { + /** + * Returns whether the EncounterSceneRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ + abstract meetsRequirement(scene: BattleScene): boolean; + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationSceneRequirement extends EncounterSceneRequirement { + orRequirements: EncounterSceneRequirement[]; + + constructor(... orRequirements: EncounterSceneRequirement[]) { + super(); + this.orRequirements = orRequirements; + } + + override meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return this.orRequirements[0].getDialogueToken(scene, pokemon); + } +} + +export abstract class EncounterPokemonRequirement implements EncounterRequirement { + public minNumberOfPokemon: number; + public invertQuery: boolean; + + /** + * Returns whether the EncounterPokemonRequirement's... requirements, are met by the given scene + * @param partyPokemon + */ + abstract meetsRequirement(scene: BattleScene): boolean; + + /** + * Returns all party members that are compatible with this requirement. For non pokemon related requirements, the entire party is returned. + * @param partyPokemon + */ + abstract queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[]; + + /** + * Returns a dialogue token key/value pair for a given Requirement. + * Should be overridden by child Requirement classes. + * @param scene + * @param pokemon + */ + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationPokemonRequirement extends EncounterPokemonRequirement { + orRequirements: EncounterPokemonRequirement[]; + + constructor(...orRequirements: EncounterPokemonRequirement[]) { + super(); + this.invertQuery = false; + this.minNumberOfPokemon = 1; + this.orRequirements = orRequirements; + } + + override meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + for (const req of this.orRequirements) { + const result = req.queryParty(partyPokemon); + if (result?.length > 0) { + return result; + } + } + + return []; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return this.orRequirements[0].getDialogueToken(scene, pokemon); + } +} + +export class PreviousEncounterRequirement extends EncounterSceneRequirement { + previousEncounterRequirement: MysteryEncounterType; + + /** + * Used for specifying an encounter that must be seen before this encounter can spawn + * @param previousEncounterRequirement + */ + constructor(previousEncounterRequirement: MysteryEncounterType) { + super(); + this.previousEncounterRequirement = previousEncounterRequirement; + } + + override meetsRequirement(scene: BattleScene): boolean { + return scene.mysteryEncounterSaveData.encounteredEvents.some(e => e.type === this.previousEncounterRequirement); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["previousEncounter", scene.mysteryEncounterSaveData.encounteredEvents.find(e => e.type === this.previousEncounterRequirement)?.[0].toString() ?? ""]; + } +} + +export class WaveRangeRequirement extends EncounterSceneRequirement { + waveRange: [number, number]; + + /** + * Used for specifying a unique wave or wave range requirement + * If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number + * @param waveRange [min, max] + */ + constructor(waveRange: [number, number]) { + super(); + this.waveRange = waveRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + if (!isNullOrUndefined(this.waveRange) && this.waveRange?.[0] <= this.waveRange?.[1]) { + const waveIndex = scene.currentBattle.waveIndex; + if (waveIndex >= 0 && (this.waveRange?.[0] >= 0 && this.waveRange?.[0] > waveIndex) || (this.waveRange?.[1] >= 0 && this.waveRange?.[1] < waveIndex)) { + return false; + } + } + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["waveIndex", scene.currentBattle.waveIndex.toString()]; + } +} + +export class WaveModulusRequirement extends EncounterSceneRequirement { + waveModuli: number[]; + modulusValue: number; + + /** + * Used for specifying a modulus requirement on the wave index + * For example, can be used to require the wave index to end with 1, 2, or 3 + * @param waveModuli The allowed modulus results + * @param modulusValue The modulus calculation value + * + * Example: + * new WaveModulusRequirement([1, 2, 3], 10) will check for 1st/2nd/3rd waves that are immediately after a multiple of 10 wave + * So waves 21, 32, 53 all return true. 58, 14, 99 return false. + */ + constructor(waveModuli: number[], modulusValue: number) { + super(); + this.waveModuli = waveModuli; + this.modulusValue = modulusValue; + } + + override meetsRequirement(scene: BattleScene): boolean { + return this.waveModuli.includes(scene.currentBattle.waveIndex % this.modulusValue); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["waveIndex", scene.currentBattle.waveIndex.toString()]; + } +} + +export class TimeOfDayRequirement extends EncounterSceneRequirement { + requiredTimeOfDay: TimeOfDay[]; + + constructor(timeOfDay: TimeOfDay | TimeOfDay[]) { + super(); + this.requiredTimeOfDay = Array.isArray(timeOfDay) ? timeOfDay : [timeOfDay]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const timeOfDay = scene.arena?.getTimeOfDay(); + if (!isNullOrUndefined(timeOfDay) && this.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) { + return false; + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["timeOfDay", TimeOfDay[scene.arena.getTimeOfDay()].toLocaleLowerCase()]; + } +} + +export class WeatherRequirement extends EncounterSceneRequirement { + requiredWeather: WeatherType[]; + + constructor(weather: WeatherType | WeatherType[]) { + super(); + this.requiredWeather = Array.isArray(weather) ? weather : [weather]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const currentWeather = scene.arena.weather?.weatherType; + if (!isNullOrUndefined(currentWeather) && this.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather!)) { + return false; + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const currentWeather = scene.arena.weather?.weatherType; + let token = ""; + if (!isNullOrUndefined(currentWeather)) { + token = WeatherType[currentWeather!].replace("_", " ").toLocaleLowerCase(); + } + return ["weather", token]; + } +} + +export class PartySizeRequirement extends EncounterSceneRequirement { + partySizeRange: [number, number]; + excludeFainted: boolean; + + /** + * Used for specifying a party size requirement + * If min and max are equivalent, will check for exact size + * @param partySizeRange + * @param excludeFainted + */ + constructor(partySizeRange: [number, number], excludeFainted: boolean) { + super(); + this.partySizeRange = partySizeRange; + this.excludeFainted = excludeFainted; + } + + override meetsRequirement(scene: BattleScene): boolean { + if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { + const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; + if (partySize >= 0 && (this.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { + return false; + } + } + + return true; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["partySize", scene.getParty().length.toString()]; + } +} + +export class PersistentModifierRequirement extends EncounterSceneRequirement { + requiredHeldItemModifiers: string[]; + minNumberOfItems: number; + + constructor(heldItem: string | string[], minNumberOfItems: number = 1) { + super(); + this.minNumberOfItems = minNumberOfItems; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredHeldItemModifiers?.length < 0) { + return false; + } + let modifierCount = 0; + this.requiredHeldItemModifiers.forEach(modifier => { + const matchingMods = scene.findModifiers(m => m.constructor.name === modifier); + if (matchingMods?.length > 0) { + matchingMods.forEach(matchingMod => { + modifierCount += matchingMod.stackCount; + }); + } + }); + + return modifierCount >= this.minNumberOfItems; + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["requiredItem", this.requiredHeldItemModifiers[0]]; + } +} + +export class MoneyRequirement extends EncounterSceneRequirement { + requiredMoney: number; // Static value + scalingMultiplier: number; // Calculates required money based off wave index + + constructor(requiredMoney?: number, scalingMultiplier?: number) { + super(); + this.requiredMoney = requiredMoney ?? 0; + this.scalingMultiplier = scalingMultiplier ?? 0; + } + + override meetsRequirement(scene: BattleScene): boolean { + const money = scene.money; + if (isNullOrUndefined(money)) { + return false; + } + + if (this.scalingMultiplier > 0) { + this.requiredMoney = scene.getWaveMoneyAmount(this.scalingMultiplier); + } + return !(this.requiredMoney > 0 && this.requiredMoney > money); + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const value = this.scalingMultiplier > 0 ? scene.getWaveMoneyAmount(this.scalingMultiplier).toString() : this.requiredMoney.toString(); + return ["money", value]; + } +} + +export class SpeciesRequirement extends EncounterPokemonRequirement { + requiredSpecies: Species[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(species: Species | Species[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredSpecies = Array.isArray(species) ? species : [species]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredSpecies?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed speciess + return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (pokemon?.species.speciesId && this.requiredSpecies.includes(pokemon.species.speciesId)) { + return ["species", Species[pokemon.species.speciesId]]; + } + return ["species", ""]; + } +} + + +export class NatureRequirement extends EncounterPokemonRequirement { + requiredNature: Nature[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(nature: Nature | Nature[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredNature = Array.isArray(nature) ? nature : [nature]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredNature?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed natures + return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (!isNullOrUndefined(pokemon?.nature) && this.requiredNature.includes(pokemon!.nature)) { + return ["nature", Nature[pokemon!.nature]]; + } + return ["nature", ""]; + } +} + +export class TypeRequirement extends EncounterPokemonRequirement { + requiredType: Type[]; + excludeFainted: boolean; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(type: Type | Type[], excludeFainted: boolean = true, minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.excludeFainted = excludeFainted; + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredType = Array.isArray(type) ? type : [type]; + } + + override meetsRequirement(scene: BattleScene): boolean { + let partyPokemon = scene.getParty(); + + if (isNullOrUndefined(partyPokemon)) { + return false; + } + + if (this.excludeFainted) { + partyPokemon = partyPokemon.filter((pokemon) => !pokemon.isFainted()); + } + + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed types + return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedTypes = this.requiredType.filter((ty) => pokemon?.getTypes().includes(ty)); + if (includedTypes.length > 0) { + return ["type", Type[includedTypes[0]]]; + } + return ["type", ""]; + } +} + + +export class MoveRequirement extends EncounterPokemonRequirement { + requiredMoves: Moves[] = []; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(moves: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredMoves = Array.isArray(moves) ? moves : [moves]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length > 0).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move?.moveId === reqMove).length === 0).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedMoves = pokemon?.moveset.filter((move) => move?.moveId && this.requiredMoves.includes(move.moveId)); + if (includedMoves && includedMoves.length > 0 && includedMoves[0]) { + return ["move", includedMoves[0].getName()]; + } + return ["move", ""]; + } + +} + +/** + * Find out if Pokemon in the party are able to learn one of many specific moves by TM. + * NOTE: Egg moves are not included as learnable. + * NOTE: If the Pokemon already knows the move, this requirement will fail, since it's not technically learnable. + */ +export class CompatibleMoveRequirement extends EncounterPokemonRequirement { + requiredMoves: Moves[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(learnableMove: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredMoves = Array.isArray(learnableMove) ? learnableMove : [learnableMove]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m?.moveId === tm)).includes(learnableMove)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed learnableMoves + return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m?.moveId === tm)).includes(learnableMove)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const includedCompatMoves = this.requiredMoves.filter((reqMove) => pokemon?.compatibleTms.filter((tm) => !pokemon.moveset.find(m => m?.moveId === tm)).includes(reqMove)); + if (includedCompatMoves.length > 0) { + return ["compatibleMove", Moves[includedCompatMoves[0]]]; + } + return ["compatibleMove", ""]; + } + +} + +export class AbilityRequirement extends EncounterPokemonRequirement { + requiredAbilities: Abilities[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(abilities: Abilities | Abilities[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredAbilities = Array.isArray(abilities) ? abilities : [abilities]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredAbilities?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredAbilities.some((ability) => pokemon.getAbility().id === ability)); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess + return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (pokemon?.getAbility().id && this.requiredAbilities.some(a => pokemon.getAbility().id === a)) { + return ["ability", pokemon.getAbility().name]; + } + return ["ability", ""]; + } +} + +export class StatusEffectRequirement extends EncounterPokemonRequirement { + requiredStatusEffect: StatusEffect[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredStatusEffect = Array.isArray(statusEffect) ? statusEffect : [statusEffect]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredStatusEffect?.length < 0) { + return false; + } + const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + console.log(x); + return x; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => { + return this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status!.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed StatusEffects + // return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((statusEffect) => pokemon.status?.effect === statusEffect).length === 0); + return partyPokemon.filter((pokemon) => { + return !this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status!.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const reqStatus = this.requiredStatusEffect.filter((a) => { + if (a === StatusEffect.NONE) { + return isNullOrUndefined(pokemon?.status) || isNullOrUndefined(pokemon!.status!.effect) || pokemon!.status!.effect === a; + } + return pokemon!.status?.effect === a; + }); + if (reqStatus.length > 0) { + return ["status", StatusEffect[reqStatus[0]]]; + } + return ["status", ""]; + } + +} + +/** + * Finds if there are pokemon that can form change with a given item. + * Notice that we mean specific items, like Charizardite, not the Mega Bracelet. + * If you want to trigger the event based on the form change enabler, use PersistentModifierRequirement. + */ +export class CanFormChangeWithItemRequirement extends EncounterPokemonRequirement { + requiredFormChangeItem: FormChangeItem[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(formChangeItem: FormChangeItem | FormChangeItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredFormChangeItem = Array.isArray(formChangeItem) ? formChangeItem : [formChangeItem]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredFormChangeItem?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + filterByForm(pokemon, formChangeItem) { + if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId) + // Get all form changes for this species with an item trigger, including any compound triggers + && pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger)) + // Returns true if any form changes match this item + .map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger) + .flat().flatMap(fc => fc.item).includes(formChangeItem)) { + return true; + } else { + return false; + } + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed formChangeItems + return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)); + if (requiredItems.length > 0) { + return ["formChangeItem", FormChangeItem[requiredItems[0]]]; + } + return ["formChangeItem", ""]; + } + +} + +export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { + requiredEvolutionItem: EvolutionItem[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(evolutionItems: EvolutionItem | EvolutionItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredEvolutionItem = Array.isArray(evolutionItems) ? evolutionItems : [evolutionItems]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this.requiredEvolutionItem?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + filterByEvo(pokemon, evolutionItem) { + if (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions[pokemon.species.speciesId].filter(e => e.item === evolutionItem + && (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX)) { + return true; + } else if (pokemon.isFusion() && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.item === evolutionItem + && (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX)) { + return true; + } + return false; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItem) => this.filterByEvo(pokemon, evolutionItem)).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionItemss + return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItems) => this.filterByEvo(pokemon, evolutionItems)).length === 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = this.requiredEvolutionItem.filter((evoItem) => this.filterByEvo(pokemon, evoItem)); + if (requiredItems.length > 0) { + return ["evolutionItem", EvolutionItem[requiredItems[0]]]; + } + return ["evolutionItem", ""]; + } +} + +export class HeldItemRequirement extends EncounterPokemonRequirement { + requiredHeldItemModifiers: string[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredHeldItemModifiers.some((heldItem) => { + return pokemon.getHeldItems().some((it) => { + return it.constructor.name === heldItem; + }); + })); + } else { + // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers + // E.g. functions as a blacklist + return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { + return !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); + }).length > 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter((it) => { + return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", requiredItems[0].type.name]; + } + return ["heldItem", ""]; + } +} + +export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRequirement { + requiredHeldItemTypes: Type[]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(heldItemTypes: Type | Type[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHeldItemTypes = Array.isArray(heldItemTypes) ? heldItemTypes : [heldItemTypes]; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredHeldItemTypes.some((heldItemType) => { + return pokemon.getHeldItems().some((it) => { + return it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType; + }); + })); + } else { + // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers + // E.g. functions as a blacklist + return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { + return !this.requiredHeldItemTypes.some(heldItemType => it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType); + }).length > 0); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter((it) => { + return this.requiredHeldItemTypes.some(heldItemType => it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", requiredItems[0].type.name]; + } + return ["heldItem", ""]; + } +} + +export class LevelRequirement extends EncounterPokemonRequirement { + requiredLevelRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredLevelRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredLevelRange = requiredLevelRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required level range + if (!isNullOrUndefined(this.requiredLevelRange) && this.requiredLevelRange[0] <= this.requiredLevelRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.level >= this.requiredLevelRange[0] && pokemon.level <= this.requiredLevelRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredLevelRanges + return partyPokemon.filter((pokemon) => pokemon.level < this.requiredLevelRange[0] || pokemon.level > this.requiredLevelRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["level", pokemon?.level.toString() ?? ""]; + } +} + +export class FriendshipRequirement extends EncounterPokemonRequirement { + requiredFriendshipRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredFriendshipRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredFriendshipRange = requiredFriendshipRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required friendship range + if (!isNullOrUndefined(this.requiredFriendshipRange) && this.requiredFriendshipRange[0] <= this.requiredFriendshipRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.friendship >= this.requiredFriendshipRange[0] && pokemon.friendship <= this.requiredFriendshipRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredFriendshipRanges + return partyPokemon.filter((pokemon) => pokemon.friendship < this.requiredFriendshipRange[0] || pokemon.friendship > this.requiredFriendshipRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["friendship", pokemon?.friendship.toString() ?? ""]; + } +} + +/** + * .1 -> 10% hp + * .5 -> 50% hp + * 1 -> 100% hp + */ +export class HealthRatioRequirement extends EncounterPokemonRequirement { + requiredHealthRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredHealthRange = requiredHealthRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon's health inside required health range + if (!isNullOrUndefined(this.requiredHealthRange) && this.requiredHealthRange[0] <= this.requiredHealthRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => { + return pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]; + }); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredHealthRanges + return partyPokemon.filter((pokemon) => pokemon.getHpRatio() < this.requiredHealthRange[0] || pokemon.getHpRatio() > this.requiredHealthRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + if (!isNullOrUndefined(pokemon?.getHpRatio())) { + return ["healthRatio", Math.floor(pokemon!.getHpRatio() * 100).toString() + "%"]; + } + return ["healthRatio", ""]; + } +} + +export class WeightRequirement extends EncounterPokemonRequirement { + requiredWeightRange: [number, number]; + minNumberOfPokemon: number; + invertQuery: boolean; + + constructor(requiredWeightRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredWeightRange = requiredWeightRange; + } + + override meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon's weight inside required weight range + if (!isNullOrUndefined(this.requiredWeightRange) && this.requiredWeightRange[0] <= this.requiredWeightRange[1]) { + const partyPokemon = scene.getParty(); + const pokemonInRange = this.queryParty(partyPokemon); + if (pokemonInRange.length < this.minNumberOfPokemon) { + return false; + } + } + return true; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => pokemon.getWeight() >= this.requiredWeightRange[0] && pokemon.getWeight() <= this.requiredWeightRange[1]); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredWeightRanges + return partyPokemon.filter((pokemon) => pokemon.getWeight() < this.requiredWeightRange[0] || pokemon.getWeight() > this.requiredWeightRange[1]); + } + } + + override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["weight", pokemon?.getWeight().toString() ?? ""]; + } +} + + diff --git a/src/data/mystery-encounters/mystery-encounter-save-data.ts b/src/data/mystery-encounters/mystery-encounter-save-data.ts new file mode 100644 index 000000000000..259fbff7b854 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-save-data.ts @@ -0,0 +1,38 @@ +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/data/mystery-encounters/mystery-encounters"; +import { isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; + +export class SeenEncounterData { + type: MysteryEncounterType; + tier: MysteryEncounterTier; + waveIndex: number; + selectedOption: number; + + constructor(type: MysteryEncounterType, tier: MysteryEncounterTier, waveIndex: number, selectedOption?: number) { + this.type = type; + this.tier = tier; + this.waveIndex = waveIndex; + this.selectedOption = selectedOption ?? -1; + } +} + +export interface QueuedEncounter { + type: MysteryEncounterType; + spawnPercent: number; // Out of 100 +} + +export class MysteryEncounterSaveData { + encounteredEvents: SeenEncounterData[] = []; + encounterSpawnChance: number = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + queuedEncounters: QueuedEncounter[] = []; + + constructor(data?: MysteryEncounterSaveData) { + if (!isNullOrUndefined(data)) { + Object.assign(this, data); + } + + this.encounteredEvents = this.encounteredEvents ?? []; + this.queuedEncounters = this.queuedEncounters ?? []; + } +} diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts new file mode 100644 index 000000000000..a204ea848da2 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -0,0 +1,957 @@ +import { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { capitalizeFirstLetter, isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; +import * as Utils from "#app/utils"; +import { StatusEffect } from "../status-effect"; +import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; +import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, HealthRatioRequirement, PartySizeRequirement, StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; +import { BattlerIndex } from "#app/battle"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { GameModes } from "#app/game-mode"; +import { EncounterAnim } from "#enums/encounter-anims"; + +export interface EncounterStartOfBattleEffect { + sourcePokemon?: Pokemon; + sourceBattlerIndex?: BattlerIndex; + targets: BattlerIndex[]; + move: PokemonMove; + ignorePp: boolean; + followUp?: boolean; +} + +/** + * Used by {@linkcode MysteryEncounterBuilder} class to define required/optional properties on the {@linkcode MysteryEncounter} class when building. + * + * Should ONLY contain properties that are necessary for {@linkcode MysteryEncounter} construction. + * Post-construct and flag data properties are defined in the {@linkcode MysteryEncounter} class itself. + */ +export interface IMysteryEncounter { + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + encounterTier: MysteryEncounterTier; + encounterAnimations?: EncounterAnim[]; + disabledGameModes?: GameModes[]; + hideBattleIntroMessage: boolean; + autoHideIntroVisuals: boolean; + enterIntroVisualsFromRight: boolean; + catchAllowed: boolean; + continuousEncounter: boolean; + maxAllowedEncounters: number; + hasBattleAnimationsWithoutTargets: boolean; + skipEnemyBattleTurns: boolean; + skipToFightInput: boolean; + + onInit?: (scene: BattleScene) => boolean; + onVisualsStart?: (scene: BattleScene) => boolean; + doEncounterExp?: (scene: BattleScene) => boolean; + doEncounterRewards?: (scene: BattleScene) => boolean; + doContinueEncounter?: (scene: BattleScene) => Promise; + + requirements: EncounterSceneRequirement[]; + primaryPokemonRequirements: EncounterPokemonRequirement[]; + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSupportRequirements: boolean; + + dialogue: MysteryEncounterDialogue; + enemyPartyConfigs: EnemyPartyConfig[]; + + dialogueTokens: Record; + expMultiplier: number; +} + +/** + * MysteryEncounter class that defines the logic for a single encounter + * These objects will be saved as part of session data any time the player is on a floor with an encounter + * Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class + */ +export default class MysteryEncounter implements IMysteryEncounter { + // #region Required params + + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + + // #region Optional params + + encounterTier: MysteryEncounterTier; + /** + * Custom battle animations that are configured for encounter effects and visuals + * Specify here so that assets are loaded on initialization of encounter + */ + encounterAnimations?: EncounterAnim[]; + /** + * If specified, defines any game modes where the {@linkcode MysteryEncounter} should *NOT* spawn + */ + disabledGameModes?: GameModes[]; + /** + * If true, hides "A Wild X Appeared" etc. messages + * Default true + */ + hideBattleIntroMessage: boolean; + /** + * If true, when an option is selected the field visuals will fade out automatically + * Default false + */ + autoHideIntroVisuals: boolean; + /** + * Intro visuals on the field will slide in from the right instead of the left + * Default false + */ + enterIntroVisualsFromRight: boolean; + /** + * If true, allows catching a wild pokemon during the encounter + * Default false + */ + catchAllowed: 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 + * Default false + */ + continuousEncounter: boolean; + /** + * Maximum number of times the encounter can be seen per run + * Rogue tier encounters default to 1, others default to 3 + */ + maxAllowedEncounters: number; + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + */ + hasBattleAnimationsWithoutTargets: boolean; + /** + * If true, will skip enemy pokemon turns during battle for the encounter + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + */ + skipEnemyBattleTurns: boolean; + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + */ + skipToFightInput: boolean; + + // #region Event callback functions + + /** Event when Encounter is first loaded, use it for data conditioning */ + onInit?: (scene: BattleScene) => boolean; + /** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */ + onVisualsStart?: (scene: BattleScene) => boolean; + /** Event triggered prior to {@linkcode CommandPhase}, during {@linkcode TurnInitPhase} */ + onTurnStart?: (scene: BattleScene) => boolean; + /** Event prior to any rewards logic in {@linkcode MysteryEncounterRewardsPhase} */ + onRewards?: (scene: BattleScene) => Promise; + /** Will provide the player party EXP before rewards are displayed for that wave */ + doEncounterExp?: (scene: BattleScene) => boolean; + /** Will provide the player a rewards shop for that wave */ + doEncounterRewards?: (scene: BattleScene) => boolean; + /** Will execute callback during VictoryPhase of a continuousEncounter */ + doContinueEncounter?: (scene: BattleScene) => Promise; + + /** + * Requirements + */ + requirements: EncounterSceneRequirement[]; + /** Primary Pokemon is a single pokemon randomly selected from the party that meet ALL primary pokemon requirements */ + primaryPokemonRequirements: EncounterPokemonRequirement[]; + /** + * Secondary Pokemon are pokemon that meet ALL secondary pokemon requirements + * Note that an individual requirement may require multiple pokemon, but the resulting pokemon after all secondary requirements are met may be lower than expected + * If the primary pokemon and secondary pokemon are the same and ExcludePrimaryFromSupportRequirements flag is true, primary pokemon may be promoted from secondary pool + */ + secondaryPokemonRequirements: EncounterPokemonRequirement[]; + excludePrimaryFromSupportRequirements: boolean; + primaryPokemon?: PlayerPokemon; + secondaryPokemon?: PlayerPokemon[]; + + // #region Post-construct / Auto-populated params + + /** + * Dialogue object containing all the dialogue, messages, tooltips, etc. for an encounter + */ + dialogue: MysteryEncounterDialogue; + /** + * Data used for setting up/initializing enemy party in battles + * Can store multiple configs so that one can be chosen based on option selected + * Should usually be defined in `onInit()` or `onPreOptionPhase()` + */ + enemyPartyConfigs: EnemyPartyConfig[]; + /** + * Object instance containing sprite data for an encounter when it is being spawned + * Otherwise, will be undefined + * You probably shouldn't do anything directly with this unless you have a very specific need + */ + introVisuals?: MysteryEncounterIntroVisuals; + + // #region Flags + + /** + * Can be set for uses programatic dialogue during an encounter (storing the name of one of the party's pokemon, etc.) + * Example use: see MYSTERIOUS_CHEST + */ + dialogueTokens: Record; + /** + * Should be set depending upon option selected as part of an encounter + * For example, if there is no battle as part of the encounter/selected option, should be set to NO_BATTLE + * Defaults to DEFAULT + */ + encounterMode: MysteryEncounterMode; + /** + * Flag for checking if it's the first time a shop is being shown for an encounter. + * Defaults to true so that the first shop does not override the specified rewards. + * Will be set to false after a shop is shown (so can't reroll same rarity items for free) + */ + lockEncounterRewardTiers: boolean; + /** + * Will be set automatically, indicates special moves in startOfBattleEffects are complete (so will not repeat) + */ + startOfBattleEffectsComplete: boolean; + /** + * Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases + */ + selectedOption?: MysteryEncounterOption; + /** + * Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases + */ + startOfBattleEffects: EncounterStartOfBattleEffect[] = []; + /** + * Can be set higher or lower based on the type of battle or exp gained for an option/encounter + * Defaults to 1 + */ + expMultiplier: number; + /** + * Can add any asset load promises here during onInit() to make sure the scene awaits the loads properly + */ + loadAssets: Promise[]; + /** + * Generic property to set any custom data required for the encounter + * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase + */ + misc?: any; + /** + * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave + * You should only need to interact via getter/update methods + */ + private seedOffset?: any; + + constructor(encounter: IMysteryEncounter | null) { + if (!isNullOrUndefined(encounter)) { + Object.assign(this, encounter); + } + 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; + this.encounterMode = MysteryEncounterMode.DEFAULT; + this.requirements = this.requirements ? this.requirements : []; + this.hideBattleIntroMessage = this.hideBattleIntroMessage ?? false; + this.autoHideIntroVisuals = this.autoHideIntroVisuals ?? true; + this.enterIntroVisualsFromRight = this.enterIntroVisualsFromRight ?? false; + this.continuousEncounter = this.continuousEncounter ?? false; + + // Reset any dirty flags or encounter data + this.startOfBattleEffectsComplete = false; + this.lockEncounterRewardTiers = true; + this.dialogueTokens = {}; + this.enemyPartyConfigs = []; + this.startOfBattleEffects = []; + this.introVisuals = undefined; + this.misc = null; + this.expMultiplier = 1; + this.loadAssets = []; + } + + /** + * Checks if the current scene state meets the requirements for the {@linkcode MysteryEncounter} to spawn + * This is used to filter the pool of encounters down to only the ones with all requirements met + * @param scene + * @returns + */ + meetsRequirements(scene: BattleScene): boolean { + const sceneReq = !this.requirements.some(requirement => !requirement.meetsRequirement(scene)); + const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary + const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); + + return sceneReq && secReqs && priReqs; + } + + /** + * Checks if a specific player pokemon meets all given primary EncounterPokemonRequirements + * Used automatically as part of {@linkcode meetsRequirements}, but can also be used to manually check certain Pokemon where needed + * @param scene + * @param pokemon + */ + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon): boolean { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + /** + * Returns true if all PRIMARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode primaryPokemon}. + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + private meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + if (!this.primaryPokemonRequirements || this.primaryPokemonRequirements.length === 0) { + const activeMon = scene.getParty().filter(p => p.isActive(true)); + if (activeMon.length > 0) { + this.primaryPokemon = activeMon[0]; + } else { + this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0]; + } + return true; + } + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.primaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + } else { + this.primaryPokemon = undefined; + return false; + } + } + + if (qualified.length === 0) { + return false; + } + + if (this.excludePrimaryFromSupportRequirements && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + const truePrimaryPool: PlayerPokemon[] = []; + const overlap: PlayerPokemon[] = []; + for (const qp of qualified) { + if (!this.secondaryPokemon.includes(qp)) { + truePrimaryPool.push(qp); + } else { + overlap.push(qp); + } + + } + if (truePrimaryPool.length > 0) { + // Always choose from the non-overlapping pokemon first + this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)]; + return true; + } else { + // If there are multiple overlapping pokemon, we're okay - just choose one and take it out of the primary pokemon pool + if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) { + // is this working? + this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)]; + this.secondaryPokemon = this.secondaryPokemon.filter((supp) => supp !== this.primaryPokemon); + return true; + } + console.log("Mystery Encounter Edge Case: Requirement not met due to primary pokemon overlapping with secondary pokemon. There's no valid primary pokemon left."); + return false; + } + } else { + // this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly. + this.primaryPokemon = qualified[Utils.randSeedInt(qualified.length, 0)]; + return true; + } + } + + /** + * Returns true if all SECONDARY {@linkcode EncounterRequirement}s for the option are met, + * AND there is a valid Pokemon assigned to {@linkcode secondaryPokemon} (if applicable). + * If both {@linkcode primaryPokemonRequirements} and {@linkcode secondaryPokemonRequirements} are defined, + * can cause scenarios where there are not enough Pokemon that are sufficient for all requirements. + * @param scene + */ + private meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { + if (!this.secondaryPokemonRequirements || this.secondaryPokemonRequirements.length === 0) { + this.secondaryPokemon = []; + return true; + } + + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.secondaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn)); + } else { + this.secondaryPokemon = []; + return false; + } + } + this.secondaryPokemon = qualified; + return true; + } + + /** + * Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs + * @param scene + */ + initIntroVisuals(scene: BattleScene): void { + this.introVisuals = new MysteryEncounterIntroVisuals(scene, this); + } + + /** + * Auto-pushes dialogue tokens from the encounter (and option) requirements. + * Will use the first support pokemon in list + * For multiple support pokemon in the dialogue token, it will have to be overridden. + */ + populateDialogueTokensFromRequirements(scene: BattleScene): void { + this.meetsRequirements(scene); + if (this.requirements?.length > 0) { + for (const req of this.requirements) { + const dialogueToken = req.getDialogueToken(scene); + if (dialogueToken?.length === 2) { + this.setDialogueToken(...dialogueToken); + } + } + } + if (this.primaryPokemon && this.primaryPokemon.length > 0) { + this.setDialogueToken("primaryName", this.primaryPokemon.getNameToRender()); + for (const req of this.primaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, this.primaryPokemon); + if (value?.length === 2) { + this.setDialogueToken("primary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon && this.secondaryPokemon.length > 0) { + this.setDialogueToken("secondaryName", this.secondaryPokemon[0].getNameToRender()); + for (const req of this.secondaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, this.secondaryPokemon[0]); + if (value?.length === 2) { + this.setDialogueToken("primary" + capitalizeFirstLetter(value[0]), value[1]); + } + this.setDialogueToken("secondary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + + // Dialogue tokens for options + for (let i = 0; i < this.options.length; i++) { + const opt = this.options[i]; + opt.meetsRequirements(scene); + const j = i + 1; + if (opt.requirements.length > 0) { + for (const req of opt.requirements) { + const dialogueToken = req.getDialogueToken(scene); + if (dialogueToken?.length === 2) { + this.setDialogueToken("option" + j + capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]); + } + } + } + if (opt.primaryPokemonRequirements.length > 0 && opt.primaryPokemon) { + this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.getNameToRender()); + for (const req of opt.primaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, opt.primaryPokemon); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Primary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon && opt.secondaryPokemon.length > 0) { + this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].getNameToRender()); + for (const req of opt.secondaryPokemonRequirements) { + if (!req.invertQuery) { + const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Secondary" + capitalizeFirstLetter(value[0]), value[1]); + } + } + } + } + } + } + + /** + * Used to cache a dialogue token for the encounter. + * Tokens will be auto-injected via the `{{key}}` pattern with `value`, + * when using the {@linkcode showEncounterText} and {@linkcode showEncounterDialogue} helper functions. + * + * @param key + * @param value + */ + setDialogueToken(key: string, value: string): void { + this.dialogueTokens[key] = value; + } + + /** + * If an encounter uses {@linkcode MysteryEncounterMode.continuousEncounter}, + * should rely on this value for seed offset instead of wave index. + * + * This offset is incremented for each new {@linkcode MysteryEncounterPhase} that occurs, + * so multi-encounter RNG will be consistent on resets and not be affected by number of turns, move RNG, etc. + */ + getSeedOffset() { + return this.seedOffset; + } + + /** + * Maintains seed offset for RNG consistency + * Increments if the same {@linkcode MysteryEncounter} has multiple option select cycles + * @param scene + */ + updateSeedOffset(scene: BattleScene) { + const currentOffset = this.seedOffset ?? scene.currentBattle.waveIndex * 1000; + this.seedOffset = currentOffset + 512; + } +} + +/** + * Builder class for creating a MysteryEncounter + * must call `build()` at the end after specifying all params for the MysteryEncounter + */ +export class MysteryEncounterBuilder implements Partial { + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + enemyPartyConfigs: EnemyPartyConfig[] = []; + + dialogue: MysteryEncounterDialogue = {}; + requirements: EncounterSceneRequirement[] = []; + primaryPokemonRequirements: EncounterPokemonRequirement[] = []; + secondaryPokemonRequirements: EncounterPokemonRequirement[] = []; + excludePrimaryFromSupportRequirements: boolean = true; + dialogueTokens: Record = {}; + + hideBattleIntroMessage: boolean = false; + autoHideIntroVisuals: boolean = true; + enterIntroVisualsFromRight: boolean = false; + continuousEncounter: boolean = false; + catchAllowed: boolean = false; + lockEncounterRewardTiers: boolean = false; + startOfBattleEffectsComplete: boolean = false; + hasBattleAnimationsWithoutTargets: boolean = false; + skipEnemyBattleTurns: boolean = false; + skipToFightInput: boolean = false; + maxAllowedEncounters: number = 3; + expMultiplier: number = 1; + + /** + * REQUIRED + */ + + /** + * @statif Defines the type of encounter which is used as an identifier, should be tied to a unique MysteryEncounterType + * NOTE: if new functions are added to {@linkcode MysteryEncounter} class + * @param encounterType + * @returns this + */ + static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick { + return Object.assign(new MysteryEncounterBuilder(), { encounterType }); + } + + /** + * Defines an option for the encounter. + * Use for complex options. + * There should be at least 2 options defined and no more than 4. + * + * @param option MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance + * @returns + */ + withOption(option: MysteryEncounterOption): this & Pick { + if (!this.options) { + const options = [option]; + return Object.assign(this, { options }); + } else { + this.options.push(option); + return this; + } + } + + /** + * Defines an option + phasefor the encounter. + * Use for easy/streamlined options. + * 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} + * @returns + */ + withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { + return this.withOption(MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); + } + + /** + * Defines an option + phasefor the encounter. + * Use for easy/streamlined options. + * 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} + * @returns + */ + withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { + return this.withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue(dialogue) + .withOptionPhase(callback).build()); + } + + /** + * Defines the sprites that will be shown on the enemy field when the encounter spawns + * Can be one or more sprites, recommended not to exceed 4 + * @param spriteConfigs + * @returns + */ + withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick { + return Object.assign(this, { spriteConfigs: spriteConfigs }); + } + + withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []): this { + this.dialogue = {...this.dialogue, intro: dialogue }; + return this; + } + + withIntro({spriteConfigs, dialogue} : {spriteConfigs: MysteryEncounterSpriteConfig[], dialogue?: MysteryEncounterDialogue["intro"]}) { + return this.withIntroSpriteConfigs(spriteConfigs).withIntroDialogue(dialogue); + } + + /** + * OPTIONAL + */ + + /** + * Sets the rarity tier for an encounter + * If not specified, defaults to COMMON + * Tiers are: + * COMMON 32/64 odds + * GREAT 16/64 odds + * ULTRA 10/64 odds + * ROGUE 6/64 odds + * ULTRA_RARE Not currently used + * @param encounterTier + * @returns + */ + withEncounterTier(encounterTier: MysteryEncounterTier): this & Pick { + return Object.assign(this, { encounterTier: encounterTier }); + } + + /** + * Defines any EncounterAnim animations that are intended to be used during the encounter + * EncounterAnims are custom battle animations (think Ice Beam) that can be played at any point during an encounter or callback + * They just need to be specified here so that resources are loaded on encounter init + * @param encounterAnimations + * @returns + */ + withAnimations(...encounterAnimations: EncounterAnim[]): this & Required> { + const animations = Array.isArray(encounterAnimations) ? encounterAnimations : [encounterAnimations]; + return Object.assign(this, { encounterAnimations: animations }); + } + + /** + * Defines any game modes where the Mystery Encounter should *NOT* spawn + * @returns + * @param disabledGameModes + */ + withDisabledGameModes(...disabledGameModes: GameModes[]): this & Required> { + const gameModes = Array.isArray(disabledGameModes) ? disabledGameModes : [disabledGameModes]; + return Object.assign(this, { disabledGameModes: gameModes }); + } + + /** + * 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 + * Default false + * @param continuousEncounter + */ + withContinuousEncounter(continuousEncounter: boolean): this & Required> { + return Object.assign(this, { continuousEncounter: continuousEncounter }); + } + + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + * Default false + * @param hasBattleAnimationsWithoutTargets + */ + withBattleAnimationsWithoutTargets(hasBattleAnimationsWithoutTargets: boolean): this & Required> { + return Object.assign(this, { hasBattleAnimationsWithoutTargets }); + } + + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@linkcode FunAndGamesEncounter} for an example) + * Default false + * @param skipEnemyBattleTurns + */ + withSkipEnemyBattleTurns(skipEnemyBattleTurns: boolean): this & Required> { + return Object.assign(this, { skipEnemyBattleTurns }); + } + + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + * Default false + * @param skipToFightInput + */ + withSkipToFightInput(skipToFightInput: boolean): this & Required> { + return Object.assign(this, { skipToFightInput }); + } + + /** + * Sets the maximum number of times that an encounter can spawn in a given Classic run + * @param maxAllowedEncounters + * @returns + */ + withMaxAllowedEncounters(maxAllowedEncounters: number): this & Required> { + return Object.assign(this, { maxAllowedEncounters: maxAllowedEncounters }); + } + + /** + * Specifies a requirement for an encounter + * For example, passing requirement as "new WaveCountRequirement([2, 180])" would create a requirement that the encounter can only be spawned between waves 2 and 180 + * Existing Requirement objects are defined in mystery-encounter-requirements.ts, and more can always be created to meet a requirement need + * @param requirement + * @returns + */ + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { + if (requirement instanceof EncounterPokemonRequirement) { + Error("Incorrectly added pokemon requirement as scene requirement."); + } + this.requirements.push(requirement); + return this; + } + + /** + * Specifies a wave range requirement for an encounter. + * + * @param min min wave (or exact wave if only min is given) + * @param max optional max wave. If not given, defaults to min => exact wave + * @returns + */ + withSceneWaveRangeRequirement(min: number, max?: number): this & Required> { + return this.withSceneRequirement(new WaveRangeRequirement([min, max ?? min])); + } + + /** + * Specifies a party size requirement for an encounter. + * + * @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 + * @returns + */ + withScenePartySizeRequirement(min: number, max?: number, excludeFainted: boolean = false): this & Required> { + return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeFainted)); + } + + /** + * Add a primary pokemon requirement + * + * @param requirement {@linkcode EncounterPokemonRequirement} + * @returns + */ + withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.primaryPokemonRequirements.push(requirement); + return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); + } + + /** + * Add a primary pokemon status effect requirement + * + * @param statusEffect the status effect/s to check + * @param minNumberOfPokemon minimum number of pokemon to have the effect + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonStatusEffectRequirement(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + return this.withPrimaryPokemonRequirement(new StatusEffectRequirement(statusEffect, minNumberOfPokemon, invertQuery)); + } + + /** + * Add a primary pokemon health ratio requirement + * + * @param requiredHealthRange the health range to check + * @param minNumberOfPokemon minimum number of pokemon to have the health range + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonHealthRatioRequirement(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + return this.withPrimaryPokemonRequirement(new HealthRatioRequirement(requiredHealthRange, minNumberOfPokemon, invertQuery)); + } + + // TODO: Maybe add an optional parameter for excluding primary pokemon from the support cast? + // ex. if your only grass type pokemon, a snivy, is chosen as primary, if the support pokemon requires a grass type, the event won't trigger because + // it's already been + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + + this.secondaryPokemonRequirements.push(requirement); + this.excludePrimaryFromSupportRequirements = excludePrimaryFromSecondaryRequirements; + return Object.assign(this, { excludePrimaryFromSecondaryRequirements: this.excludePrimaryFromSupportRequirements, secondaryPokemonRequirements: this.secondaryPokemonRequirements }); + } + + /** + * Can set custom encounter rewards via this callback function + * If rewards are always deterministic for an encounter, this is a good way to set them + * + * 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 + * @returns + */ + withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { doEncounterRewards: doEncounterRewards }); + } + + /** + * Can set custom encounter exp via this callback function + * If exp always deterministic for an encounter, this is a good way to set them + * + * 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 + * @returns + */ + withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { doEncounterExp: doEncounterExp }); + } + + /** + * 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 + * @returns + */ + withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { onInit }); + } + + /** + * 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 + * @returns + */ + withOnVisualsStart(onVisualsStart: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { onVisualsStart: onVisualsStart }); + } + + /** + * 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 + * @returns + */ + withCatchAllowed(catchAllowed: boolean): this & Required> { + return Object.assign(this, { catchAllowed: catchAllowed }); + } + + /** + * @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter + * @returns + */ + withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { + return Object.assign(this, { hideBattleIntroMessage: hideBattleIntroMessage }); + } + + /** + * @param autoHideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter + * @returns + */ + withAutoHideIntroVisuals(autoHideIntroVisuals: boolean): this & Required> { + return Object.assign(this, { autoHideIntroVisuals: autoHideIntroVisuals }); + } + + /** + * @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 + */ + withEnterIntroVisualsFromRight(enterIntroVisualsFromRight: boolean): this & Required> { + return Object.assign(this, { enterIntroVisualsFromRight: enterIntroVisualsFromRight }); + } + + /** + * Add a title for the encounter + * + * @param title - title of the encounter + * @returns + */ + withTitle(title: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + title, + } + }; + + return this; + } + + /** + * Add a description of the encounter + * + * @param description - description of the encounter + * @returns + */ + withDescription(description: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + description, + } + }; + + return this; + } + + /** + * Add a query for the encounter + * + * @param query - query to use for the encounter + * @returns + */ + withQuery(query: string): this { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + query, + } + }; + + return this; + } + + /** + * Add outro dialogue/s for the encounter + * + * @param dialogue - outro dialogue/s + * @returns + */ + withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this { + this.dialogue = {...this.dialogue, outro: dialogue }; + return this; + } + + /** + * Builds the mystery encounter + * + * @returns + */ + build(this: IMysteryEncounter): MysteryEncounter { + return new MysteryEncounter(this); + } +} diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts new file mode 100644 index 000000000000..d235ff868618 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -0,0 +1,368 @@ +import { Biome } from "#enums/biome"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { DarkDealEncounter } from "./encounters/dark-deal-encounter"; +import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter"; +import { FieldTripEncounter } from "./encounters/field-trip-encounter"; +import { FightOrFlightEncounter } from "./encounters/fight-or-flight-encounter"; +import { LostAtSeaEncounter } from "./encounters/lost-at-sea-encounter"; +import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers-encounter"; +import { MysteriousChestEncounter } from "./encounters/mysterious-chest-encounter"; +import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter"; +import { SlumberingSnorlaxEncounter } from "./encounters/slumbering-snorlax-encounter"; +import { TrainingSessionEncounter } from "./encounters/training-session-encounter"; +import MysteryEncounter from "./mystery-encounter"; +import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter"; +import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; +import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter"; +import { AnOfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; +import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; +import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; +import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter"; +import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter"; +import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; +import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; +import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter"; +import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; +import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; +import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; +import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; + +/** + * Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT + */ +export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 3; +/** + * The divisor for determining ME spawns, defines the "maximum" weight required for a spawn + * If spawn_weight === MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT, 100% chance to spawn a ME + */ +export const MYSTERY_ENCOUNTER_SPAWN_MAX_WEIGHT = 256; +/** + * When an ME spawn roll fails, WEIGHT_INCREMENT_ON_SPAWN_MISS is added to future rolls for ME spawn checks. + * These values are cleared whenever the next ME spawns, and spawn weight returns to BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + */ +export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 3; +/** + * Specifies the target average for total ME spawns in a single Classic run. + * Used by anti-variance mechanic to check whether a run is above or below the target on a given wave. + */ +export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 12; +/** + * Will increase/decrease the chance of spawning a ME based on the current run's total MEs encountered vs AVERAGE_ENCOUNTERS_PER_RUN_TARGET + * Example: + * AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 17 (expects avg 1 ME every 10 floors) + * ANTI_VARIANCE_WEIGHT_MODIFIER = 15 + * + * On wave 20, if 1 ME has been encountered, the difference from expected average is 0 MEs. + * So anti-variance adds 0/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 0 MEs have been encountered, the difference from expected average is 1 ME. + * So anti-variance adds 15/256 to the spawn weight check for ME spawn. + * + * On wave 20, if 2 MEs have been encountered, the difference from expected average is -1 ME. + * So anti-variance adds -15/256 to the spawn weight check for ME spawn. + */ +export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; + +export const EXTREME_ENCOUNTER_BIOMES = [ + Biome.SEA, + Biome.SEABED, + Biome.BADLANDS, + Biome.DESERT, + Biome.ICE_CAVE, + Biome.VOLCANO, + Biome.WASTELAND, + Biome.ABYSS, + Biome.SPACE, + Biome.END +]; + +export const NON_EXTREME_ENCOUNTER_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.FOREST, + Biome.SWAMP, + Biome.BEACH, + Biome.LAKE, + Biome.MOUNTAIN, + Biome.CAVE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.RUINS, + Biome.CONSTRUCTION_SITE, + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.TEMPLE, + Biome.SLUM, + Biome.SNOWY_FOREST, + Biome.ISLAND, + Biome.LABORATORY +]; + +/** + * Places where you could very reasonably expect to encounter a single human + * + * Diff from NON_EXTREME_ENCOUNTER_BIOMES: + * + BADLANDS + * + DESERT + * + ICE_CAVE + */ +export const HUMAN_TRANSITABLE_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.FOREST, + Biome.SWAMP, + Biome.BEACH, + Biome.LAKE, + Biome.MOUNTAIN, + Biome.BADLANDS, + Biome.CAVE, + Biome.DESERT, + Biome.ICE_CAVE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.RUINS, + Biome.CONSTRUCTION_SITE, + Biome.JUNGLE, + Biome.FAIRY_CAVE, + Biome.TEMPLE, + Biome.SLUM, + Biome.SNOWY_FOREST, + Biome.ISLAND, + Biome.LABORATORY +]; + +/** + * Places where you could expect a town or city, some form of large civilization + */ +export const CIVILIZATION_ENCOUNTER_BIOMES = [ + Biome.TOWN, + Biome.PLAINS, + Biome.GRASS, + Biome.TALL_GRASS, + Biome.METROPOLIS, + Biome.BEACH, + Biome.LAKE, + Biome.MEADOW, + Biome.POWER_PLANT, + Biome.GRAVEYARD, + Biome.DOJO, + Biome.FACTORY, + Biome.CONSTRUCTION_SITE, + Biome.SLUM, + Biome.ISLAND +]; + +export const allMysteryEncounters: { [encounterType: number]: MysteryEncounter } = {}; + + +const extremeBiomeEncounters: MysteryEncounterType[] = []; + +const nonExtremeBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.FIELD_TRIP, + MysteryEncounterType.DANCING_LESSONS, // Is also in BADLANDS, DESERT, VOLCANO, WASTELAND, ABYSS +]; + +const humanTransitableBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.MYSTERIOUS_CHALLENGERS, + MysteryEncounterType.SHADY_VITAMIN_DEALER, + MysteryEncounterType.THE_POKEMON_SALESMAN, + MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, + MysteryEncounterType.THE_WINSTRATE_CHALLENGE +]; + +const civilizationBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.DEPARTMENT_STORE_SALE, + MysteryEncounterType.PART_TIMER, + MysteryEncounterType.FUN_AND_GAMES, + MysteryEncounterType.GLOBAL_TRADE_SYSTEM +]; + +/** + * To add an encounter to every biome possible, use this array + */ +const anyBiomeEncounters: MysteryEncounterType[] = [ + MysteryEncounterType.FIGHT_OR_FLIGHT, + MysteryEncounterType.DARK_DEAL, + MysteryEncounterType.MYSTERIOUS_CHEST, + MysteryEncounterType.TRAINING_SESSION, + MysteryEncounterType.DELIBIRDY, + MysteryEncounterType.A_TRAINERS_TEST, + MysteryEncounterType.TRASH_TO_TREASURE, + MysteryEncounterType.BERRIES_ABOUND, + MysteryEncounterType.CLOWNING_AROUND, + MysteryEncounterType.WEIRD_DREAM, + MysteryEncounterType.TELEPORTING_HIJINKS, + MysteryEncounterType.BUG_TYPE_SUPERFAN, + MysteryEncounterType.UNCOMMON_BREED +]; + +/** + * ENCOUNTER BIOME MAPPING + * To add an Encounter to a biome group, instead of cluttering the map, use the biome group arrays above + * + * Adding specific Encounters to the mysteryEncountersByBiome map is for specific cases and special circumstances + * that biome groups do not cover + */ +export const mysteryEncountersByBiome = new Map([ + [Biome.TOWN, []], + [Biome.PLAINS, [ + MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.GRASS, [ + MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.TALL_GRASS, [ + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.METROPOLIS, []], + [Biome.FOREST, [ + MysteryEncounterType.SAFARI_ZONE, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + + [Biome.SEA, [ + MysteryEncounterType.LOST_AT_SEA + ]], + [Biome.SWAMP, [ + MysteryEncounterType.SAFARI_ZONE + ]], + [Biome.BEACH, []], + [Biome.LAKE, []], + [Biome.SEABED, []], + [Biome.MOUNTAIN, []], + [Biome.BADLANDS, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.CAVE, [ + MysteryEncounterType.THE_STRONG_STUFF + ]], + [Biome.DESERT, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.ICE_CAVE, []], + [Biome.MEADOW, []], + [Biome.POWER_PLANT, []], + [Biome.VOLCANO, [ + MysteryEncounterType.FIERY_FALLOUT, + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.GRAVEYARD, []], + [Biome.DOJO, []], + [Biome.FACTORY, []], + [Biome.RUINS, []], + [Biome.WASTELAND, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.ABYSS, [ + MysteryEncounterType.DANCING_LESSONS + ]], + [Biome.SPACE, []], + [Biome.CONSTRUCTION_SITE, []], + [Biome.JUNGLE, [ + MysteryEncounterType.SAFARI_ZONE + ]], + [Biome.FAIRY_CAVE, []], + [Biome.TEMPLE, []], + [Biome.SLUM, []], + [Biome.SNOWY_FOREST, []], + [Biome.ISLAND, []], + [Biome.LABORATORY, []] +]); + +export function initMysteryEncounters() { + allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS] = MysteriousChallengersEncounter; + allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHEST] = MysteriousChestEncounter; + allMysteryEncounters[MysteryEncounterType.DARK_DEAL] = DarkDealEncounter; + allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter; + allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter; + allMysteryEncounters[MysteryEncounterType.SLUMBERING_SNORLAX] = SlumberingSnorlaxEncounter; + allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; + allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; + allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter; + allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter; + allMysteryEncounters[MysteryEncounterType.LOST_AT_SEA] = LostAtSeaEncounter; + allMysteryEncounters[MysteryEncounterType.FIERY_FALLOUT] = FieryFalloutEncounter; + allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter; + allMysteryEncounters[MysteryEncounterType.THE_POKEMON_SALESMAN] = ThePokemonSalesmanEncounter; + allMysteryEncounters[MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE] = AnOfferYouCantRefuseEncounter; + allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter; + allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter; + allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter; + allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter; + allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter; + allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter; + allMysteryEncounters[MysteryEncounterType.PART_TIMER] = PartTimerEncounter; + allMysteryEncounters[MysteryEncounterType.DANCING_LESSONS] = DancingLessonsEncounter; + allMysteryEncounters[MysteryEncounterType.WEIRD_DREAM] = WeirdDreamEncounter; + allMysteryEncounters[MysteryEncounterType.THE_WINSTRATE_CHALLENGE] = TheWinstrateChallengeEncounter; + allMysteryEncounters[MysteryEncounterType.TELEPORTING_HIJINKS] = TeleportingHijinksEncounter; + allMysteryEncounters[MysteryEncounterType.BUG_TYPE_SUPERFAN] = BugTypeSuperfanEncounter; + allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; + allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter; + allMysteryEncounters[MysteryEncounterType.GLOBAL_TRADE_SYSTEM] = GlobalTradeSystemEncounter; + + // Add extreme encounters to biome map + extremeBiomeEncounters.forEach(encounter => { + EXTREME_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add non-extreme encounters to biome map + nonExtremeBiomeEncounters.forEach(encounter => { + NON_EXTREME_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add human encounters to biome map + humanTransitableBiomeEncounters.forEach(encounter => { + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + // Add civilization encounters to biome map + civilizationBiomeEncounters.forEach(encounter => { + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + const encountersForBiome = mysteryEncountersByBiome.get(biome); + if (encountersForBiome && !encountersForBiome.includes(encounter)) { + encountersForBiome.push(encounter); + } + }); + }); + + // Add ANY biome encounters to biome map + mysteryEncountersByBiome.forEach(biomeEncounters => { + anyBiomeEncounters.forEach(encounter => { + if (!biomeEncounters.includes(encounter)) { + biomeEncounters.push(encounter); + } + }); + }); +} diff --git a/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts new file mode 100644 index 000000000000..bbdf2ee5a4d4 --- /dev/null +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -0,0 +1,91 @@ +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"; + +/** + * {@linkcode CanLearnMoveRequirement} options + */ +export interface CanLearnMoveRequirementOptions { + excludeLevelMoves?: boolean; + excludeTmMoves?: boolean; + excludeEggMoves?: boolean; + includeFainted?: boolean; + minNumberOfPokemon?: number; + invertQuery?: boolean; +} + +/** + * Requires that a pokemon can learn a specific move/moveset. + */ +export class CanLearnMoveRequirement extends EncounterPokemonRequirement { + private readonly requiredMoves: Moves[]; + private readonly excludeLevelMoves?: boolean; + private readonly excludeTmMoves?: boolean; + private readonly excludeEggMoves?: boolean; + private readonly includeFainted?: boolean; + + constructor(requiredMoves: Moves | Moves[], options: CanLearnMoveRequirementOptions = {}) { + super(); + this.requiredMoves = Array.isArray(requiredMoves) ? requiredMoves : [requiredMoves]; + + this.excludeLevelMoves = options.excludeLevelMoves ?? false; + this.excludeTmMoves = options.excludeTmMoves ?? false; + this.excludeEggMoves = options.excludeEggMoves ?? false; + this.includeFainted = options.includeFainted ?? false; + this.minNumberOfPokemon = options.minNumberOfPokemon ?? 1; + this.invertQuery = options.invertQuery ?? false; + } + + override meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty().filter((pkm) => (this.includeFainted ? pkm.isAllowed() : pkm.isAllowedInBattle())); + + if (isNullOrUndefined(partyPokemon) || this.requiredMoves?.length < 0) { + return false; + } + + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => + // every required move should be included + this.requiredMoves.every((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove)) + ); + } else { + return partyPokemon.filter( + (pokemon) => + // none of the "required" moves should be included + !this.requiredMoves.some((requiredMove) => this.getAllPokemonMoves(pokemon).includes(requiredMove)) + ); + } + } + + override getDialogueToken(_scene: BattleScene, _pokemon?: PlayerPokemon): [string, string] { + return ["requiredMoves", this.requiredMoves.map(m => new PokemonMove(m).getName()).join(", ")]; + } + + private getPokemonLevelMoves(pkm: PlayerPokemon): Moves[] { + return pkm.getLevelMoves().map(([_level, move]) => move); + } + + private getAllPokemonMoves(pkm: PlayerPokemon): Moves[] { + const allPokemonMoves: Moves[] = []; + + if (!this.excludeLevelMoves) { + allPokemonMoves.push(...(this.getPokemonLevelMoves(pkm) ?? [])); + } + + if (!this.excludeTmMoves) { + allPokemonMoves.push(...(pkm.compatibleTms ?? [])); + } + + if (!this.excludeEggMoves) { + allPokemonMoves.push(...(pkm.getEggMoves() ?? [])); + } + + return allPokemonMoves; + } +} diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts new file mode 100644 index 000000000000..63c899fc5e97 --- /dev/null +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -0,0 +1,120 @@ +import { Moves } from "#enums/moves"; +import { Abilities } from "#enums/abilities"; + +/** + * Moves that "steal" things + */ +export const STEALING_MOVES = [ + Moves.PLUCK, + Moves.COVET, + Moves.KNOCK_OFF, + Moves.THIEF, + Moves.TRICK, + Moves.SWITCHEROO +]; + +/** + * Moves that "charm" someone + */ +export const CHARMING_MOVES = [ + Moves.CHARM, + Moves.FLATTER, + Moves.DRAGON_CHEER, + Moves.ALLURING_VOICE, + Moves.ATTRACT, + Moves.SWEET_SCENT, + Moves.CAPTIVATE, + Moves.AROMATIC_MIST +]; + +/** + * Moves for the Dancer ability + */ +export const DANCING_MOVES = [ + Moves.AQUA_STEP, + Moves.CLANGOROUS_SOUL, + Moves.DRAGON_DANCE, + Moves.FEATHER_DANCE, + Moves.FIERY_DANCE, + Moves.LUNAR_DANCE, + Moves.PETAL_DANCE, + Moves.REVELATION_DANCE, + Moves.QUIVER_DANCE, + Moves.SWORDS_DANCE, + Moves.TEETER_DANCE, + Moves.VICTORY_DANCE +]; + +/** + * Moves that can distract someone/something + */ +export const DISTRACTION_MOVES = [ + Moves.FAKE_OUT, + Moves.FOLLOW_ME, + Moves.TAUNT, + Moves.ROAR, + Moves.TELEPORT, + Moves.CHARM, + Moves.FAKE_TEARS, + Moves.TICKLE, + Moves.CAPTIVATE, + Moves.RAGE_POWDER, + Moves.SUBSTITUTE, + Moves.SHED_TAIL +]; + +/** + * Moves that protect in some way + */ +export const PROTECTING_MOVES = [ + Moves.PROTECT, + Moves.WIDE_GUARD, + Moves.MAX_GUARD, + Moves.SAFEGUARD, + Moves.REFLECT, + Moves.BARRIER, + Moves.QUICK_GUARD, + Moves.FLOWER_SHIELD, + Moves.KINGS_SHIELD, + Moves.CRAFTY_SHIELD, + Moves.SPIKY_SHIELD, + Moves.OBSTRUCT, + Moves.DETECT +]; + +/** + * Moves that (loosely) can be used to trap/rob someone + */ +export const EXTORTION_MOVES = [ + Moves.BIND, + Moves.CLAMP, + Moves.INFESTATION, + Moves.SAND_TOMB, + Moves.SNAP_TRAP, + Moves.THUNDER_CAGE, + Moves.WRAP, + Moves.SPIRIT_SHACKLE, + Moves.MEAN_LOOK, + Moves.JAW_LOCK, + Moves.BLOCK, + Moves.SPIDER_WEB, + Moves.ANCHOR_SHOT, + Moves.OCTOLOCK, + Moves.PURSUIT, + Moves.CONSTRICT, + Moves.BEAT_UP, + Moves.COIL, + Moves.WRING_OUT, + Moves.STRING_SHOT, +]; + +/** + * Abilities that (loosely) can be used to trap/rob someone + */ +export const EXTORTION_ABILITIES = [ + Abilities.INTIMIDATE, + Abilities.ARENA_TRAP, + Abilities.SHADOW_TAG, + Abilities.SUCTION_CUPS, + Abilities.STICKY_HOLD +]; diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts new file mode 100644 index 000000000000..494a45f69f5c --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -0,0 +1,86 @@ +import BattleScene from "#app/battle-scene"; +import { getTextWithColors, TextStyle } from "#app/ui/text"; +import { UiTheme } from "#enums/ui-theme"; +import { isNullOrUndefined } from "#app/utils"; +import i18next from "i18next"; + +/** + * Will inject all relevant dialogue tokens that exist in the {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens}, into i18n text. + * Also adds BBCodeText fragments for colored text, if applicable + * @param scene + * @param keyOrString + * @param primaryStyle Can define a text style to be applied to the entire string. Must be defined for BBCodeText styles to be applied correctly + * @param uiTheme + */ +export function getEncounterText(scene: BattleScene, keyOrString?: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string | null { + if (isNullOrUndefined(keyOrString)) { + return null; + } + + let textString: string | null = getTextWithDialogueTokens(scene, keyOrString!); + + // Can only color the text if a Primary Style is defined + // primaryStyle is applied to all text that does not have its own specified style + if (primaryStyle && textString) { + textString = getTextWithColors(textString, primaryStyle, uiTheme); + } + + return textString; +} + +/** + * Helper function to inject {@linkcode BattleScene.currentBattle.mysteryEncounter.dialogueTokens} into a given content string + * @param scene + * @param keyOrString + */ +function getTextWithDialogueTokens(scene: BattleScene, keyOrString: string): string | null { + const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; + + if (i18next.exists(keyOrString, tokens)) { + return i18next.t(keyOrString, tokens) as string; + } + + return keyOrString ?? null; +} + +/** + * Will queue a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + */ +export function queueEncounterMessage(scene: BattleScene, contentKey: string): void { + const text: string | null = getEncounterText(scene, contentKey); + scene.queueMessage(text ?? "", null, true); +} + +/** + * Will display a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + * @param delay + * @param prompt + * @param callbackDelay + * @param promptDelay + */ +export function showEncounterText(scene: BattleScene, contentKey: string, delay: number | null = null, callbackDelay: number = 0, prompt: boolean = true, promptDelay: number | null = null): Promise { + return new Promise(resolve => { + const text: string | null = getEncounterText(scene, contentKey); + scene.ui.showText(text ?? "", delay, () => resolve(), callbackDelay, prompt, promptDelay); + }); +} + +/** + * Will display a dialogue (with speaker title) in UI with injected encounter data tokens + * @param scene + * @param textContentKey + * @param delay + * @param speakerContentKey + * @param callbackDelay + */ +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, delay: number | null = null, callbackDelay: number = 0): Promise { + return new Promise(resolve => { + const text: string | null = getEncounterText(scene, textContentKey); + const speaker: string | null = getEncounterText(scene, speakerContentKey); + scene.ui.showDialogue(text ?? "", speaker ?? "", delay, () => resolve(), callbackDelay); + }); +} diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts new file mode 100644 index 000000000000..2cd369fbaad3 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -0,0 +1,1056 @@ +import Battle, { BattlerIndex, BattleType } from "#app/battle"; +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 { 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"; +import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { PartyOption, PartyUiMode, PokemonSelectFilter } from "#app/ui/party-ui-handler"; +import { Mode } from "#app/ui/ui"; +import * as Utils from "#app/utils"; +import { isNullOrUndefined } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Biome } from "#enums/biome"; +import { TrainerType } from "#enums/trainer-type"; +import i18next from "i18next"; +import BattleScene from "#app/battle-scene"; +import Trainer, { TrainerVariant } from "#app/field/trainer"; +import { Gender } from "#app/data/gender"; +import { Nature } from "#app/data/nature"; +import { Moves } from "#enums/moves"; +import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; +import PokemonSpecies from "#app/data/pokemon-species"; +import { Egg, IEggOptions } from "#app/data/egg"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; +import { MovePhase } from "#app/phases/move-phase"; +import { EggLapsePhase } from "#app/phases/egg-lapse-phase"; +import { TrainerVictoryPhase } from "#app/phases/trainer-victory-phase"; +import { BattleEndPhase } from "#app/phases/battle-end-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; + +/** + * Animates exclamation sprite over trainer's head at start of encounter + * @param scene + */ +export function doTrainerExclamation(scene: BattleScene) { + const exclamationSprite = scene.add.sprite(0, 0, "exclaim"); + exclamationSprite.setName("exclamation"); + scene.field.add(exclamationSprite); + scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1); + exclamationSprite.setVisible(true); + exclamationSprite.setPosition(110, 68); + scene.tweens.add({ + targets: exclamationSprite, + y: "-=25", + ease: "Cubic.easeOut", + duration: 300, + yoyo: true, + onComplete: () => { + scene.time.delayedCall(800, () => { + scene.field.remove(exclamationSprite, true); + }); + } + }); + + scene.playSound("battle_anims/GEN8- Exclaim", { volume: 0.7 }); +} + +export interface EnemyPokemonConfig { + species: PokemonSpecies; + isBoss: boolean; + bossSegments?: number; + bossSegmentModifier?: number; // Additive to the determined segment number + mysteryEncounterPokemonData?: MysteryEncounterPokemonData; + formIndex?: number; + abilityIndex?: number; + level?: number; + gender?: Gender; + passive?: boolean; + moveSet?: Moves[]; + nature?: Nature; + ivs?: [number, number, number, number, number, number]; + shiny?: boolean; + /** Can set just the status, or pass a timer on the status turns */ + status?: StatusEffect | [StatusEffect, number]; + mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + modifierConfigs?: HeldModifierConfig[]; + tags?: BattlerTagType[]; + dataSource?: PokemonData; +} + +export interface EnemyPartyConfig { + /** Formula for enemy: level += waveIndex / 10 * levelAdditive */ + levelAdditiveMultiplier?: 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 */ + female?: boolean; + /** True will prevent player from switching */ + disableSwitch?: boolean; +} + +/** + * Generates an enemy party for a mystery encounter battle + * This will override and replace any standard encounter generation logic + * Useful for tailoring specific battles to mystery encounters + * @param scene Battle Scene + * @param partyConfig Can pass various customizable attributes for the enemy party, see EnemyPartyConfig + */ +export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise { + const loaded: boolean = false; + const loadEnemyAssets: Promise[] = []; + + const battle: Battle = scene.currentBattle; + + let doubleBattle: boolean = partyConfig?.doubleBattle ?? false; + + // Trainer + const trainerType = partyConfig?.trainerType; + const partyTrainerConfig = partyConfig?.trainerConfig; + let trainerConfig: TrainerConfig; + if (!isNullOrUndefined(trainerType) || partyTrainerConfig) { + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; + if (scene.currentBattle.trainer) { + scene.currentBattle.trainer.setVisible(false); + scene.currentBattle.trainer.destroy(); + } + + trainerConfig = partyConfig?.trainerConfig ? partyConfig?.trainerConfig : trainerConfigs[trainerType!]; + + const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && !!partyConfig.doubleBattle); + doubleBattle = doubleTrainer; + const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!(Utils.randSeedInt(2)) : partyConfig.female; + const newTrainer = new Trainer(scene, trainerConfig.trainerType, doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, undefined, undefined, undefined, trainerConfig); + newTrainer.x += 300; + newTrainer.setVisible(false); + scene.field.add(newTrainer); + scene.currentBattle.trainer = newTrainer; + loadEnemyAssets.push(newTrainer.loadAssets()); + + battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex); + } else { + // Wild + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.WILD_BATTLE; + const numEnemies = partyConfig?.pokemonConfigs && partyConfig.pokemonConfigs.length > 0 ? partyConfig?.pokemonConfigs?.length : doubleBattle ? 2 : 1; + battle.enemyLevels = new Array(numEnemies).fill(null).map(() => scene.currentBattle.getLevelForWave()); + } + + scene.getEnemyParty().forEach(enemyPokemon => { + scene.field.remove(enemyPokemon, true); + }); + battle.enemyParty = []; + battle.double = doubleBattle; + + // 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. + // Leaving null/undefined will disable level scaling + const mult: number = !isNullOrUndefined(partyConfig.levelAdditiveMultiplier) ? partyConfig.levelAdditiveMultiplier! : 0; + const additive = Math.max(Math.round((scene.currentBattle.waveIndex / 10) * mult), 0); + battle.enemyLevels = battle.enemyLevels.map(level => level + additive); + + battle.enemyLevels.forEach((level, e) => { + let enemySpecies; + let dataSource; + let isBoss = false; + if (!loaded) { + if ((!isNullOrUndefined(trainerType) || trainerConfig) && battle.trainer) { + // Allows overriding a trainer's pokemon to use specific species/data + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + level = config.level ? config.level : level; + dataSource = config.dataSource; + enemySpecies = config.species; + isBoss = config.isBoss; + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource); + } else { + battle.enemyParty[e] = battle.trainer.genPartyMember(e); + } + } else { + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + level = config.level ? config.level : level; + dataSource = config.dataSource; + enemySpecies = config.species; + isBoss = config.isBoss; + if (isBoss) { + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.BOSS_BATTLE; + } + } else { + enemySpecies = scene.randomSpecies(battle.waveIndex, level, true); + } + + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource); + } + } + + const enemyPokemon = scene.getEnemyParty()[e]; + + // Make sure basic data is clean + enemyPokemon.hp = enemyPokemon.getMaxHp(); + enemyPokemon.status = null; + enemyPokemon.passive = false; + + if (e < (doubleBattle ? 2 : 1)) { + enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); + enemyPokemon.resetSummonData(); + } + + if (!loaded) { + scene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig)); + } + + if (partyConfig?.pokemonConfigs && e < partyConfig.pokemonConfigs.length) { + const config = partyConfig.pokemonConfigs[e]; + + // Generate new id, reset status and HP in case using data source + if (config.dataSource) { + enemyPokemon.id = Utils.randSeedInt(4294967296); + } + + // Set form + if (!isNullOrUndefined(config.formIndex)) { + enemyPokemon.formIndex = config.formIndex!; + } + + // Set shiny + if (!isNullOrUndefined(config.shiny)) { + enemyPokemon.shiny = config.shiny!; + } + + // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) + if (!isNullOrUndefined(config.mysteryEncounterPokemonData)) { + enemyPokemon.mysteryEncounterPokemonData = config.mysteryEncounterPokemonData!; + } + + // Set Boss + if (config.isBoss) { + let segments = !isNullOrUndefined(config.bossSegments) ? config.bossSegments! : scene.getEncounterBossSegments(scene.currentBattle.waveIndex, level, enemySpecies, true); + if (!isNullOrUndefined(config.bossSegmentModifier)) { + segments += config.bossSegmentModifier!; + } + enemyPokemon.setBoss(true, segments); + } + + // Set Passive + if (config.passive) { + enemyPokemon.passive = true; + } + + // Set Nature + if (config.nature) { + enemyPokemon.nature = config.nature; + } + + // Set IVs + if (config.ivs) { + enemyPokemon.ivs = config.ivs; + } + + // Set Status + const statusEffects = config.status; + if (statusEffects) { + // Default to cureturn 3 for sleep + const status = Array.isArray(statusEffects) ? statusEffects[0] : statusEffects; + const cureTurn = Array.isArray(statusEffects) ? statusEffects[1] : statusEffects === StatusEffect.SLEEP ? 3 : undefined; + enemyPokemon.status = new Status(status, 0, cureTurn); + } + + // Set summon data fields + if (!enemyPokemon.summonData) { + enemyPokemon.summonData = new PokemonSummonData(); + } + + // Set ability + if (!isNullOrUndefined(config.abilityIndex)) { + enemyPokemon.abilityIndex = config.abilityIndex!; + } + + // Set gender + if (!isNullOrUndefined(config.gender)) { + enemyPokemon.gender = config.gender!; + enemyPokemon.summonData.gender = config.gender!; + } + + // Set moves + if (config?.moveSet && config.moveSet.length > 0) { + const moves = config.moveSet.map(m => new PokemonMove(m)); + enemyPokemon.moveset = moves; + enemyPokemon.summonData.moveset = moves; + } + + // Set tags + if (config.tags && config.tags.length > 0) { + const tags = config.tags; + tags.forEach(tag => enemyPokemon.addTag(tag)); + } + + // mysteryEncounterBattleEffects will only be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied + if (config.mysteryEncounterBattleEffects) { + enemyPokemon.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects; + } + + // Requires re-priming summon data to update everything properly + enemyPokemon.primeSummonData(enemyPokemon.summonData); + + enemyPokemon.initBattleInfo(); + enemyPokemon.getBattleInfo().initInfo(enemyPokemon); + enemyPokemon.generateName(); + } + + loadEnemyAssets.push(enemyPokemon.loadAssets()); + + console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats); + }); + + scene.pushPhase(new MysteryEncounterBattlePhase(scene, partyConfig.disableSwitch)); + + await Promise.all(loadEnemyAssets); + battle.enemyParty.forEach((enemyPokemon_2, e_1) => { + if (e_1 < (doubleBattle ? 2 : 1)) { + enemyPokemon_2.setVisible(false); + if (battle.double) { + enemyPokemon_2.setFieldPosition(e_1 ? FieldPosition.RIGHT : FieldPosition.LEFT); + } + // Spawns at current visible field instead of on "next encounter" field (off screen to the left) + enemyPokemon_2.x += 300; + } + }); + if (!loaded) { + regenerateModifierPoolThresholds(scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); + const customModifierTypes = partyConfig?.pokemonConfigs + ?.filter(config => config?.modifierConfigs) + .map(config => config.modifierConfigs!); + scene.generateEnemyModifiers(customModifierTypes); + } +} + +/** + * Load special move animations/sfx for hard-coded encounter-specific moves that a pokemon uses at the start of an encounter + * See: [startOfBattleEffects](IMysteryEncounter.startOfBattleEffects) for more details + * + * This promise does not need to be awaited on if called in an encounter onInit (will just load lazily) + * @param scene + * @param moves + */ +export function loadCustomMovesForEncounter(scene: BattleScene, moves: Moves | Moves[]) { + moves = Array.isArray(moves) ? moves : [moves]; + return Promise.all(moves.map(move => initMoveAnim(scene, move))) + .then(() => loadMoveAnimAssets(scene, moves)); +} + +/** + * Will update player money, and animate change (sound optional) + * @param scene + * @param changeValue + * @param playSound + * @param showMessage + */ +export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true, showMessage: boolean = true) { + scene.money = Math.min(Math.max(scene.money + changeValue, 0), Number.MAX_SAFE_INTEGER); + scene.updateMoneyText(); + scene.animateMoneyChanged(false); + if (playSound) { + scene.playSound("se/buy"); + } + if (showMessage) { + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounterMessages:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounterMessages:receive_money", { amount: changeValue }), null, true); + } + } +} + +/** + * Converts modifier bullshit to an actual item + * @param scene Battle Scene + * @param modifier + * @param pregenArgs Can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType | null { + const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); + if (!modifierId) { + return null; + } + + let result: ModifierType = modifierTypes[modifierId](); + + // Populates item id and tier (order matters) + result = result + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); + + return result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; +} + +/** + * Converts modifier bullshit to an actual item + * @param scene - Battle Scene + * @param modifier + * @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateModifierTypeOption(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierTypeOption | null { + const result = generateModifierType(scene, modifier, pregenArgs); + if (result) { + return new ModifierTypeOption(result, 0); + } + return result; +} + +/** + * This function is intended for use inside onPreOptionPhase() of an encounter option + * @param scene + * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen + * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object + * @param onPokemonNotSelected - Any logic that needs to be performed if no Pokemon is chosen + * @param selectablePokemonFilter + */ +export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: PokemonSelectFilter): Promise { + return new Promise(resolve => { + const modeToSetOnExit = scene.ui.getMode(); + + // Open party screen to choose pokemon + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { + if (slotIndex < scene.getParty().length) { + scene.ui.setMode(modeToSetOnExit).then(() => { + const pokemon = scene.getParty()[slotIndex]; + const secondaryOptions = onPokemonSelected(pokemon); + if (!secondaryOptions) { + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + resolve(true); + return; + } + + // There is a second option to choose after selecting the Pokemon + scene.ui.setMode(Mode.MESSAGE).then(() => { + const displayOptions = () => { + // Always appends a cancel option to bottom of options + const fullOptions = secondaryOptions.map(option => { + // Update handler to resolve promise + const onSelect = option.handler; + option.handler = () => { + onSelect(); + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + resolve(true); + return true; + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + scene.ui.clearText(); + scene.ui.setMode(modeToSetOnExit); + resolve(false); + return true; + }, + onHover: () => { + showEncounterText(scene, i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + } + }); + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + supportHover: true + }; + + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true); + }; + + const textPromptKey = scene.currentBattle.mysteryEncounter?.selectedOption?.dialogue?.secondOptionPrompt; + if (!textPromptKey) { + displayOptions(); + } else { + showEncounterText(scene, textPromptKey).then(() => displayOptions()); + } + }); + }); + } else { + scene.ui.setMode(modeToSetOnExit).then(() => { + if (onPokemonNotSelected) { + onPokemonNotSelected(); + } + resolve(false); + }); + } + }, selectablePokemonFilter); + }); +} + +interface PokemonAndOptionSelected { + selectedPokemonIndex: number; + selectedOptionIndex: number; +} + +/** + * This function is intended for use inside onPreOptionPhase() of an encounter option + * @param scene + * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object + * @param options + * @param optionSelectPromptKey + * @param selectablePokemonFilter + * @param onHoverOverCancelOption + */ +export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelectItem[], optionSelectPromptKey: string, selectablePokemonFilter?: PokemonSelectFilter, onHoverOverCancelOption?: () => void): Promise { + return new Promise(resolve => { + const modeToSetOnExit = scene.ui.getMode(); + + const displayOptions = (config: OptionSelectConfig) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (!optionSelectPromptKey) { + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setMode(Mode.OPTION_SELECT, config); + } else { + showEncounterText(scene, optionSelectPromptKey).then(() => { + // Do hover over the starting selection option + if (fullOptions[0].onHover) { + fullOptions[0].onHover(); + } + scene.ui.setMode(Mode.OPTION_SELECT, config); + }); + } + }); + }; + + const selectPokemonAfterOption = (selectedOptionIndex: number) => { + // Open party screen to choose a Pokemon + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: number, option: PartyOption) => { + if (slotIndex < scene.getParty().length) { + // Pokemon and option selected + scene.ui.setMode(modeToSetOnExit).then(() => { + const result: PokemonAndOptionSelected = { selectedPokemonIndex: slotIndex, selectedOptionIndex: selectedOptionIndex }; + resolve(result); + }); + } else { + // Back to first option select screen + displayOptions(config); + } + }, selectablePokemonFilter); + }; + + // Always appends a cancel option to bottom of options + const fullOptions = options.map((option, index) => { + // Update handler to resolve promise + const onSelect = option.handler; + option.handler = () => { + onSelect(); + selectPokemonAfterOption(index); + return true; + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + scene.ui.clearText(); + scene.ui.setMode(modeToSetOnExit); + resolve(null); + return true; + }, + onHover: () => { + if (onHoverOverCancelOption) { + onHoverOverCancelOption(); + } + showEncounterText(scene, i18next.t("mysteryEncounterMessages:cancel_option"), 0, 0, false); + } + }); + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + supportHover: true + }; + + displayOptions(config); + }); +} + +/** + * Will initialize reward phases to follow the mystery encounter + * Can have shop displayed or skipped + * @param scene - Battle Scene + * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers + * @param eggRewards + * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before {@linkcode MysteryEncounterRewardsPhase}) + */ +export function setEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, eggRewards?: IEggOptions[], preRewardsCallback?: Function) { + scene.currentBattle.mysteryEncounter!.doEncounterRewards = (scene: BattleScene) => { + if (preRewardsCallback) { + preRewardsCallback(); + } + + if (customShopRewards) { + scene.unshiftPhase(new SelectModifierPhase(scene, 0, undefined, customShopRewards)); + } else { + scene.tryRemovePhase(p => p instanceof SelectModifierPhase); + } + + if (eggRewards) { + eggRewards.forEach(eggOptions => { + const egg = new Egg(eggOptions); + egg.addEggToGameData(scene); + }); + } + + return true; + }; +} + +/** + * Will initialize exp phases into the phase queue (these are in addition to any combat or other exp earned) + * Exp Share and Exp Balance will still function as normal + * @param scene - Battle Scene + * @param participantId - id/s of party pokemon that get full exp value. Other party members will receive Exp Share amounts + * @param baseExpValue - gives exp equivalent to a pokemon of the wave index's level. + * Guidelines: + * 36 - Sunkern (lowest in game) + * 62-64 - regional starter base evos + * 100 - Scyther + * 170 - Spiritomb + * 250 - Gengar + * 290 - trio legendaries + * 340 - box legendaries + * 608 - Blissey (highest in game) + * https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_effort_value_yield_(Generation_IX) + * @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue + */ +export function setEncounterExp(scene: BattleScene, participantId: number | number[], baseExpValue: number, useWaveIndex: boolean = true) { + const participantIds = Array.isArray(participantId) ? participantId : [participantId]; + + scene.currentBattle.mysteryEncounter!.doEncounterExp = (scene: BattleScene) => { + scene.unshiftPhase(new PartyExpPhase(scene, baseExpValue, useWaveIndex, new Set(participantIds))); + + return true; + }; +} + +export class OptionSelectSettings { + hideDescription?: boolean; + slideInDescription?: boolean; + overrideTitle?: string; + overrideDescription?: string; + overrideQuery?: string; + overrideOptions?: MysteryEncounterOption[]; + startingCursorIndex?: number; +} + +/** + * Can be used to queue a new series of Options to select for an Encounter + * MUST be used only in onOptionPhase, will not work in onPreOptionPhase or onPostOptionPhase + * @param scene + * @param optionSelectSettings + */ +export function initSubsequentOptionSelect(scene: BattleScene, optionSelectSettings: OptionSelectSettings) { + scene.pushPhase(new MysteryEncounterPhase(scene, optionSelectSettings)); +} + +/** + * Can be used to exit an encounter without any battles or followup + * Will skip any shops and rewards, and queue the next encounter phase as normal + * @param scene + * @param addHealPhase - when true, will add a shop phase to end of encounter with 0 rewards but healing items are available + * @param encounterMode - Can set custom encounter mode if necessary (may be required for forcing Pokemon to return before next phase) + */ +export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: boolean = false, encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE) { + scene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; + scene.clearPhaseQueue(); + scene.clearPhaseQueueSplice(); + handleMysteryEncounterVictory(scene, addHealPhase); +} + +/** + * + * @param scene + * @param addHealPhase - Adds an empty shop phase to allow player to purchase healing items + * @param doNotContinue - default `false`. If set to true, will not end the battle and continue to next wave + */ +export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) { + const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle()); + + if (allowedPkm.length === 0) { + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + return; + } + + // If in repeated encounter variant, do nothing + // Variant must eventually be swapped in order to handle "true" end of the encounter + const encounter = scene.currentBattle.mysteryEncounter!; + if (encounter.continuousEncounter || doNotContinue) { + return; + } else if (encounter.encounterMode === MysteryEncounterMode.NO_BATTLE) { + scene.pushPhase(new EggLapsePhase(scene)); + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + } 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)) { + if (!encounter.doContinueEncounter) { + // Only lapse eggs once for multi-battle encounters + scene.pushPhase(new EggLapsePhase(scene)); + } + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + } + } +} + +/** + * + * @param scene + * @param hide - If true, performs ease out and hide visuals. If false, eases in visuals. Defaults to true + * @param destroy - If true, will destroy visuals ONLY ON HIDE TRANSITION. Does nothing on show. Defaults to true + * @param duration + */ +export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: boolean = true, destroy: boolean = true, duration: number = 750): Promise { + return new Promise(resolve => { + const introVisuals = scene.currentBattle.mysteryEncounter!.introVisuals; + const enemyPokemon = scene.getEnemyField(); + if (enemyPokemon) { + scene.currentBattle.enemyParty = []; + } + if (introVisuals) { + if (!hide) { + // Make sure visuals are in proper state for showing + introVisuals.setVisible(true); + introVisuals.x = 244; + introVisuals.y = 60; + introVisuals.alpha = 0; + } + + // Transition + scene.tweens.add({ + targets: [introVisuals, enemyPokemon], + x: `${hide? "+" : "-"}=16`, + y: `${hide ? "-" : "+"}=16`, + alpha: hide ? 0 : 1, + ease: "Sine.easeInOut", + duration, + onComplete: () => { + if (hide && destroy) { + scene.field.remove(introVisuals, true); + + enemyPokemon.forEach(pokemon => { + scene.field.remove(pokemon, true); + }); + + scene.currentBattle.mysteryEncounter!.introVisuals = undefined; + } + resolve(true); + } + }); + } else { + resolve(true); + } + }); +} + +/** + * Will queue moves for any pokemon to use before the first CommandPhase of a battle + * Mostly useful for allowing {@linkcode MysteryEncounter} enemies to "cheat" and use moves before the first turn + * @param scene + */ +export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { + const effects = encounter.startOfBattleEffects; + effects.forEach(effect => { + let source; + if (effect.sourcePokemon) { + source = effect.sourcePokemon; + } else if (!isNullOrUndefined(effect.sourceBattlerIndex)) { + if (effect.sourceBattlerIndex === BattlerIndex.ATTACKER) { + source = scene.getEnemyField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY) { + source = scene.getEnemyField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY_2) { + source = scene.getEnemyField()[1]; + } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER) { + source = scene.getPlayerField()[0]; + } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER_2) { + source = scene.getPlayerField()[1]; + } + } else { + source = scene.getEnemyField()[0]; + } + scene.pushPhase(new MovePhase(scene, source, effect.targets, effect.move, effect.followUp, effect.ignorePp)); + }); + + // Pseudo turn end phase to reset flinch states, Endure, etc. + scene.pushPhase(new MysteryEncounterBattleStartCleanupPhase(scene)); + + encounter.startOfBattleEffectsComplete = true; + } +} + +/** + * Can queue extra phases or logic during {@linkcode TurnInitPhase} + * Should mostly just be used for injecting custom phases into the battle system on turn start + * @param scene + * @return boolean - if true, will skip the remainder of the {@linkcode TurnInitPhase} + */ +export function handleMysteryEncounterTurnStartEffects(scene: BattleScene): boolean { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.onTurnStart) { + return encounter.onTurnStart(scene); + } + + return false; +} + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param baseSpawnWeight + */ +export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: number) { + const numRuns = 1000; + let run = 0; + const biomes = Object.keys(Biome).filter(key => isNaN(Number(key))); + const alwaysPickTheseBiomes = [Biome.ISLAND, Biome.ABYSS, Biome.WASTELAND, Biome.FAIRY_CAVE, Biome.TEMPLE, Biome.LABORATORY, Biome.SPACE, Biome.WASTELAND]; + + const calculateNumEncounters = (): any[] => { + let encounterRate = baseSpawnWeight; // BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + const numEncounters = [0, 0, 0, 0]; + let mostRecentEncounterWave = 0; + const encountersByBiome = new Map(biomes.map(b => [b, 0])); + const validMEfloorsByBiome = new Map(biomes.map(b => [b, 0])); + let currentBiome = Biome.TOWN; + let currentArena = scene.newArena(currentBiome); + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + for (let i = 10; i < 180; i++) { + // Boss + if (i % 10 === 0) { + continue; + } + + // New biome + if (i % 10 === 1) { + if (Array.isArray(biomeLinks[currentBiome])) { + let biomes: Biome[]; + scene.executeWithSeedOffset(() => { + biomes = (biomeLinks[currentBiome] as (Biome | [Biome, number])[]) + .filter(b => { + return !Array.isArray(b) || !Utils.randSeedInt(b[1]); + }) + .map(b => !Array.isArray(b) ? b : b[0]); + }, i * 100); + if (biomes! && biomes.length > 0) { + const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b)); + if (specialBiomes.length > 0) { + currentBiome = specialBiomes[Utils.randSeedInt(specialBiomes.length)]; + } else { + currentBiome = biomes[Utils.randSeedInt(biomes.length)]; + } + } + } else if (biomeLinks.hasOwnProperty(currentBiome)) { + currentBiome = (biomeLinks[currentBiome] as Biome); + } else { + if (!(i % 50)) { + currentBiome = Biome.END; + } else { + currentBiome = scene.generateRandomBiome(i); + } + } + + currentArena = scene.newArena(currentBiome); + } + + // Fixed battle + if (scene.gameMode.isFixedBattle(i)) { + continue; + } + + // Trainer + if (scene.gameMode.isWaveTrainer(i, currentArena)) { + continue; + } + + // Otherwise, roll encounter + + const roll = Utils.randSeedInt(256); + validMEfloorsByBiome.set(Biome[currentBiome], (validMEfloorsByBiome.get(Biome[currentBiome]) ?? 0) + 1); + + // If total number of encounters is lower than expected for the run, slightly favor a new encounter + // Do the reverse as well + const expectedEncountersByFloor = AVERAGE_ENCOUNTERS_PER_RUN_TARGET / (180 - 10) * (i - 10); + const currentRunDiffFromAvg = expectedEncountersByFloor - numEncounters.reduce((a, b) => a + b); + const favoredEncounterRate = encounterRate + currentRunDiffFromAvg * 15; + + // If the most recent ME was 3 or fewer waves ago, can never spawn a ME + const canSpawn = (i - mostRecentEncounterWave) > 3; + + if (canSpawn && roll < favoredEncounterRate) { + mostRecentEncounterWave = i; + encounterRate = baseSpawnWeight; + + // Calculate encounter rarity + // Common / Uncommon / Rare / Super Rare (base is out of 128) + const tierWeights = [66, 40, 19, 3]; + + // Adjust tier weights by currently encountered events (pity system that lowers odds of multiple Common/Great) + tierWeights[0] = tierWeights[0] - 6 * numEncounters[0]; + tierWeights[1] = tierWeights[1] - 4 * numEncounters[1]; + + const totalWeight = tierWeights.reduce((a, b) => a + b); + const tierValue = Utils.randSeedInt(totalWeight); + const commonThreshold = totalWeight - tierWeights[0]; // 64 - 32 = 32 + const uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1]; // 64 - 32 - 16 = 16 + const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6 + + tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3]; + encountersByBiome.set(Biome[currentBiome], (encountersByBiome.get(Biome[currentBiome]) ?? 0) + 1); + } else { + encounterRate += WEIGHT_INCREMENT_ON_SPAWN_MISS; + } + } + + return [numEncounters, encountersByBiome, validMEfloorsByBiome]; + }; + + const encounterRuns: number[][] = []; + const encountersByBiomeRuns: Map[] = []; + const validFloorsByBiome: Map[] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const [numEncounters, encountersByBiome, validMEfloorsByBiome] = calculateNumEncounters(); + encounterRuns.push(numEncounters); + encountersByBiomeRuns.push(encountersByBiome); + validFloorsByBiome.push(validMEfloorsByBiome); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const uncommonMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const encountersPerRunPerBiome = encountersByBiomeRuns.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome)! + b.get(biome)!); + } + return a; + }); + const meanEncountersPerRunPerBiome: Map = new Map(); + encountersPerRunPerBiome.forEach((value, key) => { + meanEncountersPerRunPerBiome.set(key, value / n); + }); + + const validMEFloorsPerRunPerBiome = validFloorsByBiome.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome)! + b.get(biome)!); + } + return a; + }); + const meanMEFloorsPerRunPerBiome: Map = new Map(); + validMEFloorsPerRunPerBiome.forEach((value, key) => { + meanMEFloorsPerRunPerBiome.set(key, value / n); + }); + + let stats = `Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Greats: ${uncommonMean}\nAvg Ultras: ${rareMean}\nAvg Rogues: ${superRareMean}\n`; + + const meanEncountersPerRunPerBiomeSorted = [...meanEncountersPerRunPerBiome.entries()].sort((e1, e2) => e2[1] - e1[1]); + meanEncountersPerRunPerBiomeSorted.forEach(value => stats = stats + `${value[0]}: avg valid floors ${meanMEFloorsPerRunPerBiome.get(value[0])}, avg MEs ${value[1]},\n`); + + console.log(stats); +} + + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param luckValue - 0 to 14 + */ +export function calculateRareSpawnAggregateStats(scene: BattleScene, luckValue: number) { + const numRuns = 1000; + let run = 0; + + const calculateNumRareEncounters = (): any[] => { + const bossEncountersByRarity = [0, 0, 0, 0]; + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + // There are 12 wild boss floors + for (let i = 0; i < 12; i++) { + // Roll boss tier + // luck influences encounter rarity + let luckModifier = 0; + if (!isNaN(luckValue)) { + luckModifier = luckValue * 0.5; + } + const tierValue = Utils.randSeedInt(64 - luckModifier); + const tier = tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; + + switch (tier) { + default: + case BiomePoolTier.BOSS: + ++bossEncountersByRarity[0]; + break; + case BiomePoolTier.BOSS_RARE: + ++bossEncountersByRarity[1]; + break; + case BiomePoolTier.BOSS_SUPER_RARE: + ++bossEncountersByRarity[2]; + break; + case BiomePoolTier.BOSS_ULTRA_RARE: + ++bossEncountersByRarity[3]; + break; + } + } + + return bossEncountersByRarity; + }; + + const encounterRuns: number[][] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const bossEncountersByRarity = calculateNumRareEncounters(); + encounterRuns.push(bossEncountersByRarity); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + // const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + // const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + // const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const ultraRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const stats = `Avg Commons: ${commonMean}\nAvg Rare: ${rareMean}\nAvg Super Rare: ${superRareMean}\nAvg Ultra Rare: ${ultraRareMean}\n`; + + console.log(stats); +} diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts new file mode 100644 index 000000000000..bac8bded9bab --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -0,0 +1,723 @@ +import BattleScene from "#app/battle-scene"; +import i18next from "i18next"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { PlayerGender } from "#enums/player-gender"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; +import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; +import { achvs } from "#app/system/achv"; +import { Mode } from "#app/ui/ui"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { Gender } from "#app/data/gender"; +import { PermanentStat } from "#enums/stat"; +import { VictoryPhase } from "#app/phases/victory-phase"; + +/** + * Gets the sprite key and file root for a given PokemonSpecies (accounts for gender, shiny, variants, forms, and experimental) + * @param species + * @param female + * @param formIndex + * @param shiny + * @param variant + */ +export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: number, shiny?: boolean, variant?: number): { spriteKey: string, fileRoot: string } { + const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); + const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); + return { spriteKey, fileRoot }; +} + +/** + * Gets the sprite key and file root for a given Pokemon (accounts for gender, shiny, variants, forms, and experimental) + * @param pokemon + */ +export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string, fileRoot: string } { + const spriteKey = pokemon.getSpeciesForm().getSpriteKey(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); + const fileRoot = pokemon.getSpeciesForm().getSpriteAtlasPath(pokemon.getGender() === Gender.FEMALE, pokemon.formIndex, pokemon.shiny, pokemon.variant); + + return { spriteKey, fileRoot }; +} + +/** + * + * Will never remove the player's last non-fainted Pokemon (if they only have 1) + * Otherwise, picks a Pokemon completely at random and removes from the party + * @param scene + * @param isAllowedInBattle Default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon + * @param doNotReturnLastAbleMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) + * @returns + */ +export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let chosenIndex: number; + let chosenPokemon: PlayerPokemon; + const unfaintedMons = party.filter(p => p.isAllowedInBattle()); + const faintedMons = party.filter(p => !p.isAllowedInBattle()); + + if (doNotReturnLastAbleMon && unfaintedMons.length === 1) { + chosenIndex = randSeedInt(faintedMons.length); + chosenPokemon = faintedMons[chosenIndex]; + } else if (isAllowedInBattle) { + chosenIndex = randSeedInt(unfaintedMons.length); + chosenPokemon = unfaintedMons[chosenIndex]; + } else { + chosenIndex = randSeedInt(party.length); + chosenPokemon = party[chosenIndex]; + } + + return chosenPokemon; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param unfainted Default false. If true, only picks from unfainted mons. + * @returns + */ +export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (unfainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.level < p?.level ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param stat Stat to search for + * @param unfainted Default false. If true, only picks from unfainted mons. + * @returns + */ +export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (unfainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon.getStat(stat) < p?.getStat(stat) ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param unfainted - default false. If true, only picks from unfainted mons. + * @returns + */ +export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (unfainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.level > p?.level ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param unfainted - default false. If true, only picks from unfainted mons. + * @returns + */ +export function getHighestStatTotalPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon | null = null; + + for (const p of party) { + if (unfainted && p.isFainted()) { + continue; + } + + pokemon = pokemon ? pokemon?.stats.reduce((a, b) => a + b) < p?.stats.reduce((a, b) => a + b) ? p : pokemon : p; + } + + return pokemon!; +} + +/** + * + * NOTE: This returns ANY random species, including those locked behind eggs, etc. + * @param starterTiers + * @param excludedSpecies + * @param types + * @returns + */ +export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species { + let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers; + let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers; + + let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters) + .map(s => [parseInt(s) as Species, speciesStarters[s] as number]) + .filter(s => getPokemonSpecies(s[0]) && (!excludedSpecies || !excludedSpecies.includes(s[0]))) + .map(s => [getPokemonSpecies(s[0]), s[1]]); + + if (types && types.length > 0) { + filteredSpecies = filteredSpecies.filter(s => types.includes(s[0].type1) || (!isNullOrUndefined(s[0].type2) && types.includes(s[0].type2!))); + } + + // If no filtered mons exist at specified starter tiers, will expand starter search range until there are + // Starts by decrementing starter tier min until it is 0, then increments tier max up to 10 + let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max)); + while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) { + if (min > 0) { + min--; + } else { + max++; + } + + tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max); + } + + if (tryFilterStarterTiers.length > 0) { + const index = randSeedInt(tryFilterStarterTiers.length); + return Phaser.Math.RND.shuffle(tryFilterStarterTiers)[index][0].speciesId; + } + + return Species.BULBASAUR; +} + +/** + * Takes care of handling player pokemon KO (with all its side effects) + * + * @param scene the battle scene + * @param pokemon the player pokemon to KO + */ +export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) { + pokemon.hp = 0; + pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.updateInfo(); + queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); +} + +/** + * Handles applying hp changes to a player pokemon. + * Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info. + * TODO: should we handle special cases like wonder-guard/shedinja? + * @param scene the battle scene + * @param pokemon the player pokemon to apply the hp change to + * @param value the hp change amount. Positive for heal. Negative for damage + * + */ +function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) { + const hpChange = Math.round(pokemon.hp + value); + const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0); + if (nextHp === 0) { + koPlayerPokemon(scene, pokemon); + } else { + pokemon.hp = nextHp; + } +} + +/** + * Handles applying damage to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply damage to + * @param damage the amount of damage to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) { + if (damage <= 0) { + console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, -damage); +} + +/** + * Handles applying heal to a player pokemon + * @param scene the battle scene + * @param pokemon the player pokemon to apply heal to + * @param heal the amount of heal to apply + * @see {@linkcode applyHpChangeToPokemon} + */ +export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) { + if (heal <= 0) { + console.warn("Damaging pokemon with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead."); + } + + applyHpChangeToPokemon(scene, pokemon, heal); +} + +/** + * Will modify all of a Pokemon's base stats by a flat value + * Base stats can never go below 1 + * @param pokemon + * @param value + */ +export async function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { + const modType = modifierTypes.MYSTERY_ENCOUNTER_SHUCKLE_JUICE().generateType(pokemon.scene.getParty(), [value]); + const modifier = modType?.newModifier(pokemon); + if (modifier) { + await pokemon.scene.addModifier(modifier, false, false, false, true); + pokemon.calculateStats(); + } +} + +/** + * Will attempt to add a new modifier to a Pokemon. + * If the Pokemon already has max stacks of that item, it will instead apply 'fallbackModifierType', if specified. + * @param scene + * @param pokemon + * @param modType + * @param fallbackModifierType + */ +export async function applyModifierTypeToPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon, modType: PokemonHeldItemModifierType, fallbackModifierType?: PokemonHeldItemModifierType) { + // Check if the Pokemon has max stacks of that item already + const existing = scene.findModifier(m => ( + m instanceof PokemonHeldItemModifier && + m.type.id === modType.id && + m.pokemonId === pokemon.id + )) as PokemonHeldItemModifier; + + // At max stacks + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + if (!fallbackModifierType) { + return; + } + + // Apply fallback + return applyModifierTypeToPlayerPokemon(scene, pokemon, fallbackModifierType); + } + + const modifier = modType.newModifier(pokemon); + await scene.addModifier(modifier, false, false, false, true); +} + +/** + * Alternative to using AttemptCapturePhase + * Assumes player sprite is visible on the screen (this is intended for non-combat uses) + * + * Can await returned promise to wait for throw animation completion before continuing + * + * @param scene + * @param pokemon + * @param pokeballType + * @param ballTwitchRate - can pass custom ball catch rates (for special events, like safari) + */ +export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, pokeballType: PokeballType, ballTwitchRate?: number): Promise { + const originalY: number = pokemon.y; + + if (!ballTwitchRate) { + const _3m = 3 * pokemon.getMaxHp(); + const _2h = 2 * pokemon.hp; + const catchRate = pokemon.species.catchRate; + const pokeballMultiplier = getPokeballCatchMultiplier(this.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))); + } + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + scene.time.delayedCall(300, () => { + scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); + }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + scene.playSound("se/pb_throw"); + + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: pokeball, + x: { value: 236 + fpOffset[0], ease: "Linear" }, + y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("se/pb_rel"); + pokemon.tint(getPokeballTintColor(pokeballType)); + + addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + + scene.tweens.add({ + targets: pokemon, + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + y: 20, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + pokemon.setVisible(false); + scene.playSound("se/pb_catch"); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); + + const doShake = () => { + let shakeCount = 0; + const pbX = pokeball.x; + const shakeCounter = scene.tweens.addCounter({ + from: 0, + to: 1, + repeat: 4, + yoyo: true, + ease: "Cubic.easeOut", + duration: 250, + repeatDelay: 500, + onUpdate: t => { + if (shakeCount && shakeCount < 4) { + const value = t.getValue(); + const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; + pokeball.setX(pbX + value * 4 * directionMultiplier); + pokeball.setAngle(value * 27.5 * directionMultiplier); + } + }, + onRepeat: () => { + if (!pokemon.species.isObtainable()) { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } else if (shakeCount++ < 3) { + if (randSeedInt(65536) < ballTwitchRate) { + scene.playSound("se/pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("se/pb_lock"); + addPokeballCaptureStars(scene, pokeball); + + const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); + pbTint.setOrigin(pokeball.originX, pokeball.originY); + pbTint.setTintFill(0); + pbTint.setAlpha(0); + scene.field.add(pbTint); + scene.tweens.add({ + targets: pbTint, + alpha: 0.375, + duration: 200, + easing: "Sine.easeOut", + onComplete: () => { + scene.tweens.add({ + targets: pbTint, + alpha: 0, + duration: 200, + easing: "Sine.easeIn", + onComplete: () => pbTint.destroy() + }); + } + }); + } + }, + onComplete: () => { + catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); + } + }); + }; + + scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); + } + }); + } + }); + }); + }); +} + +/** + * Animates pokeball opening and messages when an attempted catch fails + * @param scene + * @param pokemon + * @param originalY + * @param pokeball + * @param pokeballType + */ +function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("se/pb_rel"); + pokemon.setY(originalY); + if (pokemon.status?.effect !== StatusEffect.SLEEP) { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + } + pokemon.tint(getPokeballTintColor(pokeballType)); + pokemon.setVisible(true); + pokemon.untint(250, "Sine.easeOut"); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + + scene.currentBattle.lastUsedPokeball = pokeballType; + removePb(scene, pokeball); + + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.getNameToRender() }), null, () => resolve(), null, true); + }); +} + +/** + * + * @param scene + * @param pokemon + * @param pokeball + * @param pokeballType + * @param showCatchObtainMessage + * @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) { + 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.pokemonInfoContainer.show(pokemon, true); + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + + return new Promise(resolve => { + const doPokemonCatchMenu = () => { + const end = () => { + scene.pokemonInfoContainer.hide(); + if (pokeball) { + removePb(scene, pokeball); + } + resolve(); + }; + const removePokemon = () => { + if (pokemon) { + scene.field.remove(pokemon, true); + } + }; + const addToParty = () => { + const newPokemon = pokemon.addToParty(pokeballType); + const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + if (scene.getParty().filter(p => p.isShiny()).length === 6) { + scene.validateAchv(achvs.SHINY_PARTY); + } + Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { + scene.updateModifiers(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } + }); + }; + Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { + if (scene.getParty().length === 6) { + const promptRelease = () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { + scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.ui.setMode(Mode.CONFIRM, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: number, _option: PartyOption) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (slotIndex < 6) { + addToParty(); + } else { + promptRelease(); + } + }); + }); + }, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + removePokemon(); + end(); + }); + }); + }); + }; + promptRelease(); + } else { + addToParty(); + } + }); + }; + + if (showCatchObtainMessage) { + scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, true); + } else { + doPokemonCatchMenu(); + } + }); +} + +/** + * Animates pokeball disappearing then destroys the object + * @param scene + * @param pokeball + */ +function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { + if (pokeball) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => { + pokeball.destroy(); + } + }); + } +} + +/** + * Animates a wild pokemon "fleeing", including sfx and messaging + * @param scene + * @param pokemon + */ +export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + await new Promise(resolve => { + scene.playSound("se/flee"); + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +/** + * Handles the player fleeing from a wild pokemon, including sfx and messaging + * @param scene + * @param pokemon + */ +export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + return new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), null, 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +/** + * Bug Species and their corresponding weights + */ +const GOLDEN_BUG_NET_SPECIES_POOL: [Species, number][] = [ + [Species.SCYTHER, 40], + [Species.SCIZOR, 40], + [Species.KLEAVOR, 40], + [Species.PINSIR, 40], + [Species.HERACROSS, 40], + [Species.YANMA, 40], + [Species.YANMEGA, 40], + [Species.SHUCKLE, 40], + [Species.ANORITH, 40], + [Species.ARMALDO, 40], + [Species.ESCAVALIER, 40], + [Species.ACCELGOR, 40], + [Species.JOLTIK, 40], + [Species.GALVANTULA, 40], + [Species.DURANT, 40], + [Species.LARVESTA, 40], + [Species.VOLCARONA, 40], + [Species.DEWPIDER, 40], + [Species.ARAQUANID, 40], + [Species.WIMPOD, 40], + [Species.GOLISOPOD, 40], + [Species.SIZZLIPEDE, 40], + [Species.CENTISKORCH, 40], + [Species.NYMBLE, 40], + [Species.LOKIX, 40], + [Species.BUZZWOLE, 1], + [Species.PHEROMOSA, 1], +]; + +/** + * Will randomly return one of the species from GOLDEN_BUG_NET_SPECIES_POOL, based on their weights + */ +export function getGoldenBugNetSpecies(): PokemonSpecies { + const totalWeight = GOLDEN_BUG_NET_SPECIES_POOL.reduce((a, b) => a + b[1], 0); + const roll = randSeedInt(totalWeight); + + let w = 0; + for (const species of GOLDEN_BUG_NET_SPECIES_POOL) { + w += species[1]; + if (roll < w) { + return getPokemonSpecies(species); + } + } + + // Defaults to Scyther + return getPokemonSpecies(Species.SCYTHER); +} diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts new file mode 100644 index 000000000000..fd9d43829e58 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -0,0 +1,392 @@ +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getFrameMs } from "#app/utils"; +import { cos, sin } from "#app/field/anims"; +import { getTypeRgb } from "#app/data/type"; + +export enum TransformationScreenPosition { + CENTER, + LEFT, + RIGHT +} + +/** + * Initiates an "evolution-like" animation to transform a previousPokemon (presumably from the player's party) into a new one, not necessarily an evolution species. + * @param scene + * @param previousPokemon + * @param transformPokemon + * @param screenPosition + */ +export function doPokemonTransformationSequence(scene: BattleScene, previousPokemon: PlayerPokemon, transformPokemon: PlayerPokemon, screenPosition: TransformationScreenPosition) { + return new Promise(resolve => { + const transformationContainer = scene.fieldUI.getByName("Dream Background") as Phaser.GameObjects.Container; + const transformationBaseBg = scene.add.image(0, 0, "default_bg"); + transformationBaseBg.setOrigin(0, 0); + transformationBaseBg.setVisible(false); + transformationContainer.add(transformationBaseBg); + + let pokemonSprite: Phaser.GameObjects.Sprite; + let pokemonTintSprite: Phaser.GameObjects.Sprite; + let pokemonEvoSprite: Phaser.GameObjects.Sprite; + let pokemonEvoTintSprite: Phaser.GameObjects.Sprite; + + const xOffset = screenPosition === TransformationScreenPosition.CENTER ? 0 : + screenPosition === TransformationScreenPosition.RIGHT ? 100 : -100; + // Centered transformations occur at a lower y Position + const yOffset = screenPosition !== TransformationScreenPosition.CENTER ? -15 : 0; + + const getPokemonSprite = () => { + const ret = scene.addPokemonSprite(previousPokemon, transformationBaseBg.displayWidth / 2 + xOffset, transformationBaseBg.displayHeight / 2 + yOffset, "pkmn__sub"); + ret.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + return ret; + }; + + transformationContainer.add((pokemonSprite = getPokemonSprite())); + transformationContainer.add((pokemonTintSprite = getPokemonSprite())); + transformationContainer.add((pokemonEvoSprite = getPokemonSprite())); + transformationContainer.add((pokemonEvoTintSprite = getPokemonSprite())); + + pokemonSprite.setAlpha(0); + pokemonTintSprite.setAlpha(0); + pokemonTintSprite.setTintFill(0xFFFFFF); + pokemonEvoSprite.setVisible(false); + pokemonEvoTintSprite.setVisible(false); + pokemonEvoTintSprite.setTintFill(0xFFFFFF); + + [ pokemonSprite, pokemonTintSprite, pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { + sprite.play(previousPokemon.getSpriteKey(true)); + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()) }); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", previousPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", previousPokemon.shiny); + sprite.setPipelineData("variant", previousPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (previousPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = previousPokemon.getSprite().pipelineData[k]; + }); + }); + + [ pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { + sprite.play(transformPokemon.getSpriteKey(true)); + sprite.setPipelineData("ignoreTimeTint", true); + sprite.setPipelineData("spriteKey", transformPokemon.getSpriteKey()); + sprite.setPipelineData("shiny", transformPokemon.shiny); + sprite.setPipelineData("variant", transformPokemon.variant); + [ "spriteColors", "fusionSpriteColors" ].map(k => { + if (transformPokemon.summonData?.speciesForm) { + k += "Base"; + } + sprite.pipelineData[k] = transformPokemon.getSprite().pipelineData[k]; + }); + }); + + scene.tweens.add({ + targets: pokemonSprite, + alpha: 1, + ease: "Cubic.easeInOut", + duration: 2000, + onComplete: () => { + doSpiralUpward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + scene.tweens.addCounter({ + from: 0, + to: 1, + duration: 1000, + onUpdate: t => { + pokemonTintSprite.setAlpha(t.getValue()); + }, + onComplete: () => { + pokemonSprite.setVisible(false); + scene.time.delayedCall(700, () => { + doArcDownward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + scene.time.delayedCall(1000, () => { + pokemonEvoTintSprite.setScale(0.25); + pokemonEvoTintSprite.setVisible(true); + doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(() => { + pokemonEvoSprite.setVisible(true); + doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset); + + scene.time.delayedCall(900, () => { + scene.tweens.add({ + targets: pokemonEvoTintSprite, + alpha: 0, + duration: 1500, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + scene.time.delayedCall(2500, () => { + resolve(); + scene.tweens.add({ + targets: pokemonEvoSprite, + alpha: 0, + duration: 2000, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + previousPokemon.destroy(); + transformPokemon.setVisible(false); + transformPokemon.setAlpha(1); + } + }); + }); + } + }); + }); + }); + }); + }); + } + }); + } + }); + }); +} + +/** + * Animates particles that "spiral" upwards at start of transform animation + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 64, + duration: getFrameMs(1), + onRepeat: () => { + if (f < 64) { + if (!(f & 7)) { + for (let i = 0; i < 4; i++) { + doSpiralUpwardParticle(scene, (f & 120) * 2 + i * 64, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + } + }); +} + +/** + * Animates particles that arc downwards after the upwards spiral + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 96, + duration: getFrameMs(1), + onRepeat: () => { + if (f < 96) { + if (f < 6) { + for (let i = 0; i < 9; i++) { + doArcDownParticle(scene, i * 16, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + } + }); +} + +/** + * Animates the transformation between the old pokemon form and new pokemon form + * @param scene + * @param l + * @param lastCycle + * @param pokemonTintSprite + * @param pokemonEvoTintSprite + */ +function doCycle(scene: BattleScene, l: number, lastCycle: number, pokemonTintSprite: Phaser.GameObjects.Sprite, pokemonEvoTintSprite: Phaser.GameObjects.Sprite): Promise { + return new Promise(resolve => { + const isLastCycle = l === lastCycle; + scene.tweens.add({ + targets: pokemonTintSprite, + scale: 0.25, + ease: "Cubic.easeInOut", + duration: 500 / l, + yoyo: !isLastCycle + }); + scene.tweens.add({ + targets: pokemonEvoTintSprite, + scale: 1, + ease: "Cubic.easeInOut", + duration: 500 / l, + yoyo: !isLastCycle, + onComplete: () => { + if (l < lastCycle) { + doCycle(scene, l + 0.5, lastCycle, pokemonTintSprite, pokemonEvoTintSprite).then(success => resolve(success)); + } else { + pokemonTintSprite.setVisible(false); + resolve(true); + } + } + }); + }); +} + +/** + * Animates particles in a circle pattern + * @param scene + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInward(scene: BattleScene, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + let f = 0; + + scene.tweens.addCounter({ + repeat: 48, + duration: getFrameMs(1), + onRepeat: () => { + if (!f) { + for (let i = 0; i < 16; i++) { + doCircleInwardParticle(scene, i * 16, 4, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } else if (f === 32) { + for (let i = 0; i < 16; i++) { + doCircleInwardParticle(scene, i * 16, 8, transformationBaseBg, transformationContainer, xOffset, yOffset); + } + } + f++; + } + }); +} + +/** + * Helper function for {@linkcode doSpiralUpward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doSpiralUpwardParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const particle = scene.add.image(initialX, 0, "evo_sparkle"); + transformationContainer.add(particle); + + let f = 0; + let amp = 48; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y > 8) { + particle.setPosition(initialX, 88 - (f * f) / 80 + yOffset); + particle.y += sin(trigIndex, amp) / 4; + particle.x += cos(trigIndex, amp); + particle.setScale(1 - (f / 80)); + trigIndex += 4; + if (f & 1) { + amp--; + } + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} + +/** + * Helper function for {@linkcode doArcDownward}, handles a single particle + * @param scene + * @param trigIndex + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doArcDownParticle(scene: BattleScene, trigIndex: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const particle = scene.add.image(initialX, 0, "evo_sparkle"); + particle.setScale(0.5); + transformationContainer.add(particle); + + let f = 0; + let amp = 8; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (!f || particle.y < 88) { + particle.setPosition(initialX, 8 + (f * f) / 5 + yOffset); + particle.y += sin(trigIndex, amp) / 4; + particle.x += cos(trigIndex, amp); + amp = 8 + sin(f * 4, 40); + f++; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} + +/** + * Helper function for @{link doCircleInward}, handles a single particle + * @param scene + * @param trigIndex + * @param speed + * @param transformationBaseBg + * @param transformationContainer + * @param xOffset + * @param yOffset + */ +function doCircleInwardParticle(scene: BattleScene, trigIndex: number, speed: number, transformationBaseBg: Phaser.GameObjects.Image, transformationContainer: Phaser.GameObjects.Container, xOffset: number, yOffset: number) { + const initialX = transformationBaseBg.displayWidth / 2 + xOffset; + const initialY = transformationBaseBg.displayHeight / 2 + yOffset; + const particle = scene.add.image(initialX, initialY, "evo_sparkle"); + transformationContainer.add(particle); + + let amp = 120; + + const particleTimer = scene.tweens.addCounter({ + repeat: -1, + duration: getFrameMs(1), + onRepeat: () => { + updateParticle(); + } + }); + + const updateParticle = () => { + if (amp > 8) { + particle.setPosition(initialX, initialY); + particle.y += sin(trigIndex, amp); + particle.x += cos(trigIndex, amp); + amp -= speed; + trigIndex += 4; + } else { + particle.destroy(); + particleTimer.remove(); + } + }; + + updateParticle(); +} diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts index 6479d6201826..f9602d1386a2 100644 --- a/src/data/pokemon-evolutions.ts +++ b/src/data/pokemon-evolutions.ts @@ -17,7 +17,8 @@ export enum SpeciesWildEvolutionDelay { SHORT, MEDIUM, LONG, - VERY_LONG + VERY_LONG, + NEVER } export enum EvolutionItem { @@ -39,19 +40,34 @@ export enum EvolutionItem { TART_APPLE, STRAWBERRY_SWEET, UNREMARKABLE_TEACUP, - - CHIPPED_POT = 51, - BLACK_AUGURITE, + UPGRADE, + DUBIOUS_DISC, + DRAGON_SCALE, + PRISM_SCALE, + RAZOR_CLAW, + RAZOR_FANG, + REAPER_CLOTH, + ELECTIRIZER, + MAGMARIZER, + PROTECTOR, + SACHET, + WHIPPED_DREAM, + SYRUPY_APPLE, + CHIPPED_POT, GALARICA_CUFF, GALARICA_WREATH, - PEAT_BLOCK, AUSPICIOUS_ARMOR, MALICIOUS_ARMOR, MASTERPIECE_TEACUP, + SUN_FLUTE, + MOON_FLUTE, + + BLACK_AUGURITE = 51, + PEAT_BLOCK, METAL_ALLOY, SCROLL_OF_DARKNESS, SCROLL_OF_WATERS, - SYRUPY_APPLE + LEADERS_CREST } /** @@ -222,7 +238,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.SLOWPOKE]: [ new SpeciesEvolution(Species.SLOWBRO, 37, null, null), - new SpeciesEvolution(Species.SLOWKING, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* King's Rock */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SLOWKING, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MAGNEMITE]: [ new SpeciesEvolution(Species.MAGNETON, 30, null, null) @@ -249,8 +265,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ELECTRODE, 30, null, null) ], [Species.CUBONE]: [ - new SpeciesEvolution(Species.ALOLA_MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.MAROWAK, 28, null, null) + new SpeciesEvolution(Species.ALOLA_MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), + new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.TYROGUE]: [ new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])), @@ -258,8 +274,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF])) ], [Species.KOFFING]: [ - new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.METROPOLIS || p.scene.arena.biomeType === Biome.SLUM), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.WEEZING, 35, null, null) + new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), + new SpeciesEvolution(Species.WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.RHYHORN]: [ new SpeciesEvolution(Species.RHYDON, 42, null, null) @@ -304,7 +320,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.QUILAVA, 14, null, null) ], [Species.QUILAVA]: [ - new SpeciesEvolution(Species.HISUI_TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.TYPHLOSION, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.TOTODILE]: [ @@ -652,7 +668,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DEWOTT, 17, null, null) ], [Species.DEWOTT]: [ - new SpeciesEvolution(Species.HISUI_SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.SAMUROTT, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.PATRAT]: [ @@ -800,10 +816,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.BISHARP, 52, null, null) ], [Species.BISHARP]: [ - new SpeciesEvolution(Species.KINGAMBIT, 64, null, null) + new SpeciesEvolution(Species.KINGAMBIT, 1, EvolutionItem.LEADERS_CREST, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.RUFFLET]: [ - new SpeciesEvolution(Species.HISUI_BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.BRAVIARY, 54, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.VULLABY]: [ @@ -883,20 +899,20 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLAWITZER, 37, null, null) ], [Species.TYRUNT]: [ - new SpeciesEvolution(Species.TYRANTRUM, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.TYRANTRUM, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.AMAURA]: [ - new SpeciesEvolution(Species.AURORUS, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.AURORUS, 39, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.GOOMY]: [ - new SpeciesEvolution(Species.HISUI_SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.SLIGGOO, 40, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.SLIGGOO]: [ new SpeciesEvolution(Species.GOODRA, 50, null, new SpeciesEvolutionCondition(p => [ WeatherType.RAIN, WeatherType.FOG, WeatherType.HEAVY_RAIN ].indexOf(p.scene.arena.weather?.weatherType || WeatherType.NONE) > -1), SpeciesWildEvolutionDelay.LONG) ], [Species.BERGMITE]: [ - new SpeciesEvolution(Species.HISUI_AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.AVALUGG, 37, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.NOIBAT]: [ @@ -906,7 +922,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DARTRIX, 17, null, null) ], [Species.DARTRIX]: [ - new SpeciesEvolution(Species.HISUI_DECIDUEYE, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.HISUI_DECIDUEYE, 36, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), new SpeciesEvolution(Species.DECIDUEYE, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.LITTEN]: [ @@ -928,7 +944,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.TOUCANNON, 28, null, null) ], [Species.YUNGOOS]: [ - new SpeciesEvolution(Species.GUMSHOOS, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.GUMSHOOS, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.GRUBBIN]: [ new SpeciesEvolution(Species.CHARJABUG, 20, null, null) @@ -946,7 +962,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ARAQUANID, 22, null, null) ], [Species.FOMANTIS]: [ - new SpeciesEvolution(Species.LURANTIS, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) + new SpeciesEvolution(Species.LURANTIS, 34, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)) ], [Species.MORELULL]: [ new SpeciesEvolution(Species.SHIINOTIC, 24, null, null) @@ -973,17 +989,17 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.KOMMO_O, 45, null, null) ], [Species.COSMOG]: [ - new SpeciesEvolution(Species.COSMOEM, 43, null, null) + new SpeciesEvolution(Species.COSMOEM, 23, null, null) ], [Species.COSMOEM]: [ - new SpeciesEvolution(Species.SOLGALEO, 53, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)), - new SpeciesEvolution(Species.LUNALA, 53, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.SOLGALEO, 53, EvolutionItem.SUN_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesEvolution(Species.LUNALA, 53, EvolutionItem.MOON_FLUTE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MELTAN]: [ new SpeciesEvolution(Species.MELMETAL, 48, null, null) ], [Species.ALOLA_RATTATA]: [ - new SpeciesEvolution(Species.ALOLA_RATICATE, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.ALOLA_RATICATE, 20, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.ALOLA_DIGLETT]: [ new SpeciesEvolution(Species.ALOLA_DUGTRIO, 26, null, null) @@ -1090,7 +1106,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GALAR_RAPIDASH, 40, null, null) ], [Species.GALAR_FARFETCHD]: [ - new SpeciesEvolution(Species.SIRFETCHD, 30, null, null) + new SpeciesEvolution(Species.SIRFETCHD, 30, null, null, SpeciesWildEvolutionDelay.LONG) ], [Species.GALAR_SLOWPOKE]: [ new SpeciesEvolution(Species.GALAR_SLOWBRO, 1, EvolutionItem.GALARICA_CUFF, null, SpeciesWildEvolutionDelay.VERY_LONG), @@ -1106,7 +1122,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GALAR_LINOONE, 20, null, null) ], [Species.GALAR_LINOONE]: [ - new SpeciesEvolution(Species.OBSTAGOON, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.OBSTAGOON, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.GALAR_YAMASK]: [ new SpeciesEvolution(Species.RUNERIGUS, 34, null, null) @@ -1214,7 +1230,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.GLIMMORA, 35, null, null) ], [Species.GREAVARD]: [ - new SpeciesEvolution(Species.HOUNDSTONE, 30, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) + new SpeciesEvolution(Species.HOUNDSTONE, 30, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)) ], [Species.FRIGIBAX]: [ new SpeciesEvolution(Species.ARCTIBAX, 35, null, null) @@ -1226,8 +1242,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLODSIRE, 20, null, null) ], [Species.PIKACHU]: [ - new SpeciesFormEvolution(Species.ALOLA_RAICHU, "", "", 1, EvolutionItem.THUNDER_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ALOLA_RAICHU, "partner", "", 1, EvolutionItem.THUNDER_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ALOLA_RAICHU, "", "", 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ALOLA_RAICHU, "partner", "", 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.RAICHU, "", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.RAICHU, "partner", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], @@ -1255,7 +1271,7 @@ export const pokemonEvolutions: PokemonEvolutions = { ], [Species.POLIWHIRL]: [ new SpeciesEvolution(Species.POLIWRATH, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), - new SpeciesEvolution(Species.POLITOED, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* King's Rock */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.POLITOED, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.WEEPINBELL]: [ new SpeciesEvolution(Species.VICTREEBEL, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1267,7 +1283,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CLOYSTER, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.EXEGGCUTE]: [ - new SpeciesEvolution(Species.ALOLA_EXEGGUTOR, 1, EvolutionItem.LEAF_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.biomeType === Biome.ISLAND || p.scene.arena.biomeType === Biome.BEACH), SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.ALOLA_EXEGGUTOR, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesEvolution(Species.EXEGGUTOR, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.TANGELA]: [ @@ -1280,12 +1296,12 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.STARMIE, 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.EEVEE]: [ - new SpeciesFormEvolution(Species.SYLVEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.SYLVEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ESPEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.ESPEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.UMBREON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), - new SpeciesFormEvolution(Species.UMBREON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(70, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.SYLVEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.SYLVEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => !!p.getMoveset().find(m => m?.getMove().type === Type.FAIRY)), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ESPEON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.ESPEON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.UMBREON, "", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), + new SpeciesFormEvolution(Species.UMBREON, "partner", "", 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.VAPOREON, "", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.VAPOREON, "partner", "", 1, EvolutionItem.WATER_STONE, null, SpeciesWildEvolutionDelay.LONG), new SpeciesFormEvolution(Species.JOLTEON, "", "", 1, EvolutionItem.THUNDER_STONE, null, SpeciesWildEvolutionDelay.LONG), @@ -1329,10 +1345,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.DUDUNSPARCE, 32, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.HYPER_DRILL).length > 0), SpeciesWildEvolutionDelay.LONG) ], [Species.GLIGAR]: [ - new SpeciesEvolution(Species.GLISCOR, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor fang at night*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.GLISCOR, 1, EvolutionItem.RAZOR_FANG, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor fang at night*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SNEASEL]: [ - new SpeciesEvolution(Species.WEAVILE, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor claw at night*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.WEAVILE, 1, EvolutionItem.RAZOR_CLAW, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT /* Razor claw at night*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.URSARING]: [ new SpeciesEvolution(Species.URSALUNA, 1, EvolutionItem.PEAT_BLOCK, null, SpeciesWildEvolutionDelay.VERY_LONG) //Ursaring does not evolve into Bloodmoon Ursaluna @@ -1362,8 +1378,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.SUDOWOODO, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.MIME_JR]: [ - new SpeciesEvolution(Species.GALAR_MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.biomeType === Biome.ICE_CAVE || p.scene.arena.biomeType === Biome.SNOWY_FOREST)), SpeciesWildEvolutionDelay.MEDIUM), - new SpeciesEvolution(Species.MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.GALAR_MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)), SpeciesWildEvolutionDelay.MEDIUM), + new SpeciesEvolution(Species.MR_MIME, 1, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.MIMIC).length > 0 && (p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY)), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.PANSAGE]: [ new SpeciesEvolution(Species.SIMISAGE, 1, EvolutionItem.LEAF_STONE, null, SpeciesWildEvolutionDelay.LONG) @@ -1381,8 +1397,8 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.WHIMSICOTT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.PETILIL]: [ - new SpeciesEvolution(Species.HISUI_LILLIGANT, 1, EvolutionItem.SUN_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesEvolution(Species.LILLIGANT, 1, EvolutionItem.SUN_STONE, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.HISUI_LILLIGANT, 1, EvolutionItem.SHINY_STONE, null, SpeciesWildEvolutionDelay.LONG), + new SpeciesEvolution(Species.LILLIGANT, 1, EvolutionItem.SUN_STONE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.BASCULIN]: [ new SpeciesFormEvolution(Species.BASCULEGION, "white-striped", "female", 40, null, new SpeciesEvolutionCondition(p => p.gender === Gender.FEMALE, p => p.gender = Gender.FEMALE), SpeciesWildEvolutionDelay.VERY_LONG), @@ -1435,7 +1451,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.APPLETUN, 1, EvolutionItem.SWEET_APPLE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.CLOBBOPUS]: [ - new SpeciesEvolution(Species.GRAPPLOCT, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.TAUNT).length > 0), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.GRAPPLOCT, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.TAUNT).length > 0)/*Once Taunt is implemented, change evo level to 1 and delay to LONG*/) ], [Species.SINISTEA]: [ new SpeciesFormEvolution(Species.POLTEAGEIST, "phony", "phony", 1, EvolutionItem.CRACKED_POT, null, SpeciesWildEvolutionDelay.LONG), @@ -1472,7 +1488,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.OVERQWIL, 28, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.BARB_BARRAGE).length > 0), SpeciesWildEvolutionDelay.LONG) ], [Species.HISUI_SNEASEL]: [ - new SpeciesEvolution(Species.SNEASLER, 1, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAY /* Razor claw at day*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SNEASLER, 1, EvolutionItem.RAZOR_CLAW, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY /* Razor claw at day*/), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CHARCADET]: [ new SpeciesEvolution(Species.ARMAROUGE, 1, EvolutionItem.AUSPICIOUS_ARMOR, null, SpeciesWildEvolutionDelay.LONG), @@ -1512,10 +1528,10 @@ export const pokemonEvolutions: PokemonEvolutions = { SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.RHYDON]: [ - new SpeciesEvolution(Species.RHYPERIOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Protector */), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.RHYPERIOR, 1, EvolutionItem.PROTECTOR, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SEADRA]: [ - new SpeciesEvolution(Species.KINGDRA, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Dragon scale*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.KINGDRA, 1, EvolutionItem.DRAGON_SCALE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SCYTHER]: [ new SpeciesEvolution(Species.SCIZOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition( @@ -1524,22 +1540,22 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.KLEAVOR, 1, EvolutionItem.BLACK_AUGURITE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.ELECTABUZZ]: [ - new SpeciesEvolution(Species.ELECTIVIRE, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Electirizer*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.ELECTIVIRE, 1, EvolutionItem.ELECTIRIZER, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.MAGMAR]: [ - new SpeciesEvolution(Species.MAGMORTAR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Magmarizer*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.MAGMORTAR, 1, EvolutionItem.MAGMARIZER, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.PORYGON]: [ - new SpeciesEvolution(Species.PORYGON2, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Upgrade*/), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.PORYGON2, 1, EvolutionItem.UPGRADE, null, SpeciesWildEvolutionDelay.LONG) ], [Species.PORYGON2]: [ - new SpeciesEvolution(Species.PORYGON_Z, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Dubious disc*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.PORYGON_Z, 1, EvolutionItem.DUBIOUS_DISC, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.FEEBAS]: [ - new SpeciesEvolution(Species.MILOTIC, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Prism scale*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.MILOTIC, 1, EvolutionItem.PRISM_SCALE, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.DUSCLOPS]: [ - new SpeciesEvolution(Species.DUSKNOIR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /* Reaper cloth*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.DUSKNOIR, 1, EvolutionItem.REAPER_CLOTH, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CLAMPERL]: [ new SpeciesEvolution(Species.HUNTAIL, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => p.gender === Gender.MALE, p => p.gender = Gender.MALE /* Deep Sea Tooth */), SpeciesWildEvolutionDelay.VERY_LONG), @@ -1558,10 +1574,10 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ACCELGOR, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => !!p.scene.gameData.dexData[Species.KARRABLAST].caughtAttr), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SPRITZEE]: [ - new SpeciesEvolution(Species.AROMATISSE, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Sachet*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.AROMATISSE, 1, EvolutionItem.SACHET, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.SWIRLIX]: [ - new SpeciesEvolution(Species.SLURPUFF, 1, EvolutionItem.LINKING_CORD, new SpeciesEvolutionCondition(p => true /*Whipped Dream*/), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.SLURPUFF, 1, EvolutionItem.WHIPPED_DREAM, null, SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.PHANTUMP]: [ new SpeciesEvolution(Species.TREVENANT, 1, EvolutionItem.LINKING_CORD, null, SpeciesWildEvolutionDelay.VERY_LONG) @@ -1576,7 +1592,7 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.ANNIHILAPE, 35, null, new SpeciesEvolutionCondition(p => p.moveset.filter(m => m?.moveId === Moves.RAGE_FIST).length > 0), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.GOLBAT]: [ - new SpeciesEvolution(Species.CROBAT, 1, null, new SpeciesFriendshipEvolutionCondition(110), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesEvolution(Species.CROBAT, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.VERY_LONG) ], [Species.CHANSEY]: [ new SpeciesEvolution(Species.BLISSEY, 1, null, new SpeciesFriendshipEvolutionCondition(200), SpeciesWildEvolutionDelay.LONG) @@ -1610,29 +1626,29 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.CHANSEY, 1, null, new SpeciesFriendshipEvolutionCondition(160), SpeciesWildEvolutionDelay.SHORT) ], [Species.MUNCHLAX]: [ - new SpeciesEvolution(Species.SNORLAX, 1, null, new SpeciesFriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SNORLAX, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.RIOLU]: [ - new SpeciesEvolution(Species.LUCARIO, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.LUCARIO, 1, null, new SpeciesFriendshipEvolutionCondition(120, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY), SpeciesWildEvolutionDelay.LONG) ], [Species.WOOBAT]: [ - new SpeciesEvolution(Species.SWOOBAT, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.MEDIUM) + new SpeciesEvolution(Species.SWOOBAT, 1, null, new SpeciesFriendshipEvolutionCondition(90), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.SWADLOON]: [ - new SpeciesEvolution(Species.LEAVANNY, 1, null, new SpeciesFriendshipEvolutionCondition(110), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.LEAVANNY, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.TYPE_NULL]: [ - new SpeciesEvolution(Species.SILVALLY, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.SILVALLY, 1, null, new SpeciesFriendshipEvolutionCondition(100), SpeciesWildEvolutionDelay.LONG) ], [Species.ALOLA_MEOWTH]: [ - new SpeciesEvolution(Species.ALOLA_PERSIAN, 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.LONG) + new SpeciesEvolution(Species.ALOLA_PERSIAN, 1, null, new SpeciesFriendshipEvolutionCondition(120), SpeciesWildEvolutionDelay.LONG) ], [Species.SNOM]: [ new SpeciesEvolution(Species.FROSMOTH, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.GIMMIGHOUL]: [ - new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesFriendshipEvolutionCondition(70), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/pokemon-level-moves.ts b/src/data/pokemon-level-moves.ts index 93bd57ae32ca..b56bab724be7 100644 --- a/src/data/pokemon-level-moves.ts +++ b/src/data/pokemon-level-moves.ts @@ -1609,6 +1609,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 12, Moves.DRAGON_BREATH ], [ 16, Moves.CURSE ], [ 20, Moves.ROCK_SLIDE ], + [ 22, Moves.GYRO_BALL ], //Custom, from USUM [ 24, Moves.SCREECH ], [ 28, Moves.SAND_TOMB ], [ 32, Moves.STEALTH_ROCK ], @@ -2121,7 +2122,7 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 20, Moves.DOUBLE_HIT ], [ 24, Moves.SLASH ], [ 28, Moves.FOCUS_ENERGY ], - [ 30, Moves.STEEL_WING ], + [ 30, Moves.STEEL_WING ], //Custom [ 32, Moves.AGILITY ], [ 36, Moves.AIR_SLASH ], [ 40, Moves.X_SCISSOR ], @@ -7549,14 +7550,15 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { [ 1, Moves.POUND ], [ 1, Moves.COPYCAT ], [ 1, Moves.BARRIER ], + [ 1, Moves.TICKLE ], //USUM [ 4, Moves.BATON_PASS ], [ 8, Moves.ENCORE ], [ 12, Moves.CONFUSION ], - [ 16, Moves.ROLE_PLAY ], + [ 16, Moves.MIMIC ], //Custom, swapped with Role Play to be closer to USUM [ 20, Moves.PROTECT ], [ 24, Moves.RECYCLE ], [ 28, Moves.PSYBEAM ], - [ 32, Moves.MIMIC ], + [ 32, Moves.ROLE_PLAY ], //Custom, swapped with Mimic [ 36, Moves.LIGHT_SCREEN ], [ 36, Moves.REFLECT ], [ 36, Moves.SAFEGUARD ], diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index aa85c29f5517..4eb526eeb2b3 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -243,16 +243,24 @@ export abstract class PokemonSpeciesForm { return false; } + /** + * Gets the BST for the species + * @returns The species' BST. + */ + getBaseStatTotal(): number { + return this.baseStats.reduce((i, n) => n + i); + } + /** * Gets the species' base stat amount for the given stat. * @param stat The desired stat. * @returns The species' base stat amount. */ - getBaseStat(stat: Stat): integer { + getBaseStat(stat: Stat): number { return this.baseStats[stat]; } - getBaseExp(): integer { + getBaseExp(): number { let ret = this.baseExp; switch (this.getFormSpriteKey()) { case SpeciesFormKey.MEGA: @@ -936,7 +944,7 @@ export function initSpecies() { new PokemonSpecies(Species.VENUSAUR, 1, false, false, false, "Seed Pokémon", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, GrowthRate.MEDIUM_SLOW, 87.5, true, true, new PokemonForm("Normal", "", Type.GRASS, Type.POISON, 2, 100, Abilities.OVERGROW, Abilities.NONE, Abilities.CHLOROPHYLL, 525, 80, 82, 83, 100, 100, 80, 45, 50, 263, true, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GRASS, Type.POISON, 2.4, 155.5, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 625, 80, 100, 123, 122, 120, 80, 45, 50, 263, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 100, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.POISON, 24, 999.9, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, Abilities.CHLOROPHYLL, 625, 120, 82, 98, 130, 115, 80, 45, 50, 263, true), ), new PokemonSpecies(Species.CHARMANDER, 1, false, false, false, "Lizard Pokémon", Type.FIRE, null, 0.6, 8.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 309, 39, 52, 43, 60, 50, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CHARMELEON, 1, false, false, false, "Flame Pokémon", Type.FIRE, null, 1.1, 19, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 405, 58, 64, 58, 80, 65, 80, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), @@ -944,20 +952,20 @@ export function initSpecies() { new PokemonForm("Normal", "", Type.FIRE, Type.FLYING, 1.7, 90.5, Abilities.BLAZE, Abilities.NONE, Abilities.SOLAR_POWER, 534, 78, 84, 78, 109, 85, 100, 45, 50, 267, false, null, true), new PokemonForm("Mega X", SpeciesFormKey.MEGA_X, Type.FIRE, Type.DRAGON, 1.7, 110.5, Abilities.TOUGH_CLAWS, Abilities.NONE, Abilities.TOUGH_CLAWS, 634, 78, 130, 111, 130, 85, 100, 45, 50, 267), new PokemonForm("Mega Y", SpeciesFormKey.MEGA_Y, Type.FIRE, Type.FLYING, 1.7, 100.5, Abilities.DROUGHT, Abilities.NONE, Abilities.DROUGHT, 634, 78, 104, 78, 159, 115, 100, 45, 50, 267), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 90.5, Abilities.SOLAR_POWER, Abilities.SOLAR_POWER, Abilities.SOLAR_POWER, 634, 118, 84, 93, 139, 110, 100, 45, 50, 267), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.FLYING, 28, 999.9, Abilities.BERSERK, Abilities.BERSERK, Abilities.BERSERK, 634, 118, 84, 93, 139, 110, 100, 45, 50, 267), ), new PokemonSpecies(Species.SQUIRTLE, 1, false, false, false, "Tiny Turtle Pokémon", Type.WATER, null, 0.5, 9, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 314, 44, 48, 65, 50, 64, 43, 45, 50, 63, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.WARTORTLE, 1, false, false, false, "Turtle Pokémon", Type.WATER, null, 1, 22.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 405, 59, 63, 80, 65, 80, 58, 45, 50, 142, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.BLASTOISE, 1, false, false, false, "Shellfish Pokémon", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.6, 85.5, Abilities.TORRENT, Abilities.NONE, Abilities.RAIN_DISH, 530, 79, 83, 100, 85, 105, 78, 45, 50, 265, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.WATER, null, 1.6, 101.1, Abilities.MEGA_LAUNCHER, Abilities.NONE, Abilities.MEGA_LAUNCHER, 630, 79, 103, 120, 135, 115, 78, 45, 50, 265), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 85.5, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 630, 119, 83, 130, 115, 115, 68, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.STEEL, 25, 999.9, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 630, 119, 83, 130, 115, 115, 68, 45, 50, 265), ), new PokemonSpecies(Species.CATERPIE, 1, false, false, false, "Worm Pokémon", Type.BUG, null, 0.3, 2.9, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 45, 30, 35, 20, 20, 45, 255, 50, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.METAPOD, 1, false, false, false, "Cocoon Pokémon", Type.BUG, null, 0.7, 9.9, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 50, 20, 55, 25, 25, 30, 120, 50, 72, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.BUTTERFREE, 1, false, false, false, "Butterfly Pokémon", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, GrowthRate.MEDIUM_FAST, 50, true, true, new PokemonForm("Normal", "", Type.BUG, Type.FLYING, 1.1, 32, Abilities.COMPOUND_EYES, Abilities.NONE, Abilities.TINTED_LENS, 395, 60, 45, 50, 90, 80, 70, 45, 50, 198, true, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 32, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.FLYING, 17, 999.9, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, Abilities.COMPOUND_EYES, 495, 85, 35, 80, 120, 90, 85, 45, 50, 198, true), ), new PokemonSpecies(Species.WEEDLE, 1, false, false, false, "Hairy Bug Pokémon", Type.BUG, Type.POISON, 0.3, 3.2, Abilities.SHIELD_DUST, Abilities.NONE, Abilities.RUN_AWAY, 195, 40, 35, 30, 20, 20, 50, 255, 70, 39, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KAKUNA, 1, false, false, false, "Cocoon Pokémon", Type.BUG, Type.POISON, 0.6, 10, Abilities.SHED_SKIN, Abilities.NONE, Abilities.SHED_SKIN, 205, 45, 25, 50, 25, 25, 35, 120, 70, 72, GrowthRate.MEDIUM_FAST, 50, false), @@ -986,7 +994,7 @@ export function initSpecies() { new PokemonForm("Cute Cosplay", "cute-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Smart Cosplay", "smart-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom new PokemonForm("Tough Cosplay", "tough-cosplay", Type.ELECTRIC, null, 0.4, 6, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 430, 45, 80, 50, 75, 60, 120, 190, 50, 112, true, null, true), //Custom - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 6, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, null, 21, 999.9, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 530, 125, 95, 60, 90, 70, 90, 190, 50, 112), //+100 BST from Partner Form ), new PokemonSpecies(Species.RAICHU, 1, false, false, false, "Mouse Pokémon", Type.ELECTRIC, null, 0.8, 30, Abilities.STATIC, Abilities.NONE, Abilities.LIGHTNING_ROD, 485, 60, 90, 55, 90, 80, 110, 75, 50, 243, GrowthRate.MEDIUM_FAST, 50, true), new PokemonSpecies(Species.SANDSHREW, 1, false, false, false, "Mouse Pokémon", Type.GROUND, null, 0.6, 12, Abilities.SAND_VEIL, Abilities.NONE, Abilities.SAND_RUSH, 300, 50, 75, 85, 20, 30, 40, 255, 50, 60, GrowthRate.MEDIUM_FAST, 50, false), @@ -1016,7 +1024,7 @@ export function initSpecies() { new PokemonSpecies(Species.DUGTRIO, 1, false, false, false, "Mole Pokémon", Type.GROUND, null, 0.7, 33.3, Abilities.SAND_VEIL, Abilities.ARENA_TRAP, Abilities.SAND_FORCE, 425, 35, 100, 50, 50, 70, 120, 50, 50, 149, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.MEOWTH, 1, false, false, false, "Scratch Cat Pokémon", Type.NORMAL, null, 0.4, 4.2, Abilities.PICKUP, Abilities.TECHNICIAN, Abilities.UNNERVE, 290, 40, 45, 35, 40, 40, 90, 255, 50, 58, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 0.4, 4.2, Abilities.PICKUP, Abilities.TECHNICIAN, Abilities.UNNERVE, 290, 40, 45, 35, 40, 40, 90, 255, 50, 58, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 33, 4.2, Abilities.TECHNICIAN, Abilities.TECHNICIAN, Abilities.TECHNICIAN, 540, 115, 110, 65, 65, 70, 115, 255, 50, 58), //+100 BST from Persian + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 33, 999.9, Abilities.TECHNICIAN, Abilities.TECHNICIAN, Abilities.TECHNICIAN, 540, 115, 110, 65, 65, 70, 115, 255, 50, 58), //+100 BST from Persian ), new PokemonSpecies(Species.PERSIAN, 1, false, false, false, "Classy Cat Pokémon", Type.NORMAL, null, 1, 32, Abilities.LIMBER, Abilities.TECHNICIAN, Abilities.UNNERVE, 440, 65, 70, 60, 65, 65, 115, 90, 50, 154, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.PSYDUCK, 1, false, false, false, "Duck Pokémon", Type.WATER, null, 0.8, 19.6, Abilities.DAMP, Abilities.CLOUD_NINE, Abilities.SWIFT_SWIM, 320, 50, 52, 48, 65, 50, 55, 190, 50, 64, GrowthRate.MEDIUM_FAST, 50, false), @@ -1038,7 +1046,7 @@ export function initSpecies() { new PokemonSpecies(Species.MACHOKE, 1, false, false, false, "Superpower Pokémon", Type.FIGHTING, null, 1.5, 70.5, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 405, 80, 100, 70, 50, 60, 45, 90, 50, 142, GrowthRate.MEDIUM_SLOW, 75, false), new PokemonSpecies(Species.MACHAMP, 1, false, false, false, "Superpower Pokémon", Type.FIGHTING, null, 1.6, 130, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 505, 90, 130, 80, 65, 85, 55, 45, 50, 253, GrowthRate.MEDIUM_SLOW, 75, false, true, new PokemonForm("Normal", "", Type.FIGHTING, null, 1.6, 130, Abilities.GUTS, Abilities.NO_GUARD, Abilities.STEADFAST, 505, 90, 130, 80, 65, 85, 55, 45, 50, 253, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIGHTING, null, 25, 130, Abilities.GUTS, Abilities.GUTS, Abilities.GUTS, 605, 115, 170, 95, 65, 95, 65, 45, 50, 253), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIGHTING, null, 25, 999.9, Abilities.GUTS, Abilities.GUTS, Abilities.GUTS, 605, 115, 170, 95, 65, 95, 65, 45, 50, 253), ), new PokemonSpecies(Species.BELLSPROUT, 1, false, false, false, "Flower Pokémon", Type.GRASS, Type.POISON, 0.7, 4, Abilities.CHLOROPHYLL, Abilities.NONE, Abilities.GLUTTONY, 300, 50, 75, 35, 70, 30, 40, 255, 70, 60, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.WEEPINBELL, 1, false, false, false, "Flycatcher Pokémon", Type.GRASS, Type.POISON, 1, 6.4, Abilities.CHLOROPHYLL, Abilities.NONE, Abilities.GLUTTONY, 390, 65, 90, 50, 85, 45, 55, 120, 70, 137, GrowthRate.MEDIUM_SLOW, 50, false), @@ -1071,7 +1079,7 @@ export function initSpecies() { new PokemonSpecies(Species.GENGAR, 1, false, false, false, "Shadow Pokémon", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.GHOST, Type.POISON, 1.5, 40.5, Abilities.CURSED_BODY, Abilities.NONE, Abilities.NONE, 500, 60, 65, 60, 130, 75, 110, 45, 50, 250, false, null, true), new PokemonForm("Mega", SpeciesFormKey.MEGA, Type.GHOST, Type.POISON, 1.4, 40.5, Abilities.SHADOW_TAG, Abilities.NONE, Abilities.NONE, 600, 60, 65, 80, 170, 95, 130, 45, 50, 250), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 40.5, Abilities.CURSED_BODY, Abilities.CURSED_BODY, Abilities.CURSED_BODY, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GHOST, Type.POISON, 20, 999.9, Abilities.CURSED_BODY, Abilities.CURSED_BODY, Abilities.CURSED_BODY, 600, 140, 65, 70, 140, 85, 100, 45, 50, 250), ), new PokemonSpecies(Species.ONIX, 1, false, false, false, "Rock Snake Pokémon", Type.ROCK, Type.GROUND, 8.8, 210, Abilities.ROCK_HEAD, Abilities.STURDY, Abilities.WEAK_ARMOR, 385, 35, 45, 160, 30, 45, 70, 45, 50, 77, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DROWZEE, 1, false, false, false, "Hypnosis Pokémon", Type.PSYCHIC, null, 1, 32.4, Abilities.INSOMNIA, Abilities.FOREWARN, Abilities.INNER_FOCUS, 328, 60, 48, 45, 43, 90, 42, 190, 70, 66, GrowthRate.MEDIUM_FAST, 50, false), @@ -1079,7 +1087,7 @@ export function initSpecies() { new PokemonSpecies(Species.KRABBY, 1, false, false, false, "River Crab Pokémon", Type.WATER, null, 0.4, 6.5, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 325, 30, 105, 90, 25, 25, 50, 225, 50, 65, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.KINGLER, 1, false, false, false, "Pincer Pokémon", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.3, 60, Abilities.HYPER_CUTTER, Abilities.SHELL_ARMOR, Abilities.SHEER_FORCE, 475, 55, 130, 115, 50, 50, 75, 60, 50, 166, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 60, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 80, 70, 60, 50, 166), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 19, 999.9, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, Abilities.TOUGH_CLAWS, 575, 90, 155, 140, 50, 80, 70, 60, 50, 166), ), new PokemonSpecies(Species.VOLTORB, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 0.5, 10.4, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 330, 40, 30, 50, 55, 55, 100, 190, 70, 66, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.ELECTRODE, 1, false, false, false, "Ball Pokémon", Type.ELECTRIC, null, 1.2, 66.6, Abilities.SOUNDPROOF, Abilities.STATIC, Abilities.AFTERMATH, 490, 60, 50, 70, 80, 80, 150, 60, 70, 172, GrowthRate.MEDIUM_FAST, null, false), @@ -1123,13 +1131,13 @@ export function initSpecies() { ), new PokemonSpecies(Species.LAPRAS, 1, false, false, false, "Transport Pokémon", Type.WATER, Type.ICE, 2.5, 220, Abilities.WATER_ABSORB, Abilities.SHELL_ARMOR, Abilities.HYDRATION, 535, 130, 85, 80, 85, 95, 60, 45, 50, 187, GrowthRate.SLOW, 50, false, true, new PokemonForm("Normal", "", Type.WATER, Type.ICE, 2.5, 220, Abilities.WATER_ABSORB, Abilities.SHELL_ARMOR, Abilities.HYDRATION, 535, 130, 85, 80, 85, 95, 60, 45, 50, 187, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ICE, 24, 220, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 635, 170, 85, 95, 115, 110, 60, 45, 50, 187), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ICE, 24, 999.9, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, Abilities.SHELL_ARMOR, 635, 170, 85, 95, 115, 110, 60, 45, 50, 187), ), new PokemonSpecies(Species.DITTO, 1, false, false, false, "Transform Pokémon", Type.NORMAL, null, 0.3, 4, Abilities.LIMBER, Abilities.NONE, Abilities.IMPOSTER, 288, 48, 48, 48, 48, 48, 48, 35, 50, 101, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.EEVEE, 1, false, false, false, "Evolution Pokémon", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 325, 55, 55, 50, 45, 65, 55, 45, 50, 65, GrowthRate.MEDIUM_FAST, 87.5, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 325, 55, 55, 50, 45, 65, 55, 45, 50, 65, false, null, true), new PokemonForm("Partner", "partner", Type.NORMAL, null, 0.3, 6.5, Abilities.RUN_AWAY, Abilities.ADAPTABILITY, Abilities.ANTICIPATION, 435, 65, 75, 70, 65, 85, 75, 45, 50, 65, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 18, 6.5, Abilities.PROTEAN, Abilities.PROTEAN, Abilities.PROTEAN, 535, 105, 95, 70, 95, 85, 85, 45, 50, 65), //+100 BST from Partner Form + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 18, 999.9, Abilities.PROTEAN, Abilities.PROTEAN, Abilities.PROTEAN, 535, 110, 90, 70, 95, 85, 85, 45, 50, 65), //+100 BST from Partner Form ), new PokemonSpecies(Species.VAPOREON, 1, false, false, false, "Bubble Jet Pokémon", Type.WATER, null, 1, 29, Abilities.WATER_ABSORB, Abilities.NONE, Abilities.HYDRATION, 525, 130, 65, 60, 110, 95, 65, 45, 50, 184, GrowthRate.MEDIUM_FAST, 87.5, false), new PokemonSpecies(Species.JOLTEON, 1, false, false, false, "Lightning Pokémon", Type.ELECTRIC, null, 0.8, 24.5, Abilities.VOLT_ABSORB, Abilities.NONE, Abilities.QUICK_FEET, 525, 65, 65, 60, 110, 95, 130, 45, 50, 184, GrowthRate.MEDIUM_FAST, 87.5, false), @@ -1145,7 +1153,7 @@ export function initSpecies() { ), new PokemonSpecies(Species.SNORLAX, 1, false, false, false, "Sleeping Pokémon", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, GrowthRate.SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.NORMAL, null, 2.1, 460, Abilities.IMMUNITY, Abilities.THICK_FAT, Abilities.GLUTTONY, 540, 160, 110, 65, 65, 110, 30, 25, 50, 189, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 35, 460, Abilities.HARVEST, Abilities.HARVEST, Abilities.HARVEST, 640, 200, 135, 80, 80, 125, 20, 25, 50, 189), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.NORMAL, null, 35, 999.9, Abilities.HARVEST, Abilities.HARVEST, Abilities.HARVEST, 640, 200, 135, 80, 80, 125, 20, 25, 50, 189), ), new PokemonSpecies(Species.ARTICUNO, 1, true, false, false, "Freeze Pokémon", Type.ICE, Type.FLYING, 1.7, 55.4, Abilities.PRESSURE, Abilities.NONE, Abilities.SNOW_CLOAK, 580, 90, 85, 100, 95, 125, 85, 3, 35, 290, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.ZAPDOS, 1, true, false, false, "Electric Pokémon", Type.ELECTRIC, Type.FLYING, 1.6, 52.6, Abilities.PRESSURE, Abilities.NONE, Abilities.STATIC, 580, 90, 90, 85, 125, 90, 100, 3, 35, 290, GrowthRate.SLOW, null, false), @@ -1785,7 +1793,7 @@ export function initSpecies() { new PokemonSpecies(Species.TRUBBISH, 5, false, false, false, "Trash Bag Pokémon", Type.POISON, null, 0.6, 31, Abilities.STENCH, Abilities.STICKY_HOLD, Abilities.AFTERMATH, 329, 50, 50, 62, 40, 62, 65, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GARBODOR, 5, false, false, false, "Trash Heap Pokémon", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.POISON, null, 1.9, 107.3, Abilities.STENCH, Abilities.WEAK_ARMOR, Abilities.AFTERMATH, 474, 80, 95, 82, 60, 82, 75, 60, 50, 166, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.POISON, Type.STEEL, 21, 107.3, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, 574, 135, 125, 102, 57, 102, 53, 60, 50, 166), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.POISON, Type.STEEL, 21, 999.9, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, Abilities.TOXIC_DEBRIS, 574, 135, 125, 102, 57, 102, 53, 60, 50, 166), ), new PokemonSpecies(Species.ZORUA, 5, false, false, false, "Tricky Fox Pokémon", Type.DARK, null, 0.7, 12.5, Abilities.ILLUSION, Abilities.NONE, Abilities.NONE, 330, 40, 65, 40, 80, 40, 65, 75, 50, 66, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.ZOROARK, 5, false, false, false, "Illusion Fox Pokémon", Type.DARK, null, 1.6, 81.1, Abilities.ILLUSION, Abilities.NONE, Abilities.NONE, 510, 60, 105, 60, 120, 60, 105, 45, 50, 179, GrowthRate.MEDIUM_SLOW, 87.5, false), @@ -2259,25 +2267,25 @@ export function initSpecies() { new PokemonSpecies(Species.MELTAN, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 0.2, 8, Abilities.MAGNET_PULL, Abilities.NONE, Abilities.NONE, 300, 46, 65, 65, 55, 35, 34, 3, 0, 150, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.MELMETAL, 7, false, false, true, "Hex Nut Pokémon", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 2.5, 800, Abilities.IRON_FIST, Abilities.NONE, Abilities.NONE, 600, 135, 143, 143, 80, 65, 34, 3, 0, 300, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 800, Abilities.IRON_FIST, Abilities.IRON_FIST, Abilities.IRON_FIST, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, null, 25, 999.9, Abilities.IRON_FIST, Abilities.IRON_FIST, Abilities.IRON_FIST, 700, 175, 165, 155, 85, 75, 45, 3, 0, 300), ), new PokemonSpecies(Species.GROOKEY, 8, false, false, false, "Chimp Pokémon", Type.GRASS, null, 0.3, 5, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 310, 50, 65, 50, 40, 40, 65, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.THWACKEY, 8, false, false, false, "Beat Pokémon", Type.GRASS, null, 0.7, 14, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 420, 70, 85, 70, 55, 60, 80, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RILLABOOM, 8, false, false, false, "Drummer Pokémon", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.GRASS, null, 2.1, 90, Abilities.OVERGROW, Abilities.NONE, Abilities.GRASSY_SURGE, 530, 100, 125, 90, 60, 70, 85, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 90, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, null, 28, 999.9, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, Abilities.GRASSY_SURGE, 630, 125, 150, 105, 85, 85, 80, 45, 50, 265), ), new PokemonSpecies(Species.SCORBUNNY, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.3, 4.5, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 310, 50, 71, 40, 40, 40, 69, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.RABOOT, 8, false, false, false, "Rabbit Pokémon", Type.FIRE, null, 0.6, 9, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 420, 65, 86, 60, 55, 60, 94, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.CINDERACE, 8, false, false, false, "Striker Pokémon", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.FIRE, null, 1.4, 33, Abilities.BLAZE, Abilities.NONE, Abilities.LIBERO, 530, 80, 116, 75, 65, 75, 119, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 33, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, null, 27, 999.9, Abilities.LIBERO, Abilities.LIBERO, Abilities.LIBERO, 630, 100, 146, 80, 90, 80, 134, 45, 50, 265), ), new PokemonSpecies(Species.SOBBLE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.3, 4, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 310, 50, 40, 40, 70, 40, 70, 45, 50, 62, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.DRIZZILE, 8, false, false, false, "Water Lizard Pokémon", Type.WATER, null, 0.7, 11.5, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 420, 65, 60, 55, 95, 55, 90, 45, 50, 147, GrowthRate.MEDIUM_SLOW, 87.5, false), new PokemonSpecies(Species.INTELEON, 8, false, false, false, "Secret Agent Pokémon", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, GrowthRate.MEDIUM_SLOW, 87.5, false, true, new PokemonForm("Normal", "", Type.WATER, null, 1.9, 45.2, Abilities.TORRENT, Abilities.NONE, Abilities.SNIPER, 530, 70, 85, 65, 125, 65, 120, 45, 50, 265, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 45.2, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, null, 40, 999.9, Abilities.SNIPER, Abilities.SNIPER, Abilities.SNIPER, 630, 95, 97, 77, 147, 77, 137, 45, 50, 265), ), new PokemonSpecies(Species.SKWOVET, 8, false, false, false, "Cheeky Pokémon", Type.NORMAL, null, 0.3, 2.5, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 275, 70, 55, 55, 35, 35, 25, 255, 50, 55, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.GREEDENT, 8, false, false, false, "Greedy Pokémon", Type.NORMAL, null, 0.6, 6, Abilities.CHEEK_POUCH, Abilities.NONE, Abilities.GLUTTONY, 460, 120, 95, 95, 55, 75, 20, 90, 50, 161, GrowthRate.MEDIUM_FAST, 50, false), @@ -2285,13 +2293,13 @@ export function initSpecies() { new PokemonSpecies(Species.CORVISQUIRE, 8, false, false, false, "Raven Pokémon", Type.FLYING, null, 0.8, 16, Abilities.KEEN_EYE, Abilities.UNNERVE, Abilities.BIG_PECKS, 365, 68, 67, 55, 43, 55, 77, 120, 50, 128, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.CORVIKNIGHT, 8, false, false, false, "Raven Pokémon", Type.FLYING, Type.STEEL, 2.2, 75, Abilities.PRESSURE, Abilities.UNNERVE, Abilities.MIRROR_ARMOR, 495, 98, 87, 105, 53, 85, 67, 45, 50, 248, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.FLYING, Type.STEEL, 2.2, 75, Abilities.PRESSURE, Abilities.UNNERVE, Abilities.MIRROR_ARMOR, 495, 98, 87, 105, 53, 85, 67, 45, 50, 248, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FLYING, Type.STEEL, 14, 75, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, 595, 128, 102, 140, 53, 95, 77, 45, 50, 248), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FLYING, Type.STEEL, 14, 999.9, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, Abilities.MIRROR_ARMOR, 595, 128, 102, 140, 53, 95, 77, 45, 50, 248), ), new PokemonSpecies(Species.BLIPBUG, 8, false, false, false, "Larva Pokémon", Type.BUG, null, 0.4, 8, Abilities.SWARM, Abilities.COMPOUND_EYES, Abilities.TELEPATHY, 180, 25, 20, 20, 25, 45, 45, 255, 50, 36, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DOTTLER, 8, false, false, false, "Radome Pokémon", Type.BUG, Type.PSYCHIC, 0.4, 19.5, Abilities.SWARM, Abilities.COMPOUND_EYES, Abilities.TELEPATHY, 335, 50, 35, 80, 50, 90, 30, 120, 50, 117, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.ORBEETLE, 8, false, false, false, "Seven Spot Pokémon", Type.BUG, Type.PSYCHIC, 0.4, 40.8, Abilities.SWARM, Abilities.FRISK, Abilities.TELEPATHY, 505, 60, 45, 110, 80, 120, 90, 45, 50, 253, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.BUG, Type.PSYCHIC, 0.4, 40.8, Abilities.SWARM, Abilities.FRISK, Abilities.TELEPATHY, 505, 60, 45, 110, 80, 120, 90, 45, 50, 253, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.PSYCHIC, 14, 40.8, Abilities.TRACE, Abilities.TRACE, Abilities.TRACE, 605, 90, 45, 130, 110, 140, 90, 45, 50, 253), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.BUG, Type.PSYCHIC, 14, 999.9, Abilities.TRACE, Abilities.TRACE, Abilities.TRACE, 605, 90, 45, 130, 110, 140, 90, 45, 50, 253), ), new PokemonSpecies(Species.NICKIT, 8, false, false, false, "Fox Pokémon", Type.DARK, null, 0.6, 8.9, Abilities.RUN_AWAY, Abilities.UNBURDEN, Abilities.STAKEOUT, 245, 40, 28, 28, 47, 52, 50, 255, 50, 49, GrowthRate.FAST, 50, false), new PokemonSpecies(Species.THIEVUL, 8, false, false, false, "Fox Pokémon", Type.DARK, null, 1.2, 19.9, Abilities.RUN_AWAY, Abilities.UNBURDEN, Abilities.STAKEOUT, 455, 70, 58, 58, 87, 92, 90, 127, 50, 159, GrowthRate.FAST, 50, false), @@ -2302,7 +2310,7 @@ export function initSpecies() { new PokemonSpecies(Species.CHEWTLE, 8, false, false, false, "Snapping Pokémon", Type.WATER, null, 0.3, 8.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 284, 50, 64, 50, 38, 38, 44, 255, 50, 57, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.DREDNAW, 8, false, false, false, "Bite Pokémon", Type.WATER, Type.ROCK, 1, 115.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 485, 90, 115, 90, 48, 68, 74, 75, 50, 170, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.WATER, Type.ROCK, 1, 115.5, Abilities.STRONG_JAW, Abilities.SHELL_ARMOR, Abilities.SWIFT_SWIM, 485, 90, 115, 90, 48, 68, 74, 75, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ROCK, 24, 115.5, Abilities.STRONG_JAW, Abilities.STRONG_JAW, Abilities.STRONG_JAW, 585, 115, 145, 115, 43, 83, 84, 75, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.WATER, Type.ROCK, 24, 999.9, Abilities.STRONG_JAW, Abilities.STRONG_JAW, Abilities.STRONG_JAW, 585, 115, 145, 115, 43, 83, 84, 75, 50, 170), ), new PokemonSpecies(Species.YAMPER, 8, false, false, false, "Puppy Pokémon", Type.ELECTRIC, null, 0.3, 13.5, Abilities.BALL_FETCH, Abilities.NONE, Abilities.RATTLED, 270, 59, 45, 50, 40, 50, 26, 255, 50, 54, GrowthRate.FAST, 50, false), new PokemonSpecies(Species.BOLTUND, 8, false, false, false, "Dog Pokémon", Type.ELECTRIC, null, 1, 34, Abilities.STRONG_JAW, Abilities.NONE, Abilities.COMPETITIVE, 490, 69, 90, 60, 90, 60, 121, 45, 50, 172, GrowthRate.FAST, 50, false), @@ -2310,21 +2318,21 @@ export function initSpecies() { new PokemonSpecies(Species.CARKOL, 8, false, false, false, "Coal Pokémon", Type.ROCK, Type.FIRE, 1.1, 78, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 410, 80, 60, 90, 60, 70, 50, 120, 50, 144, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.COALOSSAL, 8, false, false, false, "Coal Pokémon", Type.ROCK, Type.FIRE, 2.8, 310.5, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 510, 110, 80, 120, 80, 90, 30, 45, 50, 255, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Normal", "", Type.ROCK, Type.FIRE, 2.8, 310.5, Abilities.STEAM_ENGINE, Abilities.FLAME_BODY, Abilities.FLASH_FIRE, 510, 110, 80, 120, 80, 90, 30, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ROCK, Type.FIRE, 42, 310.5, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, 610, 140, 95, 130, 95, 110, 40, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ROCK, Type.FIRE, 42, 999.9, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, Abilities.STEAM_ENGINE, 610, 140, 95, 130, 95, 110, 40, 45, 50, 255), ), new PokemonSpecies(Species.APPLIN, 8, false, false, false, "Apple Core Pokémon", Type.GRASS, Type.DRAGON, 0.2, 0.5, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.BULLETPROOF, 260, 40, 40, 80, 40, 40, 20, 255, 50, 52, GrowthRate.ERRATIC, 50, false), new PokemonSpecies(Species.FLAPPLE, 8, false, false, false, "Apple Wing Pokémon", Type.GRASS, Type.DRAGON, 0.3, 1, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.HUSTLE, 485, 70, 110, 80, 95, 60, 70, 45, 50, 170, GrowthRate.ERRATIC, 50, false, true, new PokemonForm("Normal", "", Type.GRASS, Type.DRAGON, 0.3, 1, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.HUSTLE, 485, 70, 110, 80, 95, 60, 70, 45, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 1, Abilities.HUSTLE, Abilities.HUSTLE, Abilities.HUSTLE, 585, 90, 130, 100, 85, 80, 100, 45, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 999.9, Abilities.HUSTLE, Abilities.HUSTLE, Abilities.HUSTLE, 585, 90, 130, 100, 85, 80, 100, 45, 50, 170), ), new PokemonSpecies(Species.APPLETUN, 8, false, false, false, "Apple Nectar Pokémon", Type.GRASS, Type.DRAGON, 0.4, 13, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.THICK_FAT, 485, 110, 85, 80, 100, 80, 30, 45, 50, 170, GrowthRate.ERRATIC, 50, false, true, new PokemonForm("Normal", "", Type.GRASS, Type.DRAGON, 0.4, 13, Abilities.RIPEN, Abilities.GLUTTONY, Abilities.THICK_FAT, 485, 110, 85, 80, 100, 80, 30, 45, 50, 170, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 13, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 585, 130, 75, 115, 125, 115, 25, 45, 50, 170), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GRASS, Type.DRAGON, 24, 999.9, Abilities.THICK_FAT, Abilities.THICK_FAT, Abilities.THICK_FAT, 585, 130, 75, 115, 125, 115, 25, 45, 50, 170), ), new PokemonSpecies(Species.SILICOBRA, 8, false, false, false, "Sand Snake Pokémon", Type.GROUND, null, 2.2, 7.6, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 315, 52, 57, 75, 35, 50, 46, 255, 50, 63, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.SANDACONDA, 8, false, false, false, "Sand Snake Pokémon", Type.GROUND, null, 3.8, 65.5, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 510, 72, 107, 125, 65, 70, 71, 120, 50, 179, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.GROUND, null, 3.8, 65.5, Abilities.SAND_SPIT, Abilities.SHED_SKIN, Abilities.SAND_VEIL, 510, 72, 107, 125, 65, 70, 71, 120, 50, 179, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GROUND, null, 22, 65.5, Abilities.SAND_SPIT, Abilities.SAND_SPIT, Abilities.SAND_SPIT, 610, 117, 137, 140, 55, 80, 81, 120, 50, 179), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.GROUND, null, 22, 999.9, Abilities.SAND_SPIT, Abilities.SAND_SPIT, Abilities.SAND_SPIT, 610, 117, 137, 140, 55, 80, 81, 120, 50, 179), ), new PokemonSpecies(Species.CRAMORANT, 8, false, false, false, "Gulp Pokémon", Type.FLYING, Type.WATER, 0.8, 18, Abilities.GULP_MISSILE, Abilities.NONE, Abilities.NONE, 475, 70, 85, 55, 85, 95, 85, 45, 50, 166, GrowthRate.MEDIUM_FAST, 50, false, false, new PokemonForm("Normal", "", Type.FLYING, Type.WATER, 0.8, 18, Abilities.GULP_MISSILE, Abilities.NONE, Abilities.NONE, 475, 70, 85, 55, 85, 95, 85, 45, 50, 166, false, null, true), @@ -2337,12 +2345,12 @@ export function initSpecies() { new PokemonSpecies(Species.TOXTRICITY, 8, false, false, false, "Punk Pokémon", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.PLUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, GrowthRate.MEDIUM_SLOW, 50, false, true, new PokemonForm("Amped Form", "amped", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.PLUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, false, "", true), new PokemonForm("Low-Key Form", "lowkey", Type.ELECTRIC, Type.POISON, 1.6, 40, Abilities.PUNK_ROCK, Abilities.MINUS, Abilities.TECHNICIAN, 502, 75, 98, 70, 114, 70, 75, 45, 50, 176, false, "lowkey", true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, Type.POISON, 24, 40, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, 602, 114, 98, 82, 144, 82, 82, 45, 50, 176), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.ELECTRIC, Type.POISON, 24, 999.9, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, Abilities.PUNK_ROCK, 602, 114, 98, 82, 144, 82, 82, 45, 50, 176), ), new PokemonSpecies(Species.SIZZLIPEDE, 8, false, false, false, "Radiator Pokémon", Type.FIRE, Type.BUG, 0.7, 1, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 305, 50, 65, 45, 50, 50, 45, 190, 50, 61, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.CENTISKORCH, 8, false, false, false, "Radiator Pokémon", Type.FIRE, Type.BUG, 3, 120, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 525, 100, 115, 65, 90, 90, 65, 75, 50, 184, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.FIRE, Type.BUG, 3, 120, Abilities.FLASH_FIRE, Abilities.WHITE_SMOKE, Abilities.FLAME_BODY, 525, 100, 115, 65, 90, 90, 65, 75, 50, 184, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.BUG, 75, 120, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, 625, 140, 145, 75, 90, 100, 75, 75, 50, 184), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FIRE, Type.BUG, 75, 999.9, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, Abilities.FLASH_FIRE, 625, 140, 145, 75, 90, 100, 75, 75, 50, 184), ), new PokemonSpecies(Species.CLOBBOPUS, 8, false, false, false, "Tantrum Pokémon", Type.FIGHTING, null, 0.6, 4, Abilities.LIMBER, Abilities.NONE, Abilities.TECHNICIAN, 310, 50, 68, 60, 50, 50, 32, 180, 50, 62, GrowthRate.MEDIUM_SLOW, 50, false), new PokemonSpecies(Species.GRAPPLOCT, 8, false, false, false, "Jujitsu Pokémon", Type.FIGHTING, null, 1.6, 39, Abilities.LIMBER, Abilities.NONE, Abilities.TECHNICIAN, 480, 80, 118, 90, 70, 80, 42, 45, 50, 168, GrowthRate.MEDIUM_SLOW, 50, false), @@ -2358,13 +2366,13 @@ export function initSpecies() { new PokemonSpecies(Species.HATTREM, 8, false, false, false, "Serene Pokémon", Type.PSYCHIC, null, 0.6, 4.8, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 370, 57, 40, 65, 86, 73, 49, 120, 50, 130, GrowthRate.SLOW, 0, false), new PokemonSpecies(Species.HATTERENE, 8, false, false, false, "Silent Pokémon", Type.PSYCHIC, Type.FAIRY, 2.1, 5.1, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 510, 57, 90, 95, 136, 103, 29, 45, 50, 255, GrowthRate.SLOW, 0, false, true, new PokemonForm("Normal", "", Type.PSYCHIC, Type.FAIRY, 2.1, 5.1, Abilities.HEALER, Abilities.ANTICIPATION, Abilities.MAGIC_BOUNCE, 510, 57, 90, 95, 136, 103, 29, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.PSYCHIC, Type.FAIRY, 26, 5.1, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, 610, 97, 90, 105, 146, 122, 50, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.PSYCHIC, Type.FAIRY, 26, 999.9, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, Abilities.MAGIC_BOUNCE, 610, 97, 90, 105, 146, 122, 50, 45, 50, 255), ), new PokemonSpecies(Species.IMPIDIMP, 8, false, false, false, "Wily Pokémon", Type.DARK, Type.FAIRY, 0.4, 5.5, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 265, 45, 45, 30, 55, 40, 50, 255, 50, 53, GrowthRate.MEDIUM_FAST, 100, false), new PokemonSpecies(Species.MORGREM, 8, false, false, false, "Devious Pokémon", Type.DARK, Type.FAIRY, 0.8, 12.5, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 370, 65, 60, 45, 75, 55, 70, 120, 50, 130, GrowthRate.MEDIUM_FAST, 100, false), new PokemonSpecies(Species.GRIMMSNARL, 8, false, false, false, "Bulk Up Pokémon", Type.DARK, Type.FAIRY, 1.5, 61, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 510, 95, 120, 65, 95, 75, 60, 45, 50, 255, GrowthRate.MEDIUM_FAST, 100, false, true, new PokemonForm("Normal", "", Type.DARK, Type.FAIRY, 1.5, 61, Abilities.PRANKSTER, Abilities.FRISK, Abilities.PICKPOCKET, 510, 95, 120, 65, 95, 75, 60, 45, 50, 255, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.DARK, Type.FAIRY, 32, 61, Abilities.PRANKSTER, Abilities.PRANKSTER, Abilities.PRANKSTER, 610, 135, 138, 77, 110, 85, 65, 45, 50, 255), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.DARK, Type.FAIRY, 32, 999.9, Abilities.PRANKSTER, Abilities.PRANKSTER, Abilities.PRANKSTER, 610, 135, 138, 77, 110, 85, 65, 45, 50, 255), ), new PokemonSpecies(Species.OBSTAGOON, 8, false, false, false, "Blocking Pokémon", Type.DARK, Type.NORMAL, 1.6, 46, Abilities.RECKLESS, Abilities.GUTS, Abilities.DEFIANT, 520, 93, 90, 101, 60, 81, 95, 45, 50, 260, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.PERRSERKER, 8, false, false, false, "Viking Pokémon", Type.STEEL, null, 0.8, 28, Abilities.BATTLE_ARMOR, Abilities.TOUGH_CLAWS, Abilities.STEELY_SPIRIT, 440, 70, 110, 100, 50, 60, 50, 90, 50, 154, GrowthRate.MEDIUM_FAST, 50, false), @@ -2383,7 +2391,7 @@ export function initSpecies() { new PokemonForm("Ruby Swirl", "ruby-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Caramel Swirl", "caramel-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), new PokemonForm("Rainbow Swirl", "rainbow-swirl", Type.FAIRY, null, 0.3, 0.5, Abilities.SWEET_VEIL, Abilities.NONE, Abilities.AROMA_VEIL, 495, 65, 60, 75, 110, 121, 64, 100, 50, 173, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 0.5, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.FAIRY, null, 30, 999.9, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, Abilities.MISTY_SURGE, 595, 135, 60, 75, 130, 131, 64, 100, 50, 173), ), new PokemonSpecies(Species.FALINKS, 8, false, false, false, "Formation Pokémon", Type.FIGHTING, null, 3, 62, Abilities.BATTLE_ARMOR, Abilities.NONE, Abilities.DEFIANT, 470, 65, 100, 100, 70, 60, 75, 45, 50, 165, GrowthRate.MEDIUM_FAST, null, false), new PokemonSpecies(Species.PINCURCHIN, 8, false, false, false, "Sea Urchin Pokémon", Type.ELECTRIC, null, 0.3, 1, Abilities.LIGHTNING_ROD, Abilities.NONE, Abilities.ELECTRIC_SURGE, 435, 48, 101, 95, 91, 85, 15, 75, 50, 152, GrowthRate.MEDIUM_FAST, 50, false), @@ -2405,7 +2413,7 @@ export function initSpecies() { new PokemonSpecies(Species.CUFANT, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 1.2, 100, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 330, 72, 80, 49, 40, 49, 40, 190, 50, 66, GrowthRate.MEDIUM_FAST, 50, false), new PokemonSpecies(Species.COPPERAJAH, 8, false, false, false, "Copperderm Pokémon", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.STEEL, null, 3, 650, Abilities.SHEER_FORCE, Abilities.NONE, Abilities.HEAVY_METAL, 500, 122, 130, 69, 80, 69, 30, 90, 50, 175, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 650, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.GROUND, 23, 999.9, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, Abilities.MOLD_BREAKER, 600, 167, 155, 89, 80, 89, 20, 90, 50, 175), ), new PokemonSpecies(Species.DRACOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.DRAGON, 1.8, 190, Abilities.VOLT_ABSORB, Abilities.HUSTLE, Abilities.SAND_RUSH, 505, 90, 100, 90, 80, 70, 75, 45, 50, 177, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.ARCTOZOLT, 8, false, false, false, "Fossil Pokémon", Type.ELECTRIC, Type.ICE, 2.3, 150, Abilities.VOLT_ABSORB, Abilities.STATIC, Abilities.SLUSH_RUSH, 505, 90, 100, 90, 90, 80, 55, 45, 50, 177, GrowthRate.SLOW, null, false), @@ -2413,7 +2421,7 @@ export function initSpecies() { new PokemonSpecies(Species.ARCTOVISH, 8, false, false, false, "Fossil Pokémon", Type.WATER, Type.ICE, 2, 175, Abilities.WATER_ABSORB, Abilities.ICE_BODY, Abilities.SLUSH_RUSH, 505, 90, 90, 100, 80, 90, 55, 45, 50, 177, GrowthRate.SLOW, null, false), new PokemonSpecies(Species.DURALUDON, 8, false, false, false, "Alloy Pokémon", Type.STEEL, Type.DRAGON, 1.8, 40, Abilities.LIGHT_METAL, Abilities.HEAVY_METAL, Abilities.STALWART, 535, 70, 95, 115, 120, 50, 85, 45, 50, 187, GrowthRate.MEDIUM_FAST, 50, false, true, new PokemonForm("Normal", "", Type.STEEL, Type.DRAGON, 1.8, 40, Abilities.LIGHT_METAL, Abilities.HEAVY_METAL, Abilities.STALWART, 535, 70, 95, 115, 120, 50, 85, 45, 50, 187, false, null, true), - new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.DRAGON, 43, 40, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 635, 100, 105, 119, 166, 57, 88, 45, 50, 187), + new PokemonForm("G-Max", SpeciesFormKey.GIGANTAMAX, Type.STEEL, Type.DRAGON, 43, 999.9, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, Abilities.LIGHTNING_ROD, 635, 100, 110, 120, 175, 60, 70, 45, 50, 187), ), new PokemonSpecies(Species.DREEPY, 8, false, false, false, "Lingering Pokémon", Type.DRAGON, Type.GHOST, 0.5, 2, Abilities.CLEAR_BODY, Abilities.INFILTRATOR, Abilities.CURSED_BODY, 270, 28, 60, 30, 40, 30, 82, 45, 50, 54, GrowthRate.SLOW, 50, false), new PokemonSpecies(Species.DRAKLOAK, 8, false, false, false, "Caretaker Pokémon", Type.DRAGON, Type.GHOST, 1.4, 11, Abilities.CLEAR_BODY, Abilities.INFILTRATOR, Abilities.CURSED_BODY, 410, 68, 80, 50, 60, 50, 102, 45, 50, 144, GrowthRate.SLOW, 50, false), @@ -2428,14 +2436,14 @@ export function initSpecies() { ), new PokemonSpecies(Species.ETERNATUS, 8, false, true, false, "Gigantic Pokémon", Type.POISON, Type.DRAGON, 20, 950, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, GrowthRate.SLOW, null, false, true, new PokemonForm("Normal", "", Type.POISON, Type.DRAGON, 20, 950, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 690, 140, 85, 95, 145, 95, 130, 255, 0, 345, false, null, true), - new PokemonForm("E-Max", "eternamax", Type.POISON, Type.DRAGON, 100, 0, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345), + new PokemonForm("E-Max", "eternamax", Type.POISON, Type.DRAGON, 100, 999.9, Abilities.PRESSURE, Abilities.NONE, Abilities.NONE, 1125, 255, 115, 250, 125, 250, 130, 255, 0, 345), ), new PokemonSpecies(Species.KUBFU, 8, true, false, false, "Wushu Pokémon", Type.FIGHTING, null, 0.6, 12, Abilities.INNER_FOCUS, Abilities.NONE, Abilities.NONE, 385, 60, 90, 60, 53, 50, 72, 3, 50, 77, GrowthRate.SLOW, 87.5, false), new PokemonSpecies(Species.URSHIFU, 8, true, false, false, "Wushu Pokémon", Type.FIGHTING, Type.DARK, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, GrowthRate.SLOW, 87.5, false, true, new PokemonForm("Single Strike Style", "single-strike", Type.FIGHTING, Type.DARK, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, false, "", true), new PokemonForm("Rapid Strike Style", "rapid-strike", Type.FIGHTING, Type.WATER, 1.9, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 550, 100, 130, 100, 63, 60, 97, 3, 50, 275, false, null, true), - new PokemonForm("G-Max Single Strike Style", SpeciesFormKey.GIGANTAMAX_SINGLE, Type.FIGHTING, Type.DARK, 29, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), - new PokemonForm("G-Max Rapid Strike Style", SpeciesFormKey.GIGANTAMAX_RAPID, Type.FIGHTING, Type.WATER, 26, 105, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), + new PokemonForm("G-Max Single Strike Style", SpeciesFormKey.GIGANTAMAX_SINGLE, Type.FIGHTING, Type.DARK, 29, 999.9, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), + new PokemonForm("G-Max Rapid Strike Style", SpeciesFormKey.GIGANTAMAX_RAPID, Type.FIGHTING, Type.WATER, 26, 999.9, Abilities.UNSEEN_FIST, Abilities.NONE, Abilities.NONE, 650, 125, 150, 115, 73, 70, 117, 3, 50, 275), ), new PokemonSpecies(Species.ZARUDE, 8, false, false, true, "Rogue Monkey Pokémon", Type.DARK, Type.GRASS, 1.8, 70, Abilities.LEAF_GUARD, Abilities.NONE, Abilities.NONE, 600, 105, 120, 105, 70, 95, 105, 3, 0, 300, GrowthRate.SLOW, null, false, false, new PokemonForm("Normal", "", Type.DARK, Type.GRASS, 1.8, 70, Abilities.LEAF_GUARD, Abilities.NONE, Abilities.NONE, 600, 105, 120, 105, 70, 95, 105, 3, 0, 300, false, null, true), @@ -3340,6 +3348,7 @@ export function getStarterValueFriendshipCap(value: integer): integer { } } +export const POKERUS_STARTER_COUNT = 5; //adjust here! /** * Method to get the daily list of starters with Pokerus. * @param scene {@linkcode BattleScene} used as part of RNG @@ -3348,10 +3357,9 @@ export function getStarterValueFriendshipCap(value: integer): integer { export function getPokerusStarters(scene: BattleScene): PokemonSpecies[] { const pokerusStarters: PokemonSpecies[] = []; const date = new Date(); - const starterCount = 3; //for easy future adjustment! date.setUTCHours(0, 0, 0, 0); scene.executeWithSeedOffset(() => { - while (pokerusStarters.length < starterCount) { + while (pokerusStarters.length < POKERUS_STARTER_COUNT) { const randomSpeciesId = parseInt(Utils.randSeedItem(Object.keys(speciesStarters)), 10); const species = getPokemonSpecies(randomSpeciesId); if (!pokerusStarters.includes(species)) { diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index ac33f26de9e5..8b96e3cefb83 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1,16 +1,16 @@ -import BattleScene, {startingWave} from "../battle-scene"; -import {ModifierTypeFunc, modifierTypes} from "../modifier/modifier-type"; -import {EnemyPokemon} from "../field/pokemon"; +import BattleScene, { startingWave } from "../battle-scene"; +import { ModifierTypeFunc, modifierTypes } from "../modifier/modifier-type"; +import { EnemyPokemon } from "../field/pokemon"; import * as Utils from "../utils"; -import {PokeballType} from "./pokeball"; -import {pokemonEvolutions, pokemonPrevolutions} from "./pokemon-evolutions"; -import PokemonSpecies, {getPokemonSpecies, PokemonSpeciesFilter} from "./pokemon-species"; -import {tmSpecies} from "./tms"; -import {Type} from "./type"; -import {doubleBattleDialogue} from "./dialogue"; -import {PersistentModifier} from "../modifier/modifier"; -import {TrainerVariant} from "../field/trainer"; -import {getIsInitialized, initI18n} from "#app/plugins/i18n"; +import { PokeballType } from "./pokeball"; +import { pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; +import PokemonSpecies, { getPokemonSpecies, PokemonSpeciesFilter } from "./pokemon-species"; +import { tmSpecies } from "./tms"; +import { Type } from "./type"; +import { doubleBattleDialogue } from "./dialogue"; +import { PersistentModifier } from "../modifier/modifier"; +import { TrainerVariant } from "../field/trainer"; +import { getIsInitialized, initI18n } from "#app/plugins/i18n"; import i18next from "i18next"; import {Moves} from "#enums/moves"; import {PartyMemberStrength} from "#enums/party-member-strength"; @@ -255,7 +255,9 @@ export class TrainerConfig { name = i18next.t("trainerNames:rival"); } } + this.name = name; + return this; } @@ -355,9 +357,9 @@ export class TrainerConfig { /** * Sets the configuration for trainers with genders, including the female name and encounter background music (BGM). - * @param {string} [nameFemale] - The name of the female trainer. If 'Ivy', a localized name will be assigned. - * @param {TrainerType | string} [femaleEncounterBgm] - The encounter BGM for the female trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {string} [nameFemale] The name of the female trainer. If 'Ivy', a localized name will be assigned. + * @param {TrainerType | string} [femaleEncounterBgm] The encounter BGM for the female trainer, which can be a TrainerType or a string. + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { // If the female name is 'Ivy' (the rival), assign a localized name. @@ -392,9 +394,9 @@ export class TrainerConfig { /** * Sets the configuration for trainers with double battles, including the name of the double trainer and the encounter BGM. - * @param nameDouble - The name of the double trainer (e.g., "Ace Duo" for Trainer Class Doubles or "red_blue_double" for NAMED trainer doubles). - * @param doubleEncounterBgm - The encounter BGM for the double trainer, which can be a TrainerType or a string. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param nameDouble The name of the double trainer (e.g., "Ace Duo" for Trainer Class Doubles or "red_blue_double" for NAMED trainer doubles). + * @param doubleEncounterBgm The encounter BGM for the double trainer, which can be a TrainerType or a string. + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setHasDouble(nameDouble: string, doubleEncounterBgm?: TrainerType | string): TrainerConfig { this.hasDouble = true; @@ -407,8 +409,8 @@ export class TrainerConfig { /** * Sets the trainer type for double battles. - * @param trainerTypeDouble - The TrainerType of the partner in a double battle. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param trainerTypeDouble The TrainerType of the partner in a double battle. + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setDoubleTrainerType(trainerTypeDouble: TrainerType): TrainerConfig { this.trainerTypeDouble = trainerTypeDouble; @@ -432,8 +434,8 @@ export class TrainerConfig { /** * Sets the title for double trainers - * @param titleDouble - the key for the title in the i18n file. (e.g., "champion_double"). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param titleDouble The key for the title in the i18n file. (e.g., "champion_double"). + * @returns {TrainerConfig} The updated TrainerConfig instance. */ setDoubleTitle(titleDouble: string): TrainerConfig { // First check if i18n is initialized @@ -625,10 +627,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for an evil team admin. - * @param title - The title of the evil team admin. - * @param poolName - The evil team the admin belongs to. - * @param {Species | Species[]} signatureSpecies - The signature species for the evil team leader. - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param title The title of the evil team admin. + * @param poolName The evil team the admin belongs to. + * @param {Species | Species[]} signatureSpecies The signature species for the evil team leader. + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForEvilTeamAdmin(title: string, poolName: string, signatureSpecies: (Species | Species[])[],): TrainerConfig { if (!getIsInitialized()) { @@ -659,12 +661,49 @@ export class TrainerConfig { return this; } + /** + * Initializes the trainer configuration for a Stat Trainer, as part of the Trainer's Test Mystery Encounter. + * @param {Species | Species[]} signatureSpecies The signature species for the Elite Four member. + * @param {Type[]} specialtyTypes The specialty types for the Stat Trainer. + * @param isMale Whether the Elite Four Member is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. + **/ + initForStatTrainer(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { + if (!getIsInitialized()) { + initI18n(); + } + + this.setPartyTemplates(trainerPartyTemplates.ELITE_FOUR); + + signatureSpecies.forEach((speciesPool, s) => { + if (!Array.isArray(speciesPool)) { + speciesPool = [speciesPool]; + } + this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); + }); + if (specialtyTypes.length) { + this.setSpeciesFilter(p => specialtyTypes.find(t => p.isOfType(t)) !== undefined); + this.setSpecialtyTypes(...specialtyTypes); + } + const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); + this.name = i18next.t(`trainerNames:${nameForCall}`); + this.setMoneyMultiplier(2); + this.setBoss(); + this.setStaticParty(); + + // TODO: replace with more suitable music? + this.setBattleBgm("battle_trainer"); + this.setVictoryBgm("victory_trainer"); + + return this; + } + /** * Initializes the trainer configuration for an evil team leader. Temporarily hardcoding evil leader teams though. - * @param {Species | Species[]} signatureSpecies - The signature species for the evil team leader. - * @param {Type[]} specialtyTypes - The specialty types for the evil team Leader. - * @param boolean whether or not this is the rematch fight - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the evil team leader. + * @param {Type[]} specialtyTypes The specialty types for the evil team Leader. + * @param boolean Whether or not this is the rematch fight + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForEvilTeamLeader(title: string, signatureSpecies: (Species | Species[])[], rematch: boolean = false, ...specialtyTypes: Type[]): TrainerConfig { if (!getIsInitialized()) { @@ -700,10 +739,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Gym Leader. - * @param {Species | Species[]} signatureSpecies - The signature species for the Gym Leader. - * @param {Type[]} specialtyTypes - The specialty types for the Gym Leader. - * @param isMale - Whether the Gym Leader is Male or Not (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Gym Leader. + * @param {Type[]} specialtyTypes The specialty types for the Gym Leader. + * @param isMale Whether the Gym Leader is Male or Not (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. * **/ initForGymLeader(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -757,10 +796,10 @@ export class TrainerConfig { /** * Initializes the trainer configuration for an Elite Four member. - * @param {Species | Species[]} signatureSpecies - The signature species for the Elite Four member. - * @param {Type[]} specialtyTypes - The specialty types for the Elite Four member. - * @param isMale - Whether the Elite Four Member is Male or Female (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Elite Four member. + * @param {Type[]} specialtyTypes The specialty types for the Elite Four member. + * @param isMale Whether the Elite Four Member is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ initForEliteFour(signatureSpecies: (Species | Species[])[], isMale: boolean, ...specialtyTypes: Type[]): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -813,9 +852,9 @@ export class TrainerConfig { /** * Initializes the trainer configuration for a Champion. - * @param {Species | Species[]} signatureSpecies - The signature species for the Champion. - * @param isMale - Whether the Champion is Male or Female (for localization of the title). - * @returns {TrainerConfig} - The updated TrainerConfig instance. + * @param {Species | Species[]} signatureSpecies The signature species for the Champion. + * @param isMale Whether the Champion is Male or Female (for localization of the title). + * @returns {TrainerConfig} The updated TrainerConfig instance. **/ initForChampion(signatureSpecies: (Species | Species[])[], isMale: boolean): TrainerConfig { // Check if the internationalization (i18n) system is initialized. @@ -862,6 +901,20 @@ export class TrainerConfig { return this; } + /** + * Sets a localized name for the trainer. This should only be used for trainers that dont use a "initFor" function and are considered "named" trainers + * @param name - The name of the trainer. + * @returns {TrainerConfig} The updated TrainerConfig instance. + */ + setLocalizedName(name: string): TrainerConfig { + // Check if the internationalization (i18n) system is initialized. + if (!getIsInitialized()) { + initI18n(); + } + this.name = i18next.t(`trainerNames:${name.toLowerCase()}`); + return this; + } + /** * Retrieves the title for the trainer based on the provided trainer slot and variant. * @param {TrainerSlot} trainerSlot - The slot to determine which title to use. Defaults to TrainerSlot.NONE. @@ -956,6 +1009,66 @@ export class TrainerConfig { } }); } + + /** + * Creates a shallow copy of a trainer config so that it can be modified without affecting the {@link trainerConfigs} source map + */ + clone(): TrainerConfig { + let clone = new TrainerConfig(this.trainerType); + clone = this.trainerTypeDouble ? clone.setDoubleTrainerType(this.trainerTypeDouble) : clone; + clone = this.name ? clone.setName(this.name) : clone; + clone = this.hasGenders ? clone.setHasGenders(this.nameFemale, this.femaleEncounterBgm) : clone; + clone = this.hasDouble ? clone.setHasDouble(this.nameDouble, this.doubleEncounterBgm) : clone; + clone = this.title ? clone.setTitle(this.title) : clone; + clone = this.titleDouble ? clone.setDoubleTitle(this.titleDouble) : clone; + clone = this.hasCharSprite ? clone.setHasCharSprite() : clone; + clone = this.doubleOnly ? clone.setDoubleOnly() : clone; + clone = this.moneyMultiplier ? clone.setMoneyMultiplier(this.moneyMultiplier) : clone; + clone = this.isBoss ? clone.setBoss() : clone; + clone = this.hasStaticParty ? clone.setStaticParty() : clone; + clone = this.useSameSeedForAllMembers ? clone.setUseSameSeedForAllMembers() : clone; + clone = this.battleBgm ? clone.setBattleBgm(this.battleBgm) : clone; + clone = this.encounterBgm ? clone.setEncounterBgm(this.encounterBgm) : clone; + clone = this.victoryBgm ? clone.setVictoryBgm(this.victoryBgm) : clone; + clone = this.genModifiersFunc ? clone.setGenModifiersFunc(this.genModifiersFunc) : clone; + + if (this.modifierRewardFuncs) { + // Clones array instead of passing ref + clone.modifierRewardFuncs = this.modifierRewardFuncs.slice(0); + } + + if (this.partyTemplates) { + clone.partyTemplates = this.partyTemplates.slice(0); + } + + clone = this.partyTemplateFunc ? clone.setPartyTemplateFunc(this.partyTemplateFunc) : clone; + + if (this.partyMemberFuncs) { + Object.keys(this.partyMemberFuncs).forEach((index) => { + clone = clone.setPartyMemberFunc(parseInt(index, 10), this.partyMemberFuncs[index]); + }); + } + + clone = this.speciesPools ? clone.setSpeciesPools(this.speciesPools) : clone; + clone = this.speciesFilter ? clone.setSpeciesFilter(this.speciesFilter) : clone; + if (this.specialtyTypes) { + clone.specialtyTypes = this.specialtyTypes.slice(0); + } + + clone.encounterMessages = this.encounterMessages?.slice(0); + clone.victoryMessages = this.victoryMessages?.slice(0); + clone.defeatMessages = this.defeatMessages?.slice(0); + + clone.femaleEncounterMessages = this.femaleEncounterMessages?.slice(0); + clone.femaleVictoryMessages = this.femaleVictoryMessages?.slice(0); + clone.femaleDefeatMessages = this.femaleDefeatMessages?.slice(0); + + clone.doubleEncounterMessages = this.doubleEncounterMessages?.slice(0); + clone.doubleVictoryMessages = this.doubleVictoryMessages?.slice(0); + clone.doubleDefeatMessages = this.doubleDefeatMessages?.slice(0); + + return clone; + } } let t = 0; @@ -2041,4 +2154,154 @@ export const trainerConfigs: TrainerConfigs = { p.generateName(); p.pokeball = PokeballType.ULTRA_BALL; })), + [TrainerType.BUCK]: new TrainerConfig(++t).setName("Buck").initForStatTrainer([], true) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.CLAYDOL ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.COALOSSAL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + if (p.species.speciesId === Species.VENUSAUR) { + p.formIndex = 2; // Gmax + p.abilityIndex = 2; // Venusaur gets Chlorophyll + } else { + p.formIndex = 1; // Gmax + } + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AGGRON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.TORKOAL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.abilityIndex = 1; // Drought + })) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.GREAT_TUSK ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.HEATRAN ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.CHERYL]: new TrainerConfig(++t).setName("Cheryl").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BLISSEY ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.SNORLAX, Species.LAPRAS ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AUDINO ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.GOODRA ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.IRON_HANDS ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.CRESSELIA, Species.ENAMORUS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + if (p.species.speciesId === Species.ENAMORUS) { + p.formIndex = 1; // Therian + p.generateName(); + } + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.MARLEY]: new TrainerConfig(++t).setName("Marley").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ARCANINE ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 3); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.ULTRA_BALL; + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.CINDERACE, Species.INTELEON ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.AERODACTYL ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.formIndex = 1; // Mega + p.generateName(); + })) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.DRAGAPULT ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.IRON_BUNDLE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.REGIELEKI ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.MIRA]: new TrainerConfig(++t).setName("Mira").initForStatTrainer([], false) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.ALAKAZAM ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 1; + p.pokeball = PokeballType.ULTRA_BALL; + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.GENGAR, Species.HATTERENE ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = p.species.speciesId === Species.GENGAR ? 2 : 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.FLUTTER_MANE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.HYDREIGON ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.MAGNEZONE ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.LATIOS, Species.LATIAS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.RILEY]: new TrainerConfig(++t).setName("Riley").initForStatTrainer([], true) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.LUCARIO ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + p.formIndex = 1; + p.pokeball = PokeballType.ULTRA_BALL; + p.generateName(); + })) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.RILLABOOM, Species.CENTISKORCH ], TrainerSlot.TRAINER, true, p => { + p.generateAndPopulateMoveset(); + p.pokeball = PokeballType.GREAT_BALL; + p.formIndex = 1; // Gmax + p.generateName(); + })) + .setPartyMemberFunc(2, getRandomPartyMemberFunc([ Species.TYRANITAR ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(3, getRandomPartyMemberFunc([ Species.ROARING_MOON ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(4, getRandomPartyMemberFunc([ Species.URSALUNA ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.REGIGIGAS, Species.LANDORUS ], TrainerSlot.TRAINER, true, p => { + p.setBoss(true, 2); + p.generateAndPopulateMoveset(); + if (p.species.speciesId === Species.LANDORUS) { + p.formIndex = 1; // Therian + p.generateName(); + } + p.pokeball = PokeballType.MASTER_BALL; + })), + [TrainerType.VICTOR]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Victor") + .setMoneyMultiplier(1) // The Winstrate trainers have total money multiplier of 6 + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VICTORIA]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Victoria") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VIVI]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vivi") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.TWO_AVG_ONE_STRONG), + [TrainerType.VICKY]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vicky") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG), + [TrainerType.VITO]: new TrainerConfig(++t).setTitle("The Winstrates").setLocalizedName("Vito") + .setMoneyMultiplier(2) + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))), + [TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER) + .setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)) }; + diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 657f0d47375b..f367b1b56c0e 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -79,4 +79,5 @@ export enum BattlerTagType { TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", DOUBLE_SHOCKED = "DOUBLE_SHOCKED", + MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", } diff --git a/src/enums/encounter-anims.ts b/src/enums/encounter-anims.ts new file mode 100644 index 000000000000..bd1461473c91 --- /dev/null +++ b/src/enums/encounter-anims.ts @@ -0,0 +1,11 @@ +/** + * Animations used for Mystery Encounters + * These are custom animations that may or may not work in any other circumstance + * Use at your own risk + */ +export enum EncounterAnim { + MAGMA_BG, + MAGMA_SPOUT, + SMOKESCREEN, + DANCE +} diff --git a/src/enums/exp-gains-speed.ts b/src/enums/exp-gains-speed.ts new file mode 100644 index 000000000000..964c4f99c70a --- /dev/null +++ b/src/enums/exp-gains-speed.ts @@ -0,0 +1,22 @@ +/** + * Defines the speed of gaining experience. + * + * @remarks + * The `expGainSpeed` can have several modes: + * - `0` - Default: The normal speed. + * - `1` - Fast: Fast speed. + * - `2` - Faster: Faster speed. + * - `3` - Skip: Skip gaining exp animation. + * + * @default 0 - Uses the default normal speed. + */ +export enum ExpGainsSpeed { + /** The normal speed. */ + DEFAULT, + /** Fast speed. */ + FAST, + /** Faster speed. */ + FASTER, + /** Skip gaining exp animation. */ + SKIP +} diff --git a/src/enums/mystery-encounter-mode.ts b/src/enums/mystery-encounter-mode.ts new file mode 100644 index 000000000000..f1e98ca5b18c --- /dev/null +++ b/src/enums/mystery-encounter-mode.ts @@ -0,0 +1,12 @@ +export enum MysteryEncounterMode { + /** {@linkcode MysteryEncounter} will always begin in this mode, but will always swap modes when an option is selected */ + DEFAULT, + /** If the {@linkcode MysteryEncounter} battle is a trainer type battle */ + TRAINER_BATTLE, + /** If the {@linkcode MysteryEncounter} battle is a wild type battle */ + WILD_BATTLE, + /** Enables special boss music during encounter */ + BOSS_BATTLE, + /** If there is no battle in the {@linkcode MysteryEncounter} or option selected */ + NO_BATTLE +} diff --git a/src/enums/mystery-encounter-option-mode.ts b/src/enums/mystery-encounter-option-mode.ts new file mode 100644 index 000000000000..a994c30581b9 --- /dev/null +++ b/src/enums/mystery-encounter-option-mode.ts @@ -0,0 +1,10 @@ +export enum MysteryEncounterOptionMode { + /** Default style */ + DEFAULT, + /** Disabled on requirements not met, default style on requirements met */ + DISABLED_OR_DEFAULT, + /** Default style on requirements not met, special style on requirements met */ + DEFAULT_OR_SPECIAL, + /** Disabled on requirements not met, special style on requirements met */ + DISABLED_OR_SPECIAL +} diff --git a/src/enums/mystery-encounter-tier.ts b/src/enums/mystery-encounter-tier.ts new file mode 100644 index 000000000000..484acc7aba94 --- /dev/null +++ b/src/enums/mystery-encounter-tier.ts @@ -0,0 +1,11 @@ +/** + * Enum values are base spawn weights of each tier. + * The weights aim for 46.25/31.25/18.5/4% spawn ratios, AFTER accounting for anti-variance and pity mechanisms + */ +export enum MysteryEncounterTier { + COMMON = 66, + GREAT = 40, + ULTRA = 19, + ROGUE = 3, + MASTER = 0 // Not currently used +} diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts new file mode 100644 index 000000000000..39a8087599cf --- /dev/null +++ b/src/enums/mystery-encounter-type.ts @@ -0,0 +1,32 @@ +export enum MysteryEncounterType { + MYSTERIOUS_CHALLENGERS, + MYSTERIOUS_CHEST, + DARK_DEAL, + FIGHT_OR_FLIGHT, + SLUMBERING_SNORLAX, + TRAINING_SESSION, + DEPARTMENT_STORE_SALE, + SHADY_VITAMIN_DEALER, + FIELD_TRIP, + SAFARI_ZONE, + LOST_AT_SEA, + FIERY_FALLOUT, + THE_STRONG_STUFF, + THE_POKEMON_SALESMAN, + AN_OFFER_YOU_CANT_REFUSE, + DELIBIRDY, + ABSOLUTE_AVARICE, + A_TRAINERS_TEST, + TRASH_TO_TREASURE, + BERRIES_ABOUND, + CLOWNING_AROUND, + PART_TIMER, + DANCING_LESSONS, + WEIRD_DREAM, + THE_WINSTRATE_CHALLENGE, + TELEPORTING_HIJINKS, + BUG_TYPE_SUPERFAN, + FUN_AND_GAMES, + UNCOMMON_BREED, + GLOBAL_TRADE_SYSTEM +} diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index 835a2c9d039e..cfc52b70eb07 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -1,235 +1,246 @@ export enum TrainerType { - UNKNOWN, + UNKNOWN, - ACE_TRAINER, - ARTIST, - BACKERS, - BACKPACKER, - BAKER, - BEAUTY, - BIKER, - BLACK_BELT, - BREEDER, - CLERK, - CYCLIST, - DANCER, - DEPOT_AGENT, - DOCTOR, - FIREBREATHER, - FISHERMAN, - GUITARIST, - HARLEQUIN, - HIKER, - HOOLIGANS, - HOOPSTER, - INFIELDER, - JANITOR, - LINEBACKER, - MAID, - MUSICIAN, - HEX_MANIAC, - NURSERY_AIDE, - OFFICER, - PARASOL_LADY, - PILOT, - POKEFAN, - PRESCHOOLER, - PSYCHIC, - RANGER, - RICH, - RICH_KID, - ROUGHNECK, - SAILOR, - SCIENTIST, - SMASHER, - SNOW_WORKER, - STRIKER, - SCHOOL_KID, - SWIMMER, - TWINS, - VETERAN, - WAITER, - WORKER, - YOUNGSTER, - ROCKET_GRUNT, - ARCHER, - ARIANA, - PROTON, - PETREL, - MAGMA_GRUNT, - TABITHA, - COURTNEY, - AQUA_GRUNT, - MATT, - SHELLY, - GALACTIC_GRUNT, - JUPITER, - MARS, - SATURN, - PLASMA_GRUNT, - ZINZOLIN, - ROOD, - FLARE_GRUNT, - BRYONY, - XEROSIC, - AETHER_GRUNT, - FABA, - SKULL_GRUNT, - PLUMERIA, - MACRO_GRUNT, - OLEANA, - ROCKET_BOSS_GIOVANNI_1, - ROCKET_BOSS_GIOVANNI_2, - MAXIE, - MAXIE_2, - ARCHIE, - ARCHIE_2, - CYRUS, - CYRUS_2, - GHETSIS, - GHETSIS_2, - LYSANDRE, - LYSANDRE_2, - LUSAMINE, - LUSAMINE_2, - GUZMA, - GUZMA_2, - ROSE, - ROSE_2, + ACE_TRAINER, + ARTIST, + BACKERS, + BACKPACKER, + BAKER, + BEAUTY, + BIKER, + BLACK_BELT, + BREEDER, + CLERK, + CYCLIST, + DANCER, + DEPOT_AGENT, + DOCTOR, + FIREBREATHER, + FISHERMAN, + GUITARIST, + HARLEQUIN, + HIKER, + HOOLIGANS, + HOOPSTER, + INFIELDER, + JANITOR, + LINEBACKER, + MAID, + MUSICIAN, + HEX_MANIAC, + NURSERY_AIDE, + OFFICER, + PARASOL_LADY, + PILOT, + POKEFAN, + PRESCHOOLER, + PSYCHIC, + RANGER, + RICH, + RICH_KID, + ROUGHNECK, + SAILOR, + SCIENTIST, + SMASHER, + SNOW_WORKER, + STRIKER, + SCHOOL_KID, + SWIMMER, + TWINS, + VETERAN, + WAITER, + WORKER, + YOUNGSTER, + ROCKET_GRUNT, + ARCHER, + ARIANA, + PROTON, + PETREL, + MAGMA_GRUNT, + TABITHA, + COURTNEY, + AQUA_GRUNT, + MATT, + SHELLY, + GALACTIC_GRUNT, + JUPITER, + MARS, + SATURN, + PLASMA_GRUNT, + ZINZOLIN, + ROOD, + FLARE_GRUNT, + BRYONY, + XEROSIC, + AETHER_GRUNT, + FABA, + SKULL_GRUNT, + PLUMERIA, + MACRO_GRUNT, + OLEANA, + ROCKET_BOSS_GIOVANNI_1, + ROCKET_BOSS_GIOVANNI_2, + MAXIE, + MAXIE_2, + ARCHIE, + ARCHIE_2, + CYRUS, + CYRUS_2, + GHETSIS, + GHETSIS_2, + LYSANDRE, + LYSANDRE_2, + LUSAMINE, + LUSAMINE_2, + GUZMA, + GUZMA_2, + ROSE, + ROSE_2, + BUCK, + CHERYL, + MARLEY, + MIRA, + RILEY, + VICTOR, + VICTORIA, + VIVI, + VICKY, + VITO, + BUG_TYPE_SUPERFAN, - BROCK = 200, - MISTY, - LT_SURGE, - ERIKA, - JANINE, - SABRINA, - BLAINE, - GIOVANNI, - FALKNER, - BUGSY, - WHITNEY, - MORTY, - CHUCK, - JASMINE, - PRYCE, - CLAIR, - ROXANNE, - BRAWLY, - WATTSON, - FLANNERY, - NORMAN, - WINONA, - TATE, - LIZA, - JUAN, - ROARK, - GARDENIA, - MAYLENE, - CRASHER_WAKE, - FANTINA, - BYRON, - CANDICE, - VOLKNER, - CILAN, - CHILI, - CRESS, - CHEREN, - LENORA, - ROXIE, - BURGH, - ELESA, - CLAY, - SKYLA, - BRYCEN, - DRAYDEN, - MARLON, - VIOLA, - GRANT, - KORRINA, - RAMOS, - CLEMONT, - VALERIE, - OLYMPIA, - WULFRIC, - MILO, - NESSA, - KABU, - BEA, - ALLISTER, - OPAL, - BEDE, - GORDIE, - MELONY, - PIERS, - MARNIE, - RAIHAN, - KATY, - BRASSIUS, - IONO, - KOFU, - LARRY, - RYME, - TULIP, - GRUSHA, - LORELEI = 300, - BRUNO, - AGATHA, - LANCE, - WILL, - KOGA, - KAREN, - SIDNEY, - PHOEBE, - GLACIA, - DRAKE, - AARON, - BERTHA, - FLINT, - LUCIAN, - SHAUNTAL, - MARSHAL, - GRIMSLEY, - CAITLIN, - MALVA, - SIEBOLD, - WIKSTROM, - DRASNA, - HALA, - MOLAYNE, - OLIVIA, - ACEROLA, - KAHILI, - MARNIE_ELITE, - NESSA_ELITE, - BEA_ELITE, - ALLISTER_ELITE, - RAIHAN_ELITE, - RIKA, - POPPY, - LARRY_ELITE, - HASSEL, - CRISPIN, - AMARYS, - LACEY, - DRAYTON, - BLUE = 350, - RED, - LANCE_CHAMPION, - STEVEN, - WALLACE, - CYNTHIA, - ALDER, - IRIS, - DIANTHA, - HAU, - LEON, - GEETA, - NEMONA, - KIERAN, - RIVAL = 375, - RIVAL_2, - RIVAL_3, - RIVAL_4, - RIVAL_5, - RIVAL_6 + BROCK = 200, + MISTY, + LT_SURGE, + ERIKA, + JANINE, + SABRINA, + BLAINE, + GIOVANNI, + FALKNER, + BUGSY, + WHITNEY, + MORTY, + CHUCK, + JASMINE, + PRYCE, + CLAIR, + ROXANNE, + BRAWLY, + WATTSON, + FLANNERY, + NORMAN, + WINONA, + TATE, + LIZA, + JUAN, + ROARK, + GARDENIA, + MAYLENE, + CRASHER_WAKE, + FANTINA, + BYRON, + CANDICE, + VOLKNER, + CILAN, + CHILI, + CRESS, + CHEREN, + LENORA, + ROXIE, + BURGH, + ELESA, + CLAY, + SKYLA, + BRYCEN, + DRAYDEN, + MARLON, + VIOLA, + GRANT, + KORRINA, + RAMOS, + CLEMONT, + VALERIE, + OLYMPIA, + WULFRIC, + MILO, + NESSA, + KABU, + BEA, + ALLISTER, + OPAL, + BEDE, + GORDIE, + MELONY, + PIERS, + MARNIE, + RAIHAN, + KATY, + BRASSIUS, + IONO, + KOFU, + LARRY, + RYME, + TULIP, + GRUSHA, + LORELEI = 300, + BRUNO, + AGATHA, + LANCE, + WILL, + KOGA, + KAREN, + SIDNEY, + PHOEBE, + GLACIA, + DRAKE, + AARON, + BERTHA, + FLINT, + LUCIAN, + SHAUNTAL, + MARSHAL, + GRIMSLEY, + CAITLIN, + MALVA, + SIEBOLD, + WIKSTROM, + DRASNA, + HALA, + MOLAYNE, + OLIVIA, + ACEROLA, + KAHILI, + MARNIE_ELITE, + NESSA_ELITE, + BEA_ELITE, + ALLISTER_ELITE, + RAIHAN_ELITE, + RIKA, + POPPY, + LARRY_ELITE, + HASSEL, + CRISPIN, + AMARYS, + LACEY, + DRAYTON, + BLUE = 350, + RED, + LANCE_CHAMPION, + STEVEN, + WALLACE, + CYNTHIA, + ALDER, + IRIS, + DIANTHA, + HAU, + LEON, + GEETA, + NEMONA, + KIERAN, + RIVAL = 375, + RIVAL_2, + RIVAL_3, + RIVAL_4, + RIVAL_5, + RIVAL_6 } diff --git a/src/field/arena.ts b/src/field/arena.ts index 9f0a9691dee2..0466c01c82be 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -76,21 +76,21 @@ export class Arena { } } - randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer): PokemonSpecies { + randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer, isBoss?: boolean): PokemonSpecies { const overrideSpecies = this.scene.gameMode.getOverrideSpecies(waveIndex); if (overrideSpecies) { return overrideSpecies; } - const isBoss = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length + const isBossSpecies = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length && (this.biomeType !== Biome.END || this.scene.gameMode.isClassic || this.scene.gameMode.isWaveFinal(waveIndex)); - const randVal = isBoss ? 64 : 512; + const randVal = isBossSpecies ? 64 : 512; // luck influences encounter rarity let luckModifier = 0; if (typeof luckValue !== "undefined") { - luckModifier = luckValue * (isBoss ? 0.5 : 2); + luckModifier = luckValue * (isBossSpecies ? 0.5 : 2); } const tierValue = Utils.randSeedInt(randVal - luckModifier); - let tier = !isBoss + let tier = !isBossSpecies ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; console.log(BiomePoolTier[tier]); @@ -149,7 +149,7 @@ export class Arena { return this.randomSpecies(waveIndex, level, (attempt || 0) + 1); } - const newSpeciesId = ret.getWildSpeciesForLevel(level, true, isBoss, this.scene.gameMode); + const newSpeciesId = ret.getWildSpeciesForLevel(level, true, isBoss ?? isBossSpecies, this.scene.gameMode); if (newSpeciesId !== ret.speciesId) { console.log("Replaced", Species[ret.speciesId], "with", Species[newSpeciesId]); ret = getPokemonSpecies(newSpeciesId); @@ -157,12 +157,12 @@ export class Arena { return ret; } - randomTrainerType(waveIndex: integer): TrainerType { - const isBoss = !!this.trainerPool[BiomePoolTier.BOSS].length - && this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym); + randomTrainerType(waveIndex: integer, isBoss: boolean = false): TrainerType { + const isTrainerBoss = !!this.trainerPool[BiomePoolTier.BOSS].length + && (this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym) || isBoss); console.log(isBoss, this.trainerPool); - const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64); - let tier = !isBoss + const tierValue = Utils.randSeedInt(!isTrainerBoss ? 512 : 64); + let tier = !isTrainerBoss ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; console.log(BiomePoolTier[tier]); @@ -320,7 +320,7 @@ export class Arena { this.eventTarget.dispatchEvent(new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType!, this.weather?.turnsLeft!)); // TODO: is this bang correct? if (this.weather) { - this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1))); + this.scene.unshiftPhase(new CommonAnimPhase(this.scene, undefined, undefined, CommonAnim.SUNNY + (weather - 1), true)); this.scene.queueMessage(getWeatherStartMessage(weather)!); // TODO: is this bang correct? } else { this.scene.queueMessage(getWeatherClearMessage(oldWeatherType)!); // TODO: is this bang correct? diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts new file mode 100644 index 000000000000..7c58a4946992 --- /dev/null +++ b/src/field/mystery-encounter-intro.ts @@ -0,0 +1,456 @@ +import { GameObjects } from "phaser"; +import BattleScene from "../battle-scene"; +import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import { Species } from "#enums/species"; +import { isNullOrUndefined } from "#app/utils"; +import { getSpriteKeysFromSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PlayAnimationConfig = Phaser.Types.Animations.PlayAnimationConfig; + +type KnownFileRoot = + | "arenas" + | "battle_anims" + | "cg" + | "character" + | "effect" + | "egg" + | "events" + | "inputs" + | "items" + | "mystery-encounters" + | "pokeball" + | "pokemon" + | "pokemon/back" + | "pokemon/exp" + | "pokemon/female" + | "pokemon/icons" + | "pokemon/input" + | "pokemon/shiny" + | "pokemon/variant" + | "statuses" + | "trainer" + | "ui"; + +export class MysteryEncounterSpriteConfig { + /** The sprite key (which is the image file name). e.g. "ace_trainer_f" */ + spriteKey: string; + /** Refer to [/public/images](../../public/images) directorty for all folder names */ + fileRoot: KnownFileRoot & string | string; + /** Optional replacement for `spriteKey`/`fileRoot`. Just know this defaults to male/genderless, form 0, no shiny */ + species?: Species; + /** Enable shadow. Defaults to `false` */ + hasShadow?: boolean = false; + /** Disable animation. Defaults to `false` */ + disableAnimation?: boolean = false; + /** Repeat the animation. Defaults to `false` */ + repeat?: boolean = false; + /** What frame of the animation to start on. Defaults to 0 */ + startFrame?: number = 0; + /** Hidden at start of encounter. Defaults to `false` */ + hidden?: boolean = false; + /** Tint color. `0` - `1`. Higher means darker tint. */ + tint?: number; + /** X offset */ + x?: number; + /** Y offset */ + y?: number; + /** Y shadow offset */ + yShadow?: number; + /** Sprite scale. `0` - `n` */ + scale?: number; + /** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */ + isPokemon?: boolean; + /** If you are using an item sprite, set to `true` */ + isItem?: boolean; + /** The sprites alpha. `0` - `1` The lower the number, the more transparent */ + alpha?: number; +} + +/** + * When a mystery encounter spawns, there are visuals (mainly sprites) tied to the field for the new encounter to inform the player of the type of encounter + * These slide in with the field as part of standard field change cycle, and will typically be hidden after the player has selected an option for the encounter + * Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites + */ +export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { + public encounter: MysteryEncounter; + public spriteConfigs: MysteryEncounterSpriteConfig[]; + public enterFromRight: boolean; + + constructor(scene: BattleScene, encounter: MysteryEncounter) { + super(scene, -72, 76); + this.encounter = encounter; + this.enterFromRight = encounter.enterIntroVisualsFromRight ?? false; + // Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter + this.spriteConfigs = encounter.spriteConfigs.map(config => { + const result = { + ...config + }; + + if (!isNullOrUndefined(result.species)) { + const keys = getSpriteKeysFromSpecies(result.species!); + result.spriteKey = keys.spriteKey; + result.fileRoot = keys.fileRoot; + result.isPokemon = true; + } + + return result; + }); + if (!this.spriteConfigs) { + return; + } + + const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => { + const ret = this.scene.addFieldSprite(0, 0, spriteKey); + ret.setOrigin(0.5, 1); + ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 }); + return ret; + }; + + const getItemSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => { + const icon = this.scene.add.sprite(-19, 2, "items", spriteKey); + icon.setOrigin(0.5, 1); + icon.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 }); + return icon; + }; + + // Depending on number of sprites added, should space them to be on the circular field sprite + const minX = -40; + const maxX = 40; + const origin = 4; + let n = 0; + // Sprites with custom X or Y defined will not count for normal spacing requirements + const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1)); + + this.spriteConfigs?.forEach((config) => { + const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config; + + let sprite: GameObjects.Sprite; + let tintSprite: GameObjects.Sprite; + + if (!isItem) { + sprite = getSprite(spriteKey, hasShadow, yShadow); + tintSprite = getSprite(spriteKey); + } else { + sprite = getItemSprite(spriteKey, hasShadow, yShadow); + tintSprite = getItemSprite(spriteKey); + } + + sprite.setVisible(!config.hidden); + tintSprite.setVisible(false); + + if (scale) { + sprite.setScale(scale); + tintSprite.setScale(scale); + } + + // Sprite offset from origin + if (x || y) { + if (x) { + sprite.setPosition(origin + x, sprite.y); + tintSprite.setPosition(origin + x, tintSprite.y); + } + if (y) { + sprite.setPosition(sprite.x, sprite.y + y); + tintSprite.setPosition(tintSprite.x, tintSprite.y + y); + } + } else { + // Single sprite + if (this.spriteConfigs.length === 1) { + sprite.x = origin; + tintSprite.x = origin; + } else { + // Do standard sprite spacing (not including offset sprites) + sprite.x = minX + (n + 0.5) * spacingValue + origin; + tintSprite.x = minX + (n + 0.5) * spacingValue + origin; + n++; + } + } + + if (!isNullOrUndefined(alpha)) { + sprite.setAlpha(alpha); + tintSprite.setAlpha(alpha); + } + + this.add(sprite); + this.add(tintSprite); + }); + } + + /** + * Loads the assets that were defined on construction (async) + */ + loadAssets(): Promise { + return new Promise(resolve => { + if (!this.spriteConfigs) { + resolve(); + } + + this.spriteConfigs.forEach((config) => { + if (config.isPokemon) { + this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot); + } else if (config.isItem) { + this.scene.loadAtlas("items", ""); + } else { + this.scene.loadAtlas(config.spriteKey, config.fileRoot); + } + }); + + this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { + this.spriteConfigs.every((config) => { + if (config.isItem) { + return true; + } + + const originalWarn = console.warn; + + // Ignore warnings for missing frames, because there will be a lot + console.warn = () => { + }; + const frameNames = this.scene.anims.generateFrameNames(config.spriteKey, { zeroPad: 4, suffix: ".png", start: 1, end: 128 }); + + console.warn = originalWarn; + if (!(this.scene.anims.exists(config.spriteKey))) { + this.scene.anims.create({ + key: config.spriteKey, + frames: frameNames, + frameRate: 12, + repeat: -1 + }); + } + + return true; + }); + + resolve(); + }); + + if (!this.scene.load.isLoading()) { + this.scene.load.start(); + } + }); + } + + /** + * Sets the initial frames and tint of sprites after load + */ + initSprite(): void { + if (!this.spriteConfigs) { + return; + } + + this.getSprites().map((sprite, i) => { + if (!this.spriteConfigs[i].isItem) { + sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + } + }); + this.getTintSprites().map((tintSprite, i) => { + if (!this.spriteConfigs[i].isItem) { + tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0); + } + }); + + this.spriteConfigs.every((config, i) => { + if (!config.tint) { + return true; + } + + const tintSprite = this.getAt(i * 2 + 1); + this.tint(tintSprite, 0, config.tint); + + return true; + }); + } + + /** + * Attempts to animate a given set of {@linkcode Phaser.GameObjects.Sprite} + * @see {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate + * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint + * @param animConfig {@linkcode Phaser.Types.Animations.PlayAnimationConfig} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @returns true if the sprite was able to be animated + */ + tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, animConfig: Phaser.Types.Animations.PlayAnimationConfig): boolean { + // Show an error in the console if there isn't a texture loaded + if (sprite.texture.key === "__MISSING") { + console.error(`No texture found for '${animConfig.key}'!`); + + return false; + } + // Don't try to play an animation when there isn't one + if (sprite.texture.frameTotal <= 1) { + console.warn(`No animation found for '${animConfig.key}'. Is this intentional?`); + + return false; + } + + sprite.play(animConfig); + tintSprite.play(animConfig); + + return true; + } + + /** + * For sprites with animation and that do not have animation disabled, will begin frame animation + */ + playAnim(): void { + if (!this.spriteConfigs) { + return; + } + + const sprites = this.getSprites(); + const tintSprites = this.getTintSprites(); + this.spriteConfigs.forEach((config, i) => { + if (!config.disableAnimation) { + const trainerAnimConfig: PlayAnimationConfig = { + key: config.spriteKey, + repeat: config?.repeat ? -1 : 0, + startFrame: config?.startFrame ?? 0 + }; + + this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig); + } + }); + } + + /** + * Returns a Sprite/TintSprite pair + * @param index + */ + getSpriteAtIndex(index: number): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + ret.push(this.getAt(index * 2)); // Sprite + ret.push(this.getAt(index * 2 + 1)); // Tint Sprite + + return ret; + } + + /** + * Gets all non-tint sprites (these are the "real" unmodified sprites) + */ + getSprites(): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + this.spriteConfigs.forEach((config, i) => { + ret.push(this.getAt(i * 2)); + }); + return ret; + } + + /** + * Gets all tint sprites (duplicate sprites that have different alpha and fill values) + */ + getTintSprites(): Phaser.GameObjects.Sprite[] { + if (!this.spriteConfigs) { + return []; + } + + const ret: Phaser.GameObjects.Sprite[] = []; + this.spriteConfigs.forEach((config, i) => { + ret.push(this.getAt(i * 2 + 1)); + }); + + return ret; + } + + /** + * Tints a single sprite + * @param sprite + * @param color + * @param alpha + * @param duration + * @param ease + */ + private tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void { + // const tintSprites = this.getTintSprites(); + sprite.setTintFill(color); + sprite.setVisible(true); + + if (duration) { + sprite.setAlpha(0); + + this.scene.tweens.add({ + targets: sprite, + alpha: alpha || 1, + duration: duration, + ease: ease || "Linear" + }); + } else { + sprite.setAlpha(alpha); + } + } + + /** + * Tints all sprites + * @param color + * @param alpha + * @param duration + * @param ease + */ + tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void { + const tintSprites = this.getTintSprites(); + tintSprites.map(tintSprite => { + this.tint(tintSprite, color, alpha, duration, ease); + }); + } + + /** + * Untints a single sprite over a duration + * @param sprite + * @param duration + * @param ease + */ + private untint(sprite, duration: integer, ease?: string): void { + if (duration) { + this.scene.tweens.add({ + targets: sprite, + alpha: 0, + duration: duration, + ease: ease || "Linear", + onComplete: () => { + sprite.setVisible(false); + sprite.setAlpha(1); + } + }); + } else { + sprite.setVisible(false); + sprite.setAlpha(1); + } + } + + /** + * Untints all sprites + * @param sprite + * @param duration + * @param ease + */ + untintAll(duration: integer, ease?: string): void { + const tintSprites = this.getTintSprites(); + tintSprites.map(tintSprite => { + this.untint(tintSprite, duration, ease); + }); + } + + /** + * Sets container and all child sprites to visible + * @param value - true for visible, false for hidden + */ + setVisible(value: boolean): this { + this.getSprites().forEach(sprite => { + sprite.setVisible(value); + }); + return super.setVisible(value); + } +} + +/** + * Interface is required so as not to override {@link Phaser.GameObjects.Container.scene} + */ +export default interface MysteryEncounterIntroVisuals { + scene: BattleScene +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c648ff485b76..9c8c1e6ce46f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,14 +3,14 @@ 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 } from "../data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, 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 } from "#app/utils"; +import { Constructor, isNullOrUndefined, randSeedInt } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; import { Stat, type PermanentStat, type BattleStat, type EffectiveStat, PERMANENT_STATS, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, BaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempStatStageBoosterModifier, TempCritBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier, EvoTrackerModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; @@ -60,6 +60,7 @@ import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-ph import { Challenges } from "#enums/challenges"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export enum FieldPosition { CENTER, @@ -98,6 +99,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public pauseEvolutions: boolean; public pokerus: boolean; public wildFlee: boolean; + public evoCounter: integer; public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: integer; @@ -113,6 +115,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleData: PokemonBattleData; public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + public mysteryEncounterPokemonData: MysteryEncounterPokemonData; + + /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ + public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; public fieldPosition: FieldPosition; @@ -190,6 +196,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.metSpecies = dataSource.metSpecies ?? (this.metBiome !== -1 ? this.species.speciesId : this.species.getRootSpeciesId(true)); this.pauseEvolutions = dataSource.pauseEvolutions; this.pokerus = !!dataSource.pokerus; + this.evoCounter = dataSource.evoCounter ?? 0; this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies : dataSource.fusionSpecies ? getPokemonSpecies(dataSource.fusionSpecies) : null; this.fusionFormIndex = dataSource.fusionFormIndex; this.fusionAbilityIndex = dataSource.fusionAbilityIndex; @@ -198,6 +205,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.fusionGender = dataSource.fusionGender; this.fusionLuck = dataSource.fusionLuck; this.usedTMs = dataSource.usedTMs ?? []; + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData); } else { this.id = Utils.randSeedInt(4294967296); this.ivs = ivs || Utils.getIvsFromId(this.id); @@ -218,6 +226,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.variant = this.shiny ? this.generateVariant() : 0; } + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); + if (nature !== undefined) { this.setNature(nature); } else { @@ -319,9 +329,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns {boolean} True if pokemon is allowed in battle */ isAllowedInBattle(): boolean { + return !this.isFainted() && this.isAllowed(); + } + + /** + * Check if this pokemon is allowed (no challenge exclusion) + * This is frequently a better alternative to {@link isFainted} + * @returns {boolean} True if pokemon is allowed in battle + */ + isAllowed(): boolean { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); - return !this.isFainted() && !this.wildFlee && challengeAllowed.value; + return !this.wildFlee && challengeAllowed.value; } isActive(onField?: boolean): boolean { @@ -532,10 +551,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.speciesForm; } - if (!this.species.forms?.length) { - return this.species; + if (this.species.forms && this.species.forms.length > 0) { + return this.species.forms[this.formIndex]; } - return this.species.forms[this.formIndex]; + + return this.species; } getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm { @@ -563,6 +583,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const formKey = this.getFormKey(); if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) { return 1.5; + } else if (this.mysteryEncounterPokemonData.spriteScale > 0) { + return this.mysteryEncounterPokemonData.spriteScale; } return 1; } @@ -572,8 +594,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Resetting properties should not be shown on the field this.setVisible(false); - // Reset field position - this.setFieldPosition(FieldPosition.CENTER); + // Remove the offset from having a Substitute active if (this.isOffsetBySubstitute()) { this.x -= this.getSubstituteOffset()[0]; this.y -= this.getSubstituteOffset()[1]; @@ -850,22 +871,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param stat the desired {@linkcode EffectiveStat} * @param opponent the target {@linkcode Pokemon} * @param move the {@linkcode Move} being used + * @param ignoreAbility determines whether this Pokemon's abilities should be ignored during the stat calculation + * @param ignoreOppAbility during an attack, determines whether the opposing Pokemon's abilities should be ignored during the stat calculation. * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @param simulated if `true`, nullifies any effects that produce any changes to game state from triggering * @returns the final in-battle value of a stat */ - getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { + getEffectiveStat(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreAbility: boolean = false, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): integer { const statValue = new Utils.NumberHolder(this.getStat(stat, false)); this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); + // The Ruin abilities here are never ignored, but they reveal themselves on summon anyway const fieldApplied = new Utils.BooleanHolder(false); for (const pokemon of this.scene.getField(true)) { - applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied); + applyFieldStatMultiplierAbAttrs(FieldMultiplyStatAbAttr, pokemon, stat, statValue, this, fieldApplied, simulated); if (fieldApplied.value) { break; } } - applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue); - let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, isCritical); + if (!ignoreAbility) { + applyStatMultiplierAbAttrs(StatMultiplierAbAttr, this, stat, statValue, simulated); + } + + let ret = statValue.value * this.getStatStageMultiplier(stat, opponent, move, ignoreOppAbility, isCritical, simulated); switch (stat) { case Stat.ATK: @@ -915,19 +943,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } // Get and manipulate base stats - const baseStats = this.getSpeciesForm(true).baseStats.slice(); - if (this.isFusion()) { - const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; - for (const s of PERMANENT_STATS) { - baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); - } - } else if (this.scene.gameMode.isSplicedOnly) { - for (const s of PERMANENT_STATS) { - baseStats[s] = Math.ceil(baseStats[s] / 2); - } - } - this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); - + const baseStats = this.calculateBaseStats(); // Using base stats, calculate and store stats one by one for (const s of PERMANENT_STATS) { let value = Math.floor(((2 * baseStats[s] + this.ivs[s]) * this.level) * 0.01); @@ -955,6 +971,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.setStat(s, value); } + this.scene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, this.stats); + } + + calculateBaseStats(): number[] { + const baseStats = this.getSpeciesForm(true).baseStats.slice(0); + // Shuckle Juice + this.scene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); + // Old Gateau + this.scene.applyModifiers(PokemonBaseStatFlatModifier, this.isPlayer(), this, baseStats); + if (this.isFusion()) { + const fusionBaseStats = this.getFusionSpeciesForm(true).baseStats; + for (const s of PERMANENT_STATS) { + baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); + } + } else if (this.scene.gameMode.isSplicedOnly) { + for (const s of PERMANENT_STATS) { + baseStats[s] = Math.ceil(baseStats[s] / 2); + } + } + // Vitamins + this.scene.applyModifiers(BaseStatModifier, this.isPlayer(), this, baseStats); + + return baseStats; } getNature(): Nature { @@ -1122,7 +1161,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (!types.length || !includeTeraType) { - if (!ignoreOverride && this.summonData?.types && this.summonData.types.length !== 0) { + if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) { + // "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters + this.mysteryEncounterPokemonData.types.forEach(t => types.push(t)); + } else if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) { this.summonData.types.forEach(t => types.push(t)); } else { const speciesForm = this.getSpeciesForm(ignoreOverride); @@ -1183,6 +1225,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } + if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) { + return allAbilities[this.mysteryEncounterPokemonData.ability]; + } if (this.isFusion()) { return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } @@ -1207,6 +1252,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; } + if (!isNullOrUndefined(this.mysteryEncounterPokemonData.passive) && this.mysteryEncounterPokemonData.passive !== -1) { + return allAbilities[this.mysteryEncounterPokemonData.passive]; + } let starterSpeciesId = this.species.speciesId; while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { @@ -1697,6 +1745,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } + /** + * Get a list of all egg moves + * + * @returns list of egg moves + */ + getEggMoves() : Moves[] { + return speciesEggMoves[this.species.speciesId]; + } + setMove(moveIndex: integer, moveId: Moves): void { const move = moveId ? new PokemonMove(moveId) : null; this.moveset[moveIndex] = move; @@ -1751,6 +1808,42 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.shiny; } + /** + * Function that tries to set a Pokemon shiny based on seed. + * For manual use only, usually to roll a Pokemon's shiny chance a second time. + * + * The base shiny odds are {@linkcode baseShinyChance} / 65536 + * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns true if the Pokemon has been set as a shiny, false otherwise + */ + trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean { + /** `64/65536 -> 1/1024` */ + const baseShinyChance = 64; + const shinyThreshold = new Utils.IntegerHolder(baseShinyChance); + if (thresholdOverride === undefined || applyModifiersToOverride) { + if (thresholdOverride !== undefined && applyModifiersToOverride) { + shinyThreshold.value = thresholdOverride; + } + if (this.scene.eventManager.isEventActive()) { + shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier(); + } + if (!this.hasTrainer()) { + this.scene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); + } + } else { + shinyThreshold.value = thresholdOverride; + } + + this.shiny = randSeedInt(65536) < shinyThreshold.value; + + if (this.shiny) { + this.initShinySparkle(); + } + + return this.shiny; + } + /** * Generates a variant * Has a 10% of returning 2 (epic variant) @@ -2034,7 +2127,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { hideInfo(): Promise { return new Promise(resolve => { - if (this.battleInfo.visible) { + if (this.battleInfo && this.battleInfo.visible) { this.scene.tweens.add({ targets: [ this.battleInfo, this.battleInfo.expMaskRect ], x: this.isPlayer() ? "+=150" : `-=${!this.isBoss() ? 150 : 246}`, @@ -2138,10 +2231,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param stat the desired {@linkcode EffectiveStat} * @param opponent the target {@linkcode Pokemon} * @param move the {@linkcode Move} being used + * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) * @param isCritical determines whether a critical hit has occurred or not (`false` by default) + * @param simulated determines whether effects are applied without altering game state (`true` by default) * @return the stat stage multiplier to be used for effective stat calculation */ - getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): number { + getStatStageMultiplier(stat: EffectiveStat, opponent?: Pokemon, move?: Move, ignoreOppAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): number { const statStage = new Utils.IntegerHolder(this.getStatStage(stat)); const ignoreStatStage = new Utils.BooleanHolder(false); @@ -2158,7 +2253,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } } - applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, false, stat, ignoreStatStage); + if (!ignoreOppAbility) { + applyAbAttrs(IgnoreOpponentStatStagesAbAttr, opponent, null, simulated, stat, ignoreStatStage); + } if (move) { applyMoveAttrs(IgnoreOpponentStatStagesAttr, this, opponent, move, ignoreStatStage); } @@ -2223,13 +2320,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Apply the results of a move to this pokemon - * @param {Pokemon} source The pokemon using the move - * @param {PokemonMove} battlerMove The move being used - * @returns {HitResult} The result of the attack - */ - apply(source: Pokemon, move: Move): HitResult { - let result: HitResult; + * 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 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: + * - `cancelled`: `true` if the move was cancelled by another effect. + * - `result`: {@linkcode HitResult} indicates the attack's type effectiveness. + * - `damage`: `number` the attack's final damage output. + */ + getAttackDamage(source: Pokemon, move: Move, ignoreAbility: boolean = false, ignoreSourceAbility: boolean = false, isCritical: boolean = false, simulated: boolean = true): DamageCalculationResult { const damage = new Utils.NumberHolder(0); const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -2247,291 +2349,354 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * The effectiveness of the move being used. Along with type matchups, this * accounts for changes in effectiveness from the move's attributes and the * abilities of both the source and this Pokemon. + * + * Note that the source's abilities are not ignored here */ - const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled); + const typeMultiplier = this.getMoveEffectiveness(source, move, ignoreAbility, simulated, cancelled); - switch (moveCategory) { - case MoveCategory.PHYSICAL: - case MoveCategory.SPECIAL: - const isPhysical = moveCategory === MoveCategory.PHYSICAL; - const sourceTeraType = source.getTeraType(); + const isPhysical = moveCategory === MoveCategory.PHYSICAL; - const power = move.calculateBattlePower(source, this); + /** Combined damage multiplier from field effects such as weather, terrain, etc. */ + const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(moveType, source.isGrounded())); + applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); - if (cancelled.value) { - // Cancelled moves fail silently - source.stopMultiHit(this); - return HitResult.NO_EFFECT; - } else { - const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === moveType) as TypeBoostTag; - if (typeBoost?.oneUse) { - source.removeTag(typeBoost.tagType); - } + const isTypeImmune = (typeMultiplier * arenaAttackTypeMultiplier.value) === 0; - /** Combined damage multiplier from field effects such as weather, terrain, etc. */ - const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(moveType, source.isGrounded())); - applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); + if (cancelled.value || isTypeImmune) { + return { + cancelled: cancelled.value, + result: move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT, + damage: 0 + }; + } - /** - * Whether or not this Pokemon is immune to the incoming move. - * Note that this isn't fully resolved in `getMoveEffectiveness` because - * of possible type-suppressing field effects (e.g. Desolate Land's effect on Water-type attacks). - */ - const isTypeImmune = (typeMultiplier * arenaAttackTypeMultiplier.value) === 0; - if (isTypeImmune) { - // Moves with no effect that were not cancelled queue a "no effect" message before failing - source.stopMultiHit(this); - result = (move.id === Moves.SHEER_COLD) - ? HitResult.IMMUNE - : HitResult.NO_EFFECT; + // If the attack deals fixed damaged, return a result with that much damage + const fixedDamage = new Utils.IntegerHolder(0); + applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); + if (fixedDamage.value) { + return { + cancelled: false, + result: HitResult.EFFECTIVE, + damage: fixedDamage.value + }; + } - if (result === HitResult.IMMUNE) { - this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: this.name })); - } else { - this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); - } + // If the attack is a one-hit KO move, return a result with damage equal to this Pokemon's HP + const isOneHitKo = new Utils.BooleanHolder(false); + applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); + if (isOneHitKo.value) { + return { + cancelled: false, + result: HitResult.ONE_HIT_KO, + damage: this.hp + }; + } - return result; - } + // ----- BEGIN BASE DAMAGE MULTIPLIERS ----- - const glaiveRushModifier = new Utils.IntegerHolder(1); - if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { - glaiveRushModifier.value = 2; - } - let isCritical: boolean; - const critOnly = new Utils.BooleanHolder(false); - const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); - applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); - applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); - if (critOnly.value || critAlways) { - isCritical = true; - } else { - const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; - isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); - if (Overrides.NEVER_CRIT_OVERRIDE) { - isCritical = false; - } - } - if (isCritical) { - const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); - const blockCrit = new Utils.BooleanHolder(false); - applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); - if (noCritTag || blockCrit.value) { - isCritical = false; - } - } - const sourceAtk = new Utils.IntegerHolder(source.getEffectiveStat(isPhysical ? Stat.ATK : Stat.SPATK, this, undefined, isCritical)); - const targetDef = new Utils.IntegerHolder(this.getEffectiveStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); - const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); - applyAbAttrs(MultCritAbAttr, source, null, false, criticalMultiplier); - const screenMultiplier = new Utils.NumberHolder(1); - if (!isCritical) { - this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); - } - const sourceTypes = source.getTypes(); - const matchesSourceType = sourceTypes[0] === moveType || (sourceTypes.length > 1 && sourceTypes[1] === moveType); - const stabMultiplier = new Utils.NumberHolder(1); - if (sourceTeraType === Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value += 0.5; - } else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) { - stabMultiplier.value += 0.5; - } + /** A base damage multiplier based on the source's level */ + const levelMultiplier = (2 * source.level / 5 + 2); - applyAbAttrs(StabBoostAbAttr, source, null, false, stabMultiplier); + /** The power of the move after power boosts from abilities, etc. have applied */ + const power = move.calculateBattlePower(source, this, simulated); - if (sourceTeraType !== Type.UNKNOWN && matchesSourceType) { - stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25); - } + /** + * 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); - // 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) - const { targets, multiple } = getMoveTargets(source, move.id); - const targetMultiplier = (multiple && targets.length > 1) ? 0.75 : 1; + /** + * 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); - applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); - 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; - const effectPhase = this.scene.getCurrentPhase(); - let numTargets = 1; - if (effectPhase instanceof MoveEffectPhase) { - numTargets = effectPhase.getTargets().length; - } - const twoStrikeMultiplier = new Utils.NumberHolder(1); - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, false, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier); - - if (!isTypeImmune) { - const levelMultiplier = (2 * source.level / 5 + 2); - const randomMultiplier = (this.randSeedIntRange(85, 100) / 100); - damage.value = Utils.toDmgValue((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2) - * stabMultiplier.value - * typeMultiplier - * arenaAttackTypeMultiplier.value - * screenMultiplier.value - * twoStrikeMultiplier.value - * targetMultiplier - * criticalMultiplier.value - * glaiveRushModifier.value - * randomMultiplier); - - if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { - if (!move.hasAttr(BypassBurnDamageReductionAttr)) { - const burnDamageReductionCancelled = new Utils.BooleanHolder(false); - applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, false); - if (!burnDamageReductionCancelled.value) { - damage.value = Utils.toDmgValue(damage.value / 2); - } - } - } + // ------ END BASE DAMAGE MULTIPLIERS ------ - applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, false, damage); - - /** - * For each {@link HitsTagAttr} the move has, doubles the damage of the move if: - * The target has a {@link BattlerTagType} that this move interacts with - * AND - * The move doubles damage when used against that tag - */ - move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { - if (this.getTag(hta.tagType)) { - damage.value *= 2; - } - }); - } + /** 25% damage debuff on moves hitting more than one non-fainted target (regardless of immunities) */ + const { targets, multiple } = getMoveTargets(source, move.id); + const numTargets = multiple ? targets.length : 1; + const targetMultiplier = (numTargets > 1) ? 0.75 : 1; - if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && moveType === Type.DRAGON) { - damage.value = Utils.toDmgValue(damage.value / 2); - } + /** 0.25x multiplier if this is an added strike from the attacker's Parental Bond */ + const parentalBondMultiplier = new Utils.NumberHolder(1); + if (!ignoreSourceAbility) { + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, numTargets, new Utils.IntegerHolder(0), parentalBondMultiplier); + } + + /** Doubles damage if this Pokemon's last move was Glaive Rush */ + const glaiveRushMultiplier = new Utils.IntegerHolder(1); + if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { + glaiveRushMultiplier.value = 2; + } + + /** The damage multiplier when the given move critically hits */ + const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); + applyAbAttrs(MultCritAbAttr, source, null, simulated, criticalMultiplier); + + /** + * A multiplier for random damage spread in the range [0.85, 1] + * This is always 1 for simulated calls. + */ + const randomMultiplier = simulated ? 1 : ((this.randSeedIntRange(85, 100)) / 100); - const fixedDamage = new Utils.IntegerHolder(0); - applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); - if (!isTypeImmune && fixedDamage.value) { - damage.value = fixedDamage.value; - isCritical = false; - result = HitResult.EFFECTIVE; + const sourceTypes = source.getTypes(); + const sourceTeraType = source.getTeraType(); + const matchesSourceType = sourceTypes.includes(moveType); + /** A damage multiplier for when the attack is of the attacker's type and/or Tera type. */ + const stabMultiplier = new Utils.NumberHolder(1); + if (matchesSourceType) { + stabMultiplier.value += 0.5; + } + if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === moveType) { + stabMultiplier.value += 0.5; + } + + if (!ignoreSourceAbility) { + applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier); + } + + stabMultiplier.value = Math.min(stabMultiplier.value, 2.25); + + /** Halves damage if the attacker is using a physical attack while burned */ + const burnMultiplier = new Utils.NumberHolder(1); + if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { + if (!move.hasAttr(BypassBurnDamageReductionAttr)) { + const burnDamageReductionCancelled = new Utils.BooleanHolder(false); + if (!ignoreSourceAbility) { + applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled, simulated); } - result = result!; // telling TS compiler that result is defined! - - if (!result) { - const isOneHitKo = new Utils.BooleanHolder(false); - applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); - if (isOneHitKo.value) { - result = HitResult.ONE_HIT_KO; - isCritical = false; - damage.value = this.hp; - } else if (typeMultiplier >= 2) { - result = HitResult.SUPER_EFFECTIVE; - } else if (typeMultiplier >= 1) { - result = HitResult.EFFECTIVE; - } else { - result = HitResult.NOT_VERY_EFFECTIVE; - } + if (!burnDamageReductionCancelled.value) { + burnMultiplier.value = 0.5; } + } + } - const isOneHitKo = result === HitResult.ONE_HIT_KO; + /** Reduces damage if this Pokemon has a relevant screen (e.g. Light Screen for special attacks) */ + const screenMultiplier = new Utils.NumberHolder(1); + this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, defendingSide, move.category, this.scene.currentBattle.double, screenMultiplier); - if (!fixedDamage.value && !isOneHitKo) { - if (!source.isPlayer()) { - this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); - } - if (!this.isPlayer()) { - this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); - } + /** + * For each {@linkcode HitsTagAttr} 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 => { + if (this.getTag(hta.tagType)) { + hitsTagMultiplier.value *= 2; + } + }); - applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, false, damage); - } + /** Halves damage if this Pokemon is grounded in Misty Terrain against a Dragon-type attack */ + const mistyTerrainMultiplier = (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && moveType === Type.DRAGON) + ? 0.5 + : 1; + + damage.value = Utils.toDmgValue( + baseDamage + * targetMultiplier + * parentalBondMultiplier.value + * arenaAttackTypeMultiplier.value + * glaiveRushMultiplier.value + * criticalMultiplier.value + * randomMultiplier + * stabMultiplier.value + * typeMultiplier + * burnMultiplier.value + * screenMultiplier.value + * hitsTagMultiplier.value + * mistyTerrainMultiplier + ); - // This attribute may modify damage arbitrarily, so be careful about changing its order of application. - applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); + /** Doubles damage if the attacker has Tinted Lens and is using a resisted move */ + if (!ignoreSourceAbility) { + applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, simulated, damage); + } - console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + /** Apply the enemy's Damage and Resistance tokens */ + if (!source.isPlayer()) { + this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); + } + if (!this.isPlayer()) { + this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); + } - // In case of fatal damage, this tag would have gotten cleared before we could lapse it. - const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ + if (!ignoreAbility) { + applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); + } - if (damage.value) { - this.lapseTags(BattlerTagLapseType.HIT); + // This attribute may modify damage arbitrarily, so be careful about changing its order of application. + applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); - const substitute = this.getTag(SubstituteTag); - if (substitute && move.hitsSubstitute(source, this)) { - substitute.hp -= damage.value; - damage.value = 0; - } - if (this.isFullHp()) { - applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); - } else if (!this.isPlayer() && damage.value >= this.hp) { - this.scene.applyModifiers(EnemyEndureChanceModifier, false, this); - } + if (this.isFullHp() && !ignoreAbility) { + applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, false, damage); + } + + // debug message for when damage is applied (i.e. not simulated) + if (!simulated) { + console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); + } + + let hitResult: HitResult; + if (typeMultiplier < 1) { + hitResult = HitResult.NOT_VERY_EFFECTIVE; + } else if (typeMultiplier > 1) { + hitResult = HitResult.SUPER_EFFECTIVE; + } else { + hitResult = HitResult.EFFECTIVE; + } + + return { + cancelled: cancelled.value, + result: hitResult, + damage: damage.value + }; + } + + /** + * Applies the results of a move to this pokemon + * @param source The {@linkcode Pokemon} using the move + * @param move The {@linkcode Move} being used + * @returns The {@linkcode HitResult} of the attack + */ + apply(source: Pokemon, move: Move): HitResult { + const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; + if (move.category === MoveCategory.STATUS) { + const cancelled = new Utils.BooleanHolder(false); + const typeMultiplier = this.getMoveEffectiveness(source, move, false, false, cancelled); + + if (!cancelled.value && typeMultiplier === 0) { + this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); + } + return (typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; + } else { + /** Determines whether the attack critically hits */ + let isCritical: boolean; + const critOnly = new Utils.BooleanHolder(false); + const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); + applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); + applyAbAttrs(ConditionalCritAbAttr, source, null, false, critOnly, this, move); + if (critOnly.value || critAlways) { + isCritical = true; + } else { + const critChance = [24, 8, 2, 1][Math.max(0, Math.min(this.getCritStage(source, move), 3))]; + isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance); + } + + const noCritTag = this.scene.arena.getTagOnSide(NoCritTag, defendingSide); + const blockCrit = new Utils.BooleanHolder(false); + applyAbAttrs(BlockCritAbAttr, this, null, false, blockCrit); + if (noCritTag || blockCrit.value || Overrides.NEVER_CRIT_OVERRIDE) { + isCritical = false; + } + + const { cancelled, result, damage: dmg } = this.getAttackDamage(source, move, false, false, isCritical, false); - /** - * We explicitly require to ignore the faint phase here, as we want to show the messages - * about the critical hit and the super effective/not very effective messages before the faint phase. - */ - damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); - this.turnData.damageTaken += damage.value; + const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === source.getMoveType(move)) as TypeBoostTag; + if (typeBoost?.oneUse) { + source.removeTag(typeBoost.tagType); + } + + if (cancelled || result === HitResult.IMMUNE || result === HitResult.NO_EFFECT) { + source.stopMultiHit(this); - if (isCritical) { - this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); + if (!cancelled) { + if (result === HitResult.IMMUNE) { + this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(this) })); + } else { + this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); } + } + return result; + } + + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + + const isOneHitKo = result === HitResult.ONE_HIT_KO; + + if (dmg) { + this.lapseTags(BattlerTagLapseType.HIT); + + const substitute = this.getTag(SubstituteTag); + const isBlockedBySubstitute = !!substitute && move.hitsSubstitute(source, this); + if (isBlockedBySubstitute) { + substitute.hp -= dmg; + } + if (!this.isPlayer() && dmg >= this.hp) { + this.scene.applyModifiers(EnemyEndureChanceModifier, false, this); + } + + /** + * We explicitly require to ignore the faint phase here, as we want to show the messages + * about the critical hit and the super effective/not very effective messages before the faint phase. + */ + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); + + if (damage > 0) { if (source.isPlayer()) { this.scene.validateAchvs(DamageAchv, damage); - if (damage.value > this.scene.gameData.gameStats.highestDamage) { - this.scene.gameData.gameStats.highestDamage = damage.value; + if (damage > this.scene.gameData.gameStats.highestDamage) { + this.scene.gameData.gameStats.highestDamage = damage; } } - - if (damage.value > 0) { - source.turnData.damageDealt += damage.value; - source.turnData.currDamageDealt = damage.value; - this.battleData.hitCount++; - const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; - this.turnData.attacksReceived.unshift(attackResult); - - if (source.isPlayer() && !this.isPlayer()) { - this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage); - } + source.turnData.damageDealt += damage; + source.turnData.currDamageDealt = damage; + this.turnData.damageTaken += damage; + this.battleData.hitCount++; + const attackResult = { move: move.id, result: result as DamageResult, damage: damage, critical: isCritical, sourceId: source.id, sourceBattlerIndex: source.getBattlerIndex() }; + this.turnData.attacksReceived.unshift(attackResult); + if (source.isPlayer() && !this.isPlayer()) { + this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, new Utils.NumberHolder(damage)); } } + } - // want to include is.Fainted() in case multi hit move ends early, still want to render message - if (source.turnData.hitsLeft === 1 || this.isFainted()) { - switch (result) { - case HitResult.SUPER_EFFECTIVE: - this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); - break; - case HitResult.NOT_VERY_EFFECTIVE: - this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective")); - break; - case HitResult.ONE_HIT_KO: - this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO")); - break; - case HitResult.IMMUNE: - case HitResult.NO_EFFECT: - console.error("Unhandled move immunity!"); - break; - } - } + if (isCritical) { + this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); + } - if (this.isFainted()) { - // set splice index here, so future scene queues happen before FaintedPhase - this.scene.setPhaseQueueSplice(); - this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); - this.destroySubstitute(); - this.resetSummonData(); + // want to include is.Fainted() in case multi hit move ends early, still want to render message + if (source.turnData.hitsLeft === 1 || this.isFainted()) { + switch (result) { + case HitResult.SUPER_EFFECTIVE: + this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); + break; + case HitResult.NOT_VERY_EFFECTIVE: + this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective")); + break; + case HitResult.ONE_HIT_KO: + this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO")); + break; } + } - if (damage) { - destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM); - } + if (this.isFainted()) { + // set splice index here, so future scene queues happen before FaintedPhase + this.scene.setPhaseQueueSplice(); + this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); + this.destroySubstitute(); + this.resetSummonData(); } - break; - case MoveCategory.STATUS: - if (!cancelled.value && typeMultiplier === 0) { - this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); + + if (dmg) { + destinyTag?.lapse(source, BattlerTagLapseType.CUSTOM); } - result = (cancelled.value || typeMultiplier === 0) ? HitResult.NO_EFFECT : HitResult.STATUS; - break; - } - return result; + return result; + } } /** @@ -3883,6 +4048,12 @@ export class PlayerPokemon extends Pokemon { this.updateInfo(true).then(() => resolve()); }); }; + if (preEvolution.speciesId === Species.GIMMIGHOUL) { + const evotracker = this.getHeldItems().filter(m => m instanceof EvoTrackerModifier)[0] ?? null; + if (evotracker) { + this.scene.removeModifier(evotracker); + } + } if (!this.scene.gameMode.isDaily || this.metBiome > -1) { this.scene.gameData.updateSpeciesDexIvs(this.species.speciesId, this.ivs); this.scene.gameData.setPokemonSeen(this, false); @@ -4202,7 +4373,7 @@ export class EnemyPokemon extends Pokemon { } // Filter out any moves this Pokemon cannot use - const movePool = this.getMoveset().filter(m => m?.isUsable(this)); + let movePool = this.getMoveset().filter(m => m?.isUsable(this)); // If no moves are left, use Struggle. Otherwise, continue with move selection if (movePool.length) { // If there's only 1 move in the move pool, use it. @@ -4223,6 +4394,39 @@ export class EnemyPokemon extends Pokemon { return { move: moveId, targets: this.getNextTargets(moveId) }; case AiType.SMART_RANDOM: case AiType.SMART: + /** + * Search this Pokemon's move pool for moves that will KO an opposing target. + * If there are any moves that can KO an opponent (i.e. a player Pokemon), + * those moves are the only ones considered for selection on this turn. + */ + const koMoves = movePool.filter(pkmnMove => { + if (!pkmnMove) { + return false; + } + + const move = pkmnMove.getMove()!; + if (move.moveTarget === MoveTarget.ATTACKER) { + return false; + } + + const fieldPokemon = this.scene.getField(); + const moveTargets = getMoveTargets(this, move.id).targets + .map(ind => fieldPokemon[ind]) + .filter(p => this.isPlayer() !== p.isPlayer()); + // Only considers critical hits for crit-only moves or when this Pokemon is under the effect of Laser Focus + const isCritical = move.hasAttr(CritOnlyAttr) || !!this.getTag(BattlerTagType.ALWAYS_CRIT); + + return move.category !== MoveCategory.STATUS + && moveTargets.some(p => { + const doesNotFail = move.applyConditions(this, p, move) || [Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id); + return doesNotFail && p.getAttackDamage(this, move, !p.battleData.abilityRevealed, false, isCritical).damage >= p.hp; + }); + }, this); + + if (koMoves.length > 0) { + movePool = koMoves; + } + /** * Move selection is based on the move's calculated "benefit score" against the * best possible target(s) (as determined by {@linkcode getNextTargets}). @@ -4626,6 +4830,7 @@ export class PokemonSummonData { public speciesForm: PokemonSpeciesForm | null; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; + public passiveAbility: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; public stats: number[] = [ 0, 0, 0, 0, 0, 0 ]; @@ -4693,6 +4898,16 @@ export enum HitResult { export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER; +/** Interface containing the results of a damage calculation for a given move */ +export interface DamageCalculationResult { + /** `true` if the move was cancelled (thus suppressing "No Effect" messages) */ + cancelled: boolean; + /** The effectiveness of the move */ + result: HitResult; + /** The damage dealt by the move */ + damage: number; +} + /** * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. * These are the moves assigned to a {@linkcode Pokemon} object. diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 326ef0edefbb..1973d7216a6a 100644 --- a/src/field/trainer.ts +++ b/src/field/trainer.ts @@ -35,11 +35,16 @@ export default class Trainer extends Phaser.GameObjects.Container { public name: string; public partnerName: string; - constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string) { + constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string, trainerConfigOverride?: TrainerConfig) { super(scene, -72, 80); this.config = trainerConfigs.hasOwnProperty(trainerType) ? trainerConfigs[trainerType] : trainerConfigs[TrainerType.ACE_TRAINER]; + + if (trainerConfigOverride) { + this.config = trainerConfigOverride; + } + this.variant = variant; this.partyTemplateIndex = Math.min(partyTemplateIndex !== undefined ? partyTemplateIndex : Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)), this.config.partyTemplates.length - 1); diff --git a/src/game-mode.ts b/src/game-mode.ts index f5dadad6f1b4..a2d61d7cffff 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -29,8 +29,13 @@ interface GameModeConfig { hasRandomBosses?: boolean; isSplicedOnly?: boolean; isChallenge?: boolean; + hasMysteryEncounters?: boolean; } +// Describes min and max waves for MEs in specific game modes +export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180]; +export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [10, 180]; + export class GameMode implements GameModeConfig { public modeId: GameModes; public isClassic: boolean; @@ -45,6 +50,9 @@ export class GameMode implements GameModeConfig { public isChallenge: boolean; public challenges: Challenge[]; public battleConfig: FixedBattleConfigs; + public hasMysteryEncounters: boolean; + public minMysteryEncounterWave: number; + public maxMysteryEncounterWave: number; constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) { this.modeId = modeId; @@ -317,6 +325,20 @@ export class GameMode implements GameModeConfig { } } + /** + * Returns the wave range where MEs can spawn for the game mode [min, max] + */ + getMysteryEncounterLegalWaves(): [number, number] { + switch (this.modeId) { + default: + return [0, 0]; + case GameModes.CLASSIC: + return CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES; + case GameModes.CHALLENGE: + return CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES; + } + } + static getModeName(modeId: GameModes): string { switch (modeId) { case GameModes.CLASSIC: @@ -336,7 +358,7 @@ export class GameMode implements GameModeConfig { export function getGameMode(gameMode: GameModes): GameMode { switch (gameMode) { case GameModes.CLASSIC: - return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true }, classicFixedBattles); + return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true, hasMysteryEncounters: true }, classicFixedBattles); case GameModes.ENDLESS: return new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true }); case GameModes.SPLICED_ENDLESS: @@ -344,6 +366,6 @@ export function getGameMode(gameMode: GameModes): GameMode { case GameModes.DAILY: return new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true }); case GameModes.CHALLENGE: - return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true }, classicFixedBattles); + return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true, hasMysteryEncounters: true }, classicFixedBattles); } } diff --git a/src/interfaces/held-modifier-config.ts b/src/interfaces/held-modifier-config.ts new file mode 100644 index 000000000000..2285babdbfdb --- /dev/null +++ b/src/interfaces/held-modifier-config.ts @@ -0,0 +1,8 @@ +import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; + +export default interface HeldModifierConfig { + modifier: PokemonHeldItemModifierType | PokemonHeldItemModifier; + stackCount?: number; + isTransferable?: boolean; +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 6de441fb162f..d0818aa1e194 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -22,6 +22,7 @@ import { initStatsKeys } from "./ui/game-stats-ui-handler"; import { initVouchers } from "./system/voucher"; import { Biome } from "#enums/biome"; import { TrainerType } from "#enums/trainer-type"; +import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters"; export class LoadingScene extends SceneBase { public static readonly KEY = "loading"; @@ -286,6 +287,9 @@ export class LoadingScene extends SceneBase { } } + // Load Mystery Encounter dex progress icon + this.loadImage("encounter_radar", "mystery-encounters"); + this.loadAtlas("dualshock", "inputs"); this.loadAtlas("xbox", "inputs"); this.loadAtlas("keyboard", "inputs"); @@ -362,6 +366,7 @@ export class LoadingScene extends SceneBase { initMoves(); initAbilities(); initChallenges(); + initMysteryEncounters(); } loadLoadingScreen() { diff --git a/src/locales/de/battle.json b/src/locales/de/battle.json index aa7b5c59725b..52d4f192a5be 100644 --- a/src/locales/de/battle.json +++ b/src/locales/de/battle.json @@ -14,10 +14,10 @@ "moneyWon": "Du gewinnst {{moneyAmount}} ₽!", "moneyPickedUp": "Du hebst {{moneyAmount}} ₽ auf!", "pokemonCaught": "{{pokemonName}} wurde gefangen!", - "pokemonObtained": "You got {{pokemonName}}!", - "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", - "pokemonFled": "The wild {{pokemonName}} fled!", - "playerFled": "You fled from the {{pokemonName}}!", + "pokemonObtained": "Du erhältst {{pokemonName}}!", + "pokemonBrokeFree": "Mist!\nDas Pokémon hat sich befreit!", + "pokemonFled": "Das wilde {{pokemonName}} ist geflohen!", + "playerFled": "Du bist vor dem wilden {{pokemonName}} geflohen!", "addedAsAStarter": "{{pokemonName}} wurde als Starterpokémon hinzugefügt!", "partyFull": "Dein Team ist voll. Möchtest du ein Pokémon durch {{pokemonName}} ersetzen?", "pokemon": "Pokémon", @@ -102,5 +102,5 @@ "congratulations": "Glückwunsch!", "beatModeFirstTime": "{{speciesName}} hat den {{gameMode}} Modus zum ersten Mal beendet! Du erhältst {{newModifier}}!", "eggSkipPrompt": "Zur Ei-Zusammenfassung springen?", - "mysteryEncounterAppeared": "What's this?" + "mysteryEncounterAppeared": "Was ist das?" } diff --git a/src/locales/de/bgm-name.json b/src/locales/de/bgm-name.json index c747fc34b9e8..6355c33c49c2 100644 --- a/src/locales/de/bgm-name.json +++ b/src/locales/de/bgm-name.json @@ -148,8 +148,8 @@ "menu": "PMD Erkundungsteam Himmel Willkommen in der Welt der Pokémon!", "title": "PMD Erkundungsteam Himmel Top-Menü-Thema", - "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_weird_dream": "PMD Erkundungsteam Himmel Zeitturmspitze", + "mystery_encounter_fun_and_games": "PMD Erkundungsteam Himmel Gildenmeister Knuddeluff\n", + "mystery_encounter_gen_5_gts": "SW GTS", "mystery_encounter_gen_6_gts": "XY GTS" } diff --git a/src/locales/de/dialogue.json b/src/locales/de/dialogue.json index a8cdf78457df..3c6c6f712eb1 100644 --- a/src/locales/de/dialogue.json +++ b/src/locales/de/dialogue.json @@ -935,112 +935,112 @@ }, "stat_trainer_buck": { "encounter": { - "1": "...I'm telling you right now. I'm seriously tough. Act surprised!", - "2": "I can feel my Pokémon shivering inside their Pokéballs!" + "1": "...Ich sag dir jetzt mal was. Ich bin echt stark. Tue überrascht!", + "2": "Ich fühle, wie meine Pokémon in ihren Pokébällen zittern!" }, "victory": { - "1": "Heeheehee!\nSo hot, you!", - "2": "Heeheehee!\nSo hot, you!" + "1": "Hehehehe! So heiß bist du!", + "2": "Hehehehe! So heiß bist du!" }, "defeat": { - "1": "Whoa! You're all out of gas, I guess.", - "2": "Whoa! You're all out of gas, I guess." + "1": "Whoa! Du scheinst ja wirklich erschöpft zu sein.", + "2": "Whoa! Du scheinst ja wirklich erschöpft zu sein." } }, "stat_trainer_cheryl": { "encounter": { - "1": "My Pokémon have been itching for a battle.", - "2": "I should warn you, my Pokémon can be quite rambunctious." + "1": "Meine Pokémon können es kaum erwarten, zu kämpfen.", + "2": "Ich sollte dich warnen, meine Pokémon können ziemlich wild sein." }, "victory": { - "1": "Striking the right balance of offense and defense... It's not easy to do.", - "2": "Striking the right balance of offense and defense... It's not easy to do." + "1": "Ein gutes Verhältnis von Angriff und Verteidigung... Das ist nicht einfach.", + "2": "Ein gutes Verhältnis von Angriff und Verteidigung... Das ist nicht einfach." }, "defeat": { - "1": "Do your Pokémon need any healing?", - "2": "Do your Pokémon need any healing?" + "1": "Brauchen deine Pokémon Heilung?", + "2": "Brauchen deine Pokémon Heilung?" } }, "stat_trainer_marley": { "encounter": { - "1": "... OK.\nI'll do my best.", - "2": "... OK.\nI... won't lose...!" + "1": "...OK. Ich werde mein Bestes geben.", + "2": "...OK. Ich werde nicht verlieren...!" }, "victory": { "1": "... Awww.", "2": "... Awww." }, "defeat": { - "1": "... Goodbye.", - "2": "... Goodbye." + "1": "... Auf Wiedersehen.", + "2": "... Auf Wiedersehen." } }, "stat_trainer_mira": { "encounter": { - "1": "You will be shocked by Mira!", - "2": "Mira will show you that Mira doesn't get lost anymore!" + "1": "Du wirst von Mira schockiert sein!", + "2": "Mira wird dir zeigen, dass Mira sich nicht mehr verirrt!" }, "victory": { - "1": "Mira wonders if she can get very far in this land.", - "2": "Mira wonders if she can get very far in this land." + "1": "Mira wundern, ob sie in diesem Land weit kommen kann.", + "2": "Mira wundern, ob sie in diesem Land weit kommen kann." }, "defeat": { - "1": "Mira knew she would win!", - "2": "Mira knew she would win!" + "1": "Mira wuss, dass sie gewinnen würde!", + "2": "Mira wuss, dass sie gewinnen würde!" } }, "stat_trainer_riley": { "encounter": { - "1": "Battling is our way of greeting!", - "2": "We're pulling out all the stops to put your Pokémon down." + "1": "Kämpfe sind unsere Art der Begrüßung.", + "2": "Wir setzen alles daran, deine Pokémon zu besiegen." }, "victory": { - "1": "At times we battle, and sometimes we team up...$It's great how Trainers can interact.", - "2": "At times we battle, and sometimes we team up...$It's great how Trainers can interact." + "1": "Manchmal kämpfen wir, und manchmal schließen wir uns zusammen...\n$Es ist großartig, wie Trainer interagieren können.", + "2": "Manchmal kämpfen wir, und manchmal schließen wir uns zusammen...\n$Es ist großartig, wie Trainer interagieren können." }, "defeat": { - "1": "You put up quite the display.\nBetter luck next time.", - "2": "You put up quite the display.\nBetter luck next time." + "1": "Du hast dich gut geschlagen. Bis zum nächsten Mal.", + "2": "Du hast dich gut geschlagen. Bis zum nächsten Mal." } }, "winstrates_victor": { "encounter": { - "1": "That's the spirit! I like you!" + "1": "Das ist der Kampfgeist den ich sehen will! Ich mag dich!" }, "victory": { - "1": "A-ha! You're stronger than I thought!" + "1": "Ahh! Du bist stärker als ich dachte!" } }, "winstrates_victoria": { "encounter": { - "1": "My goodness! Aren't you young?$You must be quite the trainer to beat my husband, though.$Now I suppose it's my turn to battle!" + "1": "Mein Gott! Bist du nicht etwas jung?\n$Du musst ein ziemlich guter Trainer sein, um meinen Mann zu besiegen.\n$Jetzt bin ich wohl an der Reihe!" }, "victory": { - "1": "Uwah! Just how strong are you?!" + "1": "Waas? Wie stark bist du denn?" } }, "winstrates_vivi": { "encounter": { - "1": "You're stronger than Mom? Wow!$But I'm strong, too!\nReally! Honestly!" + "1": "Du bist stärker als Mama? Wow! Aber ich bin auch stark! Wirklich! Ehrlich!" }, "victory": { - "1": "Huh? Did I really lose?\nSnivel... Grandmaaa!" + "1": "Huh? Habe ich wirklich verloren?\nSchnief... Omaaa!" } }, "winstrates_vicky": { "encounter": { - "1": "How dare you make my precious\ngranddaughter cry!$I see I need to teach you a lesson.\nPrepare to feel the sting of defeat!" + "1": "Wie kannst du es wagen, meine kostbare Enkelin zum Weinen zu bringen!\n$Ich sehe, ich muss dir eine Lektion erteilen.\n$Mach dich bereit, eine Niederlage zu erleiden!" }, "victory": { - "1": "Whoa! So strong!\nMy granddaughter wasn't lying." + "1": "Wow! So stark!\nMeine Enkelin hat nicht gelogen." } }, "winstrates_vito": { "encounter": { - "1": "I trained together with my whole family,\nevery one of us!$I'm not losing to anyone!" + "1": "Ich habe zusammen mit meiner ganzen Familie trainiert, mit jedem von uns!\n$Ich verliere gegen niemanden!" }, "victory": { - "1": "I was better than everyone in my family.\nI've never lost before..." + "1": "Ich war besser als jeder in meiner Familie. Ich habe noch nie verloren..." } }, "brock": { diff --git a/src/locales/de/egg.json b/src/locales/de/egg.json index 63c2b4314840..4097ac175784 100644 --- a/src/locales/de/egg.json +++ b/src/locales/de/egg.json @@ -11,7 +11,7 @@ "gachaTypeLegendary": "Erhöhte Chance auf legendäre Eier.", "gachaTypeMove": "Erhöhte Chance auf Eier mit seltenen Attacken.", "gachaTypeShiny": "Erhöhte Chance auf schillernde Eier.", - "eventType": "Mystery Event", + "eventType": "Geheimnisvolles Ereignis", "selectMachine": "Wähle eine Maschine.", "notEnoughVouchers": "Du hast nicht genug Ei-Gutscheine!", "tooManyEggs": "Du hast schon zu viele Eier!", diff --git a/src/locales/de/modifier-select-ui-handler.json b/src/locales/de/modifier-select-ui-handler.json index d92d2d51f564..fbc6820244ad 100644 --- a/src/locales/de/modifier-select-ui-handler.json +++ b/src/locales/de/modifier-select-ui-handler.json @@ -9,6 +9,6 @@ "checkTeamDesc": "Überprüfe dein Team or nutze Formänderungsitems.", "rerollCost": "{{formattedMoney}}₽", "itemCost": "{{formattedMoney}}₽", - "continueNextWaveButton": "Continue", - "continueNextWaveDescription": "Continue to the next wave" + "continueNextWaveButton": "Fortfahren", + "continueNextWaveDescription": "Zur nächsten Welle fortfahren." } diff --git a/src/locales/de/modifier-type.json b/src/locales/de/modifier-type.json index a008fe981564..4f08727f9fd1 100644 --- a/src/locales/de/modifier-type.json +++ b/src/locales/de/modifier-type.json @@ -69,18 +69,18 @@ "description": "Erhöht den {{stat}} Basiswert des Trägers um 10%. Das Stapellimit erhöht sich, je höher dein IS-Wert ist." }, "PokemonBaseStatTotalModifierType": { - "name": "Shuckle Juice", - "description": "{{increaseDecrease}} all of the holder's base stats by {{statValue}}. You were {{blessCurse}} by the Shuckle.", + "name": "Pottrottsaft", + "description": "{{increaseDecrease}} alle Basiswerte des Trägers um {{statValue}}. Du wurdest von Pottrott {{blessCurse}}.", "extra": { - "increase": "Increases", - "decrease": "Decreases", - "blessed": "blessed", - "cursed": "cursed" + "increase": "Erhöht", + "decrease": "Verringert", + "blessed": "gesegnet", + "cursed": "verflucht" } }, "PokemonBaseStatFlatModifierType": { - "name": "Old Gateau", - "description": "Increases the holder's {{stats}} base stats by {{statValue}}. Found after a strange dream." + "name": "Spezialität", + "description": "Erhöht den {{stats}}-Wert des Trägers um {{statValue}}. Nach einem komischen Traum gefunden." }, "AllPokemonFullHpRestoreModifierType": { "description": "Stellt 100% der KP aller Pokémon her." @@ -417,11 +417,11 @@ "description": "Fügt eine 1%ige Chance hinzu, dass ein wildes Pokémon eine Fusion ist." }, - "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Shuckle Juice" }, - "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Black Sludge", "description": "The stench is so powerful that shops will only sell you items at a steep cost increase." }, - "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Macho Brace", "description": "Defeating a Pokémon grants the holder a Macho Brace stack. Each stack slightly boosts stats, with an extra bonus at max stacks." }, - "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Old Gateau", "description": "Increases the holder's {{stats}} stats by {{statValue}}." }, - "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Imbues the owner with luck to find Bug Type Pokémon more often. Has a strange heft to it." } + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Pottrottsaft" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Giftschleim", "description": "Der Geruch ist so stark, dass die Geschäfte ihre Items nur zu einem stark erhöhten Preis verkaufen." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Machoschiene", "description": "Das Besiegen eines Pokémon gewährt dem Besitzer einen Machoschiene-Stapel. Jeder Stapel steigert die Werte leicht, mit einem zusätzlichen Bonus bei maximalen Stapeln." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Spezialität", "description": "Erhöht den {{stats}}-Wert des Trägers um {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Erhöht die Chance, dass der Besitzer mehr Pokémon vom Typ Käfer findet. Hat ein seltsames Gewicht." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/de/mystery-encounter-messages.json b/src/locales/de/mystery-encounter-messages.json index 3b81c8e46f0a..5c10b06a355b 100644 --- a/src/locales/de/mystery-encounter-messages.json +++ b/src/locales/de/mystery-encounter-messages.json @@ -1,7 +1,7 @@ { - "paid_money": "You paid ₽{{amount, number}}.", - "receive_money": "You received ₽{{amount, number}}!", - "affects_pokedex": "Affects Pokédex Data", - "cancel_option": "Return to encounter option select.", - "view_party_button": "View Party" + "paid_money": "Du bezahlst {{amount, number}} ₽.", + "receive_money": "Du erhältst {{amount, number}} ₽!", + "affects_pokedex": "Beeinflusst Pokédex-Daten", + "cancel_option": "Zurück zur Auswahl der Begegnungsoptionen.", + "view_party_button": "Team überprüfen" } diff --git a/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json index c96c0d5f3277..94571efdfb6f 100644 --- a/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json +++ b/src/locales/de/mystery-encounters/a-trainers-test-dialogue.json @@ -1,47 +1,47 @@ { - "intro": "An extremely strong trainer approaches you...", + "intro": "Ein sehr starker Trainer kommt auf dich zu...", "buck": { - "intro_dialogue": "Yo, trainer! My name's Buck.$I have a super awesome proposal\nfor a strong trainer such as yourself!$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer egg!", - "accept": "Whoooo, I'm getting fired up!", - "decline": "Darn, it looks like your\nteam isn't in peak condition.$Here, let me help with that." + "intro_dialogue": "Yo, Trainer! Mein Name ist Avenaro.$Ich habe ein super Angebot für einen starken Trainer wie dich!$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Wohooo! Ich bin Feuer und Flamme!", + "decline": "Manno, es sieht so aus, als wäre dein Team nicht in Bestform.$Hier, lass mich dir helfen." }, "cheryl": { - "intro_dialogue": "Hello, my name's Cheryl.$I have a particularly interesting request,\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer Egg!", - "accept": "I hope you're ready!", - "decline": "I understand, it looks like your team\nisn't in the best condition at the moment.$Here, let me help with that." + "intro_dialogue": "Hallo mein Name ist Raissa, ich habe eine besondere Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Ich hoffe, du bist bereit!", + "decline": "Ich verstehe, es sieht so aus, als wäre dein Team nicht in der besten Verfassung.$Hier, lass mich dir helfen." }, "marley": { - "intro_dialogue": "...@d{64} I'm Marley.$I have an offer for you...$I'm carrying two Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you're stronger than me,\nI'll give you the rarer Egg.", - "accept": "... I see.", - "decline": "... I see.$Your Pokémon look hurt...\nLet me help." + "intro_dialogue": "...@d{64} Ich bin Charlie.$Ich habe ein Angebot für dich...$Ich trage zwei Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du stärker bist als ich, werde ich dir das seltenere Ei geben.", + "accept": "...So ist das also.", + "decline": "...Deine Pokémon sehen verletzt aus...Lass mich helfen." }, "mira": { - "intro_dialogue": "Hi! I'm Mira!$Mira has a request\nfor a strong trainer like you!$Mira has two rare Pokémon Eggs,\nbut Mira wants someone else to take one!$If you show Mira that you're strong,\nMira will give you the rarer Egg!", - "accept": "You'll battle Mira?\nYay!", - "decline": "Aww, no battle?\nThat's okay!$Here, Mira will heal your team!" + "intro_dialogue": "Hi, ich bin Orisa!$Ich habe eine Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Du wirst Orisa herausfordern? Juhu!", + "decline": "Aww, kein Kampf? Das ist okay!$Hier, Orisa wird dein Team heilen!" }, "riley": { - "intro_dialogue": "I'm Riley.$I have an odd proposal\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like to give one to another trainer.$If you can prove your strength to me,\nI'll give you the rarer Egg!", - "accept": "That look you have...\nLet's do this.", - "decline": "I understand, your team looks beat up.$Here, let me help with that." + "intro_dialogue": "Ich Urs, ich habe eine Bitte an dich, einen starken Trainer.$Ich trage zwei seltene Pokémon-Eier bei mir, aber ich möchte, dass sich jemand anderes um eines kümmert.$Wenn du mir beweisen kannst, dass du ein starker Trainer bist, werde ich dir das seltenere Ei geben!", + "accept": "Dieser Blick...Lass uns das machen.", + "decline": "Ich verstehe, dein Team sieht geschlagen aus.$Hier, lass mich dir helfen." }, - "title": "A Trainer's Test", - "description": "It seems this trainer is willing to give you an Egg regardless of your decision. However, if you can manage to defeat this strong trainer, you'll receive a much rarer Egg.", - "query": "What will you do?", + "title": "Ein Trainer-Test", + "description": "Es scheint als würde dieser Trainer dir ein Ei geben, egal wie du dich entscheidest. Wenn du es jedoch schaffst, diesen starken Trainer zu besiegen, wirst du ein viel selteneres Ei erhalten.", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Accept the Challenge", - "tooltip": "(-) Tough Battle\n(+) Gain a @[TOOLTIP_TITLE]{Very Rare Egg}" + "label": "Die Herausforderung annehmen", + "tooltip": "(-) Schwerer Kampf\n(+) Erhalte ein @[TOOLTIP_TITLE]{Sehr seltenes Ei}" }, "2": { - "label": "Refuse the Challenge", - "tooltip": "(+) Full Heal Party\n(+) Gain an @[TOOLTIP_TITLE]{Egg}" + "label": "Die Herausforderung ablehnen", + "tooltip": "(+) Team wird geheilt\n(+) Erhalte ein @[TOOLTIP_TITLE]{Ei}" } }, "eggTypes": { - "rare": "a Rare Egg", - "epic": "an Epic Egg", - "legendary": "a Legendary Egg" + "rare": "seltenes Ei", + "epic": "episches Ei", + "legendary": "legendäres Ei" }, - "outro": "{{statTrainerName}} gave you {{eggType}}!" + "outro": "{{statTrainerName}} gibt dir ein {{eggType}}!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json index 1d675d936609..eb4d06fc5273 100644 --- a/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json +++ b/src/locales/de/mystery-encounters/absolute-avarice-dialogue.json @@ -1,25 +1,25 @@ { - "intro": "A {{greedentName}} ambushes you\nand steals your party's berries!", - "title": "Absolute Avarice", - "description": "The {{greedentName}} has caught you totally off guard now all your berries are gone!\n\nThe {{greedentName}} looks like it's about to eat them when it pauses to look at you, interested.", - "query": "What will you do?", + "intro": "Ein {{greedentName}} überfällt dich und stiehlt die Beeren deines Teams!", + "title": "Absoluter Geiz", + "description": "Der {{greedentName}} hat dich total überrascht und all deine Beeren gestohlen!\nEs sieht so aus, als ob das {{greedentName}} sie gleich essen würde, aber dann hält es inne und sieht dich interessiert an.", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle It", - "tooltip": "(-) Tough Battle\n(+) Rewards from its Berry Hoard", - "selected": "The {{greedentName}} stuffs its cheeks\nand prepares for battle!", - "boss_enraged": "{{greedentName}}'s fierce love for food has it incensed!", - "food_stash": "It looks like the {{greedentName}} was guarding an enormous stash of food!$@s{item_fanfare}Each Pokémon in your party gains a {{foodReward}}!" + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Belohnungen aus seinem Beerenversteck", + "selected": "Der {{greedentName}} füllt seine Backen und bereitet sich auf den Kampf vor!", + "boss_enraged": "{{greedentName}} Liebe für Essen hat es aufgebracht!", + "food_stash": "Es scheint, als ob das {{greedentName}} ein riesiges Nahrungslager bewacht hat!$Jedes Pokémon in deinem Team erhält {{foodReward}}!" }, "2": { - "label": "Reason with It", - "tooltip": "(+) Regain Some Lost Berries", - "selected": "Your pleading strikes a chord with the {{greedentName}}.$It doesn't give all your berries back, but still tosses a few in your direction." + "label": "Verhandeln", + "tooltip": "(+) Einige Beeren zurückbekommen", + "selected": "Deine Bitte berührt das {{greedentName}}.$Es gibt dir nicht alle Beeren zurück, aber wirft dir trotzdem ein paar zu." }, "3": { - "label": "Let It Have the Food", - "tooltip": "(-) Lose All Berries\n(?) The {{greedentName}} Will Like You", - "selected": "The {{greedentName}} devours the entire\nstash of berries in a flash!$Patting its stomach,\nit looks at you appreciatively.$Perhaps you could feed it\nmore berries on your adventure...$@s{level_up_fanfare}The {{greedentName}} wants to join your party!" + "label": "Beeren überlassen", + "tooltip": "(-) Alle Beeren verlieren\n(?) Das {{greedentName}} wird dich mögen", + "selected": "Das {{greedentName}} verschlingt den gesamten Beerenversteck in einem Blitz!$Es klopft sich auf den Bauch und sieht dich dankbar an.$Vielleicht könntest du ihm auf deinem Abenteuer mehr Beeren geben...$@s{level_up_fanfare}Das {{greedentName}} möchte sich deiner Gruppe anschließen!" } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..67ac2b1e7f3f 100644 --- a/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/de/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -1,26 +1,26 @@ { - "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!", - "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?", + "intro": "Du wirst von einem reich aussehenden Jungen aufgehalten.", + "speaker": "Reicher Junge", + "intro_dialogue": "Guten Tag!$Ich kann nicht anders, als zu bemerken, dass dein\n{{strongestPokemon}} einfach göttlich aussieht!$Ich habe schon immer ein Pokémon wie dieses haben wollen!$Ich würde es dir großzügig bezahlen, und dir auch diesen alten Kram geben!", + "title": "Ein Angebot das du nicht ablehnen kannst", + "description": "Dir wird ein @[TOOLTIP_TITLE]{Schillerpin} und {{price, money}} für dein {{strongestPokemon}} angeboten!\nEs ist ein extrem gutes Angebot, aber kannst du es wirklich ertragen, dich von einem so starken Teammitglied zu trennen?", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Accept the Deal", - "tooltip": "(-) Lose {{strongestPokemon}}\n(+) Gain a @[TOOLTIP_TITLE]{Shiny Charm}\n(+) Gain {{price, money}}", - "selected": "Wonderful!@d{32} Come along, {{strongestPokemon}}!$It's time to show you off to everyone at the yacht club!$They'll be so jealous!" + "label": "Den Deal annehmen", + "tooltip": "(-) Verliere {{strongestPokemon}}\n(+) Erhalte einen @[TOOLTIP_TITLE]{Schillerpin}\n(+) Erhalte {{price, money}}", + "selected": "Wunderbar!@d{32} Komm mit, {{strongestPokemon}}!$Es ist Zeit, dich allen im Yachtclub zu zeigen!$Die werden so neidisch sein!" }, "2": { - "label": "Extort the Kid", - "tooltip": "(+) {{option2PrimaryName}} uses {{moveOrAbility}}\n(+) Gain {{price, money}}", - "tooltip_disabled": "Your Pokémon need to have certain moves or abilities to choose this", - "selected": "My word, we're being robbed, {{liepardName}}!$You'll be hearing from my lawyers for this!" + "label": "Das Kind erpressen", + "tooltip": "(+) {{option2PrimaryName}} setzt {{moveOrAbility}} ein\n(+) Erhalte {{price, money}}", + "tooltip_disabled": "Dein Pokémon muss bestimmte Attacken oder Fähigkeiten haben, um diese Option zu wählen", + "selected": "Mein Gott, wir werden ausgeraubt, {{liepardName}}!$Du wirst von meinen Anwälten hören!" }, "3": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Was ein beschissener Tag...$Ach, was solls. Lass uns zurück zum Yachtclub gehen, {{liepardName}}." } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/berries-abound-dialogue.json b/src/locales/de/mystery-encounters/berries-abound-dialogue.json index 26eae2c6b880..e7ff57a32ef8 100644 --- a/src/locales/de/mystery-encounters/berries-abound-dialogue.json +++ b/src/locales/de/mystery-encounters/berries-abound-dialogue.json @@ -1,26 +1,26 @@ { - "intro": "There's a huge berry bush\nnear that Pokémon!", - "title": "Berries Abound", - "description": "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but it looks strong. Perhaps a fast Pokémon could grab some berries without getting caught?", - "query": "What will you do?", - "berries": "Berries!", + "intro": "Da ist ein riesiger Beerenstrauch in der Nähe dieses Pokémons!", + "title": "Überall Beeren", + "description": "Es scheint, als ob ein starkes Pokémon einen Beerenstrauch bewacht. Ein Kampf wäre der direkte Weg, aber es sieht stark aus. Vielleicht könnte ein schnelles Pokémon ein paar Beeren schnappen, ohne erwischt zu werden?", + "query": "Was wirst du tun?", + "berries": "Berren!", "option": { "1": { - "label": "Battle the Pokémon", - "tooltip": "(-) Hard Battle\n(+) Gain Berries", - "selected": "You approach the\nPokémon without fear." + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Beeren erhalten", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen." }, "2": { - "label": "Race to the Bush", - "tooltip": "(-) {{fastestPokemon}} Uses its Speed\n(+) Gain Berries", - "selected": "Your {{fastestPokemon}} races for the berry bush!$It manages to nab {{numBerries}} before the {{enemyPokemon}} can react!$You quickly retreat with your newfound prize.", - "selected_bad": "Your {{fastestPokemon}} races for the berry bush!$Oh no! The {{enemyPokemon}} was faster and blocked off the approach!", - "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" + "label": "Zum Strauch rennen", + "tooltip": "(-) {{fastestPokemon}} nutzt seine Geschwindigkeit\n(+) Beeren erhalten", + "selected": "Dein {{fastestPokemon}} rennt zum Strauch!$Es schafft es, {{numBerries}} zu schnappen, bevor das {{enemyPokemon}} reagieren kann!$Du ziehst dich schnell mit deiner neuen Beute zurück.", + "selected_bad": "Dein {{fastestPokemon}} rennt zum Strauch!$Oh nein! Das {{enemyPokemon}} war schneller und hat den Weg blockiert!", + "boss_enraged": "Das gegnerische {{enemyPokemon}} ist wütend geworden!" }, "3": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You leave the strong Pokémon\nwith its prize and continue on." + "label": "Verlassen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du lässt das starke Pokémon mit seinem Item zurück und gehst weiter." } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json index 5e4e8be90e79..b88be8ee9705 100644 --- a/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json +++ b/src/locales/de/mystery-encounters/bug-type-superfan-dialogue.json @@ -1,38 +1,38 @@ { - "intro": "An unusual trainer with all kinds of Bug paraphernalia blocks your way!", - "intro_dialogue": "Hey, trainer! I'm on a mission to find the rarest Bug Pokémon in existence!$You must love Bug Pokémon too, right?\nEveryone loves Bug Pokémon!", - "title": "The Bug-Type Superfan", - "speaker": "Bug-Type Superfan", - "description": "The trainer prattles, not even waiting for a response...\n\nIt seems the only way to get out of this situation is by catching the trainer's attention!", - "query": "What will you do?", + "intro": "Ein ungewöhnlicher Trainer mit allerlei Käfer-Schnickschnack versperrt dir den Weg!", + "intro_dialogue": "Hey, Trainer! Ich bin auf einer Mission, um die seltensten Käfer-Pokémon zu finden!$Du musst Käfer-Pokémon auch lieben, oder? Jeder liebt Käfer-Pokémon!", + "title": "Der Käfersammler-Superfan", + "speaker": "Käfersammler-Superfan", + "description": "Der Trainer plappert drauf los, ohne auf eine Antwort zu warten...\nEs scheint, als gäbe es nur einen Weg, um aus dieser Situation herauszukommen... Die Aufmerksamkeit des Trainers zu erregen!", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Offer to Battle", - "tooltip": "(-) Challenging Battle\n(+) Teach a Pokémon a Bug Type Move", - "selected": "A challenge, eh?\nMy bugs are more than ready for you!" + "label": "Pokémon-Kampf", + "tooltip": "(-) Herausfordernder Kampf\n(+) Einem Pokémon eine Käfer-Attacke beibringen", + "selected": "Ein Pokémon-Kampf? Meine Käfer-Pokémon sind mehr als bereit für dich!" }, "2": { - "label": "Show Your Bug Types", - "tooltip": "(+) Receive a Gift Item", - "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!" + "label": "Käfer-Pokémon zeigen", + "tooltip": "(+) Erhalte ein Geschenk", + "disabled_tooltip": "Du brauchst mindestens 1 Käfer-Pokémon in deinem Team, um das auszuwählen.", + "selected": "Du zeigst dem Trainer all deine Käfer-Pokémon...", + "selected_0_to_1": "Huh? Du hast nur {{numBugTypes}} Käfer-Pokémon...$Ich verschwende hier meine Zeit...", + "selected_2_to_3": "Hey, du hast {{numBugTypes}} Käfer-Pokémon! Nicht schlecht.$Hier, das könnte dir auf deiner Reise helfen, mehr zu fangen!", + "selected_4_to_5": "Was? Du hast {{numBugTypes}} Käfer-Pokémon? Nicht schlecht!$Du bist noch nicht ganz auf meinem Level, aber ich kann mich in dir erkennen! $Nimm das, mein junger Padawan!", + "selected_6": "Wow! {{numBugTypes}} Käfer-Pokémon!$Du musst Käfer-Pokémon fast so sehr lieben wie ich!$Hier, nimm das als Zeichen unserer Kameradschaft!" }, "3": { - "label": "Gift a Bug Item", - "tooltip": "(-) Give the trainer a {{requiredBugItems}}\n(+) Receive a Gift Item", - "disabled_tooltip": "You need to have a {{requiredBugItems}} to select this.", - "select_prompt": "Select an item to give.", - "invalid_selection": "Pokémon doesn't have that kind of item.", - "selected": "You hand the trainer a {{selectedItem}}.", - "selected_dialogue": "Whoa! A {{selectedItem}}, for me?\nYou're not so bad, kid!$As a token of my appreciation,\nI want you to have this special gift!$It's been passed all through my family, and now I want you to have it!" + "label": "Verschenke ein Käfer-Item", + "tooltip": "(-) Du gibst dem Trainer ein {{requiredBugItems}}\n(+) Erhalte ein Geschenk", + "disabled_tooltip": "Du brauchst ein {{requiredBugItems}}, um das auszuwählen.", + "select_prompt": "Wählen Sie ein Item aus, um es zu verschenken.", + "invalid_selection": "Das Pokémon hat kein solches Item.", + "selected": "Du gibst {{selectedItem}} an dem Trainer .", + "selected_dialogue": "Wow! {{selectedItem}}, für mich? Du bist nicht so schlecht, Junge!$Als Zeichen meiner Anerkennung möchte ich, dass du dieses besondere Geschenk bekommst!$Es wurde in meiner Familie weitergegeben, und jetzt möchte ich, dass du es hast!" } }, - "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!" + "battle_won": "Dein Wissen und Können waren perfekt, um unsere Schwächen auszunutzen!$Als Gegenleistung für die wertvolle Lektion, erlaube mir, einem deiner Pokémon eine Käfer-Attacke beizubringen!", + "teach_move_prompt": "Wähle eine Attacke aus die du deinem Pokémon beibringen möchtest.", + "confirm_no_teach": "Bist du sicher, dass du keine dieser großartigen Attacken lernen möchtest?", + "outro": "Ich sehe großartige Käfer-Pokémon in deiner Zukunft! Mögen sich unsere Wege wieder kreuzen!$Mach's gut!" } diff --git a/src/locales/de/mystery-encounters/clowning-around-dialogue.json b/src/locales/de/mystery-encounters/clowning-around-dialogue.json index 177812408387..5dce7b515a95 100644 --- a/src/locales/de/mystery-encounters/clowning-around-dialogue.json +++ b/src/locales/de/mystery-encounters/clowning-around-dialogue.json @@ -1,34 +1,35 @@ { - "intro": "It's...@d{64} a clown?", + "intro": "Es ist...@d{64} ein Clown?", "speaker": "Clown", - "intro_dialogue": "Bumbling buffoon, brace for a brilliant battle!\nYou'll be beaten by this brawling busker!", - "title": "Clowning Around", - "description": "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe {{blacephalonName}} is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}", - "query": "What will you do?", + "intro_dialogue": "Du tollpatschiger Trottel, bereite dich auf einen brillanten Kampf vor!\nDu wirst von diesem prügelnden Straßenmusikanten besiegt!", + "title": "Rumgeblödel", + "description": "Irgendwas stimmt nicht mit dieser Begegnung. Der Clown scheint darauf aus zu sein, dich zu einem Kampf zu provozieren, aber zu welchem Zweck?\n\nDas {{blacephalonName}} ist besonders seltsam, als hätte es @[TOOLTIP_TITLE]{seltsame Typen} und eine @[TOOLTIP_TITLE]{Fähigkeit.}", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle the Clown", - "tooltip": "(-) Strange Battle\n(?) Affects Pokémon Abilities", - "selected": "Your pitiful Pokémon are poised for a pathetic performance!", - "apply_ability_dialogue": "A sensational showcase!\nYour savvy suits a sensational skill as spoils!", - "apply_ability_message": "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!", - "ability_prompt": "Would you like to permanently teach a Pokémon the {{ability}} ability?", - "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!" + "label": "Kampf beginnen", + "tooltip": "(-) Komischer Kampf\n(?) Beeinflusst Pokémon-Fähigkeiten", + "selected": "Deine erbärmlichen Pokémon sind bereit für eine erbärmliche Vorstellung!", + "apply_ability_dialogue": "Eine sensationelle Vorstellung! Dein Können passt zu einer sensationellen Fähigkeit als Beute!", + "apply_ability_message": "Der Clown bietet an, die Fähigkeit eines deiner Pokémon dauerhaft auf {{ability}} zu wechseln!", + "ability_prompt": "Soll eines deiner Pokémon die Fähigkeit {{ability}} dauerhaft erlangen?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} hat die Fähigkeit {{ability}} erhalten!" }, "2": { - "label": "Remain Unprovoked", - "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Items", - "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", - "selected_2": "The clown's {{blacephalonName}} uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!", - "selected_3": "Flustered fool, fall for my flawless deception!" + "label": "Nicht provozieren lassen", + "tooltip": "(-) Der Clown ist beleidigt\n(?) Beeinflusst Pokémon-Items", + "selected": "Du erbärmlicher Feigling, du verweigerst einen wunderbaren Kampf? Fühle meinen Zorn!", + "selected_2": "Das {{blacephalonName}} des Clowns verwendet Trickbetrug! Alle Items deines {{switchPokemon}} wurden zufällig vertauscht!", + "selected_3": "Meine perfekte List hat dich in die Irre geführt!" }, "3": { - "label": "Return the Insults", - "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Types", - "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", - "selected_2": "The clown's {{blacephalonName}} uses a strange move!\nAll of your team's types were randomly swapped!", - "selected_3": "Flustered fool, fall for my flawless deception!" + "label": "Die Beleidigungen erwidern", + "tooltip": "(-) Den Clown verärgern\n(?) Beeinflusst Pokémon-Typen", + "selected": "Du erbärmlicher Feigling verweigerst einen wunderbaren Kampf? Fühle meinen Zorn!", + "selected_2": "Das {{blacephalonName}} des Clowns verwendet eine seltsame Attacke! Alle Typen deines Teams wurden zufällig vertauscht!", + "selected_3": "Meine perfekte List hat dich in die Irre geführt!" } + }, - "outro": "The clown and his cohorts\ndisappear in a puff of smoke." + "outro": "Der Clown und seine Kumpanen verschwinden in einer Rauchwolke." } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json index 8e2883ecb161..3ea02955309e 100644 --- a/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json +++ b/src/locales/de/mystery-encounters/dancing-lessons-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "An {{oricorioName}} dances sadly alone, without a partner.", - "title": "Dancing Lessons", - "description": "The {{oricorioName}} doesn't seem aggressive, if anything it seems sad.\n\nMaybe it just wants someone to dance with...", - "query": "What will you do?", + "intro": "Ein {{oricorioName}} tanzt traurig allein, ohne einen Partner.", + "title": "Tanzstunden", + "description": "Das {{oricorioName}} scheint nicht aggressiv zu sein, im Gegenteil, es scheint traurig zu sein.\nVielleicht möchte es einfach nur mit jemandem tanzen...", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle It", - "tooltip": "(-) Tough Battle\n(+) Gain a Baton", - "selected": "The {{oricorioName}} is distraught and moves to defend itself!", - "boss_enraged": "The {{oricorioName}}'s fear boosted its stats!" + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Erhalte ein Stab", + "selected": "Das {{oricorioName}} ist verstört und verteidigt sich!", + "boss_enraged": "Das {{oricorioName}} ist wütend und steigert seine Werte!" }, "2": { - "label": "Learn Its Dance", - "tooltip": "(+) Teach a Pokémon Revelation Dance", - "selected": "You watch the {{oricorioName}} closely as it performs its dance...$@s{level_up_fanfare}Your {{selectedPokemon}} learned from the {{oricorioName}}!" + "label": "Lerne den Tanz", + "tooltip": "(+) Bringe einem Pokémon Wecktanz bei", + "selected": "Du schaust dem {{oricorioName}} genau zu, wie es seinen Tanz aufführt...$@s{level_up_fanfare}Dein {{selectedPokemon}} hat von {{oricorioName}} gelernt!" }, "3": { - "label": "Show It a Dance", - "tooltip": "(-) Teach the {{oricorioName}} a Dance Move\n(+) The {{oricorioName}} Will Like You", - "disabled_tooltip": "Your Pokémon need to know a Dance move for this.", - "select_prompt": "Select a Dance type move to use.", - "selected": "The {{oricorioName}} watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!$It loves the display!$@s{level_up_fanfare}The {{oricorioName}} wants to join your party!" + "label": "Zeig einen Tanz", + "tooltip": "(-) Bringe dem {{oricorioName}} einen Tanz bei\n(+) Das {{oricorioName}} wird dich mögen", + "disabled_tooltip": "Dein Pokémon muss einen Tanz beherrschen, um diese Option zu wählen.", + "select_prompt": "Wählen Sie eine Tanzattacke aus, die verwendet werden soll.", + "selected": "Das {{oricorioName}} schaut fasziniert zu, wie {{selectedPokemon}} {{selectedMove}} vorführt!$Es liebt die Vorführung!$@s{level_up_fanfare}Das {{oricorioName}} möchte sich dir anschließen!" } }, - "invalid_selection": "This Pokémon doesn't know a Dance move" + "invalid_selection": "Das Pokémon kennt keine Tanzattacke" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/dark-deal-dialogue.json b/src/locales/de/mystery-encounters/dark-deal-dialogue.json index 3086ebb0f9b7..85c8d4565bb8 100644 --- a/src/locales/de/mystery-encounters/dark-deal-dialogue.json +++ b/src/locales/de/mystery-encounters/dark-deal-dialogue.json @@ -1,24 +1,24 @@ { - "intro": "A strange man in a tattered coat\nstands in your way...", - "speaker": "Shady Guy", - "intro_dialogue": "Hey, you!$I've been working on a new device\nto bring out a Pokémon's latent power!$It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.$Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.", - "title": "Dark Deal", - "description": "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"", - "query": "What will you do?", + "intro": "Ein seltsamer Mann in einem zerrissenen Mantel steht dir im Weg...", + "speaker": "Seltsamer Mann", + "intro_dialogue": "Hey, du!$Ich habe an einem neuen Gerät gearbeitet, um die verborgene Kraft eines Pokémon zum Vorschein zu bringen!$Es bindet die Atome des Pokémon auf molekularer Ebene vollständig neu und bringt sie in eine$weitaus mächtigere Form.$Hehe...@d{64} Ich brauche nur ein paar Opf-@d{32} Ähm, Testpersonen, um zu beweisen, dass es funktioniert.", + "title": "Dunkler Handel", + "description": "Der verstörende Typ hält einige Pokébälle hoch.\n\"Es wird such für dich lohnen! Du kannst diese tollen Pokébälle als Bezahlung haben, alles was ich brauche ist ein Pokémon aus deinem Team! Hehe...\"", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Accept", - "tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", - "selected_dialogue": "Let's see, that {{pokeName}} will do nicely!$Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...", - "selected_message": "The man hands you 5 Rogue Balls.${{pokeName}} hops into the strange machine...$Flashing lights and weird noises\nstart coming from the machine!$...@d{96} Something emerges\nfrom the device, raging wildly!" + "label": "Aktzeptieren", + "tooltip": "(+) 5 Roguebälle\n(?) Ein zufälliges Pokémon wird verbessert", + "selected_dialogue": "Lass mich mal sehen...${{pokeName}} ist eine gute Wahl!$Denk dran, ich bin nicht verantwortlich, wenn etwas schief geht!@d{32} Hehe...", + "selected_message": "Der Mann übergibt dir 5 Roguebälle.${{pokeName}} springt in die seltsame Maschine...$Blinkende Lichter und seltsame Geräusche kommen aus der Maschine!$...@d{96} Etwas kommt aus der Maschine,\nwütend und wild!" }, "2": { - "label": "Refuse", - "tooltip": "(-) No Rewards", - "selected": "Not gonna help a poor fellow out?\nPah!" + "label": "Ablehnen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du willst einem armen Kerl nicht helfen? Pah!" } }, - "outro": "After the harrowing encounter,\nyou collect yourself and depart." + "outro": "Nach der schrecklichen Begegnung, sammelst du dich und gehst weiter." } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/delibirdy-dialogue.json b/src/locales/de/mystery-encounters/delibirdy-dialogue.json index ca1fefa3a39c..34ca9666d09c 100644 --- a/src/locales/de/mystery-encounters/delibirdy-dialogue.json +++ b/src/locales/de/mystery-encounters/delibirdy-dialogue.json @@ -1,29 +1,29 @@ { - "intro": "A pack of {{delibirdName}} have appeared!", - "title": "Delibir-dy", - "description": "The {{delibirdName}}s are looking at you expectantly, as if they want something. Perhaps giving them an item or some money would satisfy them?", - "query": "What will you give them?", - "invalid_selection": "Pokémon doesn't have that kind of item.", + "intro": "Ein Schwarm {{delibirdName}} ist aufgetaucht!", + "title": "Botogel-Bande", + "description": "Die {{delibirdName}} schauen dich erwartungsvoll an, als ob sie etwas wollen. Vielleicht würde es sie zufriedenstellen, wenn du ihnen ein Item oder etwas Geld gibst?", + "query": "Was möchtest du ihnen geben?", + "invalid_selection": "Das Pokémon hat kein solches Item.", "option": { "1": { - "label": "Give Money", - "tooltip": "(-) Give the {{delibirdName}}s {{money, money}}\n(+) Receive a Gift Item", - "selected": "You toss the money to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + "label": "Geld geben", + "tooltip": "(-) Den {{delibirdName}} {{money, money}} geben\n(+) Erhalte ein Geschenk", + "selected": "Du wirfst das Geld zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" }, "2": { - "label": "Give Food", - "tooltip": "(-) Give the {{delibirdName}}s a Berry or Reviver Seed\n(+) Receive a Gift Item", - "select_prompt": "Select an item to give.", - "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + "label": "Futter geben", + "tooltip": "(-) Gib den {{delibirdName}} eine Beere oder einen Belebersamen\n(+) Erhalte ein Geschenk", + "select_prompt": "Wähle ein Item aus.", + "selected": "Du wirfst {{chosenItem}} zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" }, "3": { - "label": "Give an Item", - "tooltip": "(-) Give the {{delibirdName}}s a Held Item\n(+) Receive a Gift Item", - "select_prompt": "Select an item to give.", - "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + "label": "Ein Item geben", + "tooltip": "(-) Gebe den {{delibirdName}} ein Item\n(+) Erhalte ein Geschenk", + "select_prompt": "Wähle ein Item aus.", + "selected": "Du wirfst {{chosenItem}} zu den {{delibirdName}}, die aufgeregt miteinander schnattern.$Sie drehen sich zu dir um und geben dir glücklich ein Geschenk!" } }, - "outro": "The {{delibirdName}} pack happily waddles off into the distance.$What a curious little exchange!" + "outro": "Die {{delibirdName}} watscheln glücklich davon.$Was für ein seltsamer kleiner Austausch!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/department-store-sale-dialogue.json b/src/locales/de/mystery-encounters/department-store-sale-dialogue.json index d651f32665a1..66e41975a31d 100644 --- a/src/locales/de/mystery-encounters/department-store-sale-dialogue.json +++ b/src/locales/de/mystery-encounters/department-store-sale-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "It's a lady with a ton of shopping bags.", - "speaker": "Shopper", - "intro_dialogue": "Hello! Are you here for\nthe amazing sales too?$There's a special coupon that you can\nredeem for a free item during the sale!$I have an extra one. Here you go!", - "title": "Department Store Sale", - "description": "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!", - "query": "Which counter will you go to?", + "intro": "Es ist eine Dame mit vielen Einkaufstüten.", + "speaker": "Einkäuferin", + "intro_dialogue": "Hallo! Bist du auch wegen der tollen Angebote hier?$Es gibt einen speziellen Gutschein, den du während des Verkaufs einlösen kannst!$Ich habe einen zusätzlichen. Hier, bitte!", + "title": "Einkaufszentrum-Verkauf", + "description": "Es gibt Angebote in jede Richtung! Es sieht so aus, als ob es 4 Kassen gibt, an denen du den Gutschein gegen verschiedene Artikel eintauschen kannst. Die Möglichkeiten sind endlos!", + "query": "Welche Kasse wählst du?", "option": { "1": { - "label": "TM Counter", + "label": "TM-Kasse", "tooltip": "(+) TM Shop" }, "2": { - "label": "Vitamin Counter", - "tooltip": "(+) Vitamin Shop" + "label": "Nährstoff-Kasse", + "tooltip": "(+) Nährstoff Shop" }, "3": { - "label": "Battle Item Counter", - "tooltip": "(+) X Item Shop" + "label": "Kampf-Item-Kasse", + "tooltip": "(+) X-Item Shop" }, "4": { - "label": "Pokéball Counter", + "label": "Pokéball-Kasse", "tooltip": "(+) Pokéball Shop" } }, - "outro": "What a deal! You should shop there more often." + "outro": "Was für ein Schnäppchen! Du solltest öfter hier einkaufen." } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/field-trip-dialogue.json b/src/locales/de/mystery-encounters/field-trip-dialogue.json index 61900d56cd7c..61e6d4d93673 100644 --- a/src/locales/de/mystery-encounters/field-trip-dialogue.json +++ b/src/locales/de/mystery-encounters/field-trip-dialogue.json @@ -1,31 +1,31 @@ { - "intro": "It's a teacher and some school children!", - "speaker": "Teacher", - "intro_dialogue": "Hello, there! Would you be able to\nspare a minute for my students?$I'm teaching them about Pokémon moves\nand would love to show them a demonstration.$Would you mind showing us one of\nthe moves your Pokémon can use?", - "title": "Field Trip", - "description": "A teacher is requesting a move demonstration from a Pokémon. Depending on the move you choose, she might have something useful for you in exchange.", - "query": "Which move category will you show off?", + "intro": "Eine Lehrerin und ein paar Schulkinder stehen auf einmal vor dir!", + "speaker": "Lehrerin", + "intro_dialogue": "Hallo! Könntest du eine Minute für meine Schüler erübrigen?$Ich bringe ihnen gerade bei, wie Pokémon-Attacken funktionieren und würde ihnen gerne$eine Demonstration zeigen.$Würdest du uns eine Attacke deines Pokémon vorführen?", + "title": "Exkursion", + "description": "Eine Lehrerin fragt nach einer Attackenvorführung eines Pokémon. Je nachdem, welche Attacke du wählst, hat sie vielleicht etwas Nützliches für dich als Belohnung.", + "query": "Welchen Attacken-Typ wählst du?", "option": { "1": { - "label": "A Physical Move", - "tooltip": "(+) Physical Item Rewards" + "label": "Physische Attacke", + "tooltip": "(+) Physische Item-Belohnungen" }, "2": { - "label": "A Special Move", - "tooltip": "(+) Special Item Rewards" + "label": "Spezielle Attacke", + "tooltip": "(+) Spezielle Item-Belohnungen" }, "3": { - "label": "A Status Move", - "tooltip": "(+) Status Item Rewards" + "label": "Status-Attacke", + "tooltip": "(+) Status Item-Belohnungen" }, - "selected": "{{pokeName}} shows off an awesome display of {{move}}!" + "selected": "{{pokeName}} zeigt eine beeindruckende Vorführung von {{move}}!" }, - "second_option_prompt": "Choose a move for your Pokémon to use.", - "incorrect": "...$That isn't a {{moveCategory}} move!\nI'm sorry, but I can't give you anything.$Come along children, we'll\nfind a better demonstration elsewhere.", - "incorrect_exp": "Looks like you learned a valuable lesson?$Your Pokémon also gained some experience.", - "correct": "Thank you so much for your kindness!\nI hope these items might be of use to you!", - "correct_exp": "{{pokeName}} also gained some valuable experience!", - "status": "Status", - "physical": "Physical", - "special": "Special" + "second_option_prompt": "Wähle eine Attacke die dein Pokémon einsetzen soll.", + "incorrect": "...$Das ist keine {{moveCategory}}Attacke!\nEs tut mir leid, aber ich kann dir nichts geben.$Kommt Kinder, wir suchen uns woanders einen besseren Trainer.", + "incorrect_exp": "Es scheint, als hättest du eine wertvolle Lektion gelernt?$Dein Pokémon hat auch etwas Erfahrung gesammelt.", + "correct": "Ich dank dir vielmals für deine Freundlichkeit!$Ich hoffe, diese Items sind nützlich für dich.", + "correct_exp": "{{pokeName}} hat auch etwas wertvolle Erfahrung gesammelt!", + "status": "Status-", + "physical": "physische ", + "special": "spezielle " } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json index a1644d89a3fb..167efd48b53b 100644 --- a/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json +++ b/src/locales/de/mystery-encounters/fiery-fallout-dialogue.json @@ -1,26 +1,26 @@ { - "intro": "You encounter a blistering storm of smoke and ash!", - "title": "Fiery Fallout", - "description": "The whirling ash and embers have cut visibility to nearly zero. It seems like there might be some... source that is causing these conditions. But what could be behind a phenomenon of this magnitude?", - "query": "What will you do?", + "intro": "Du hast einen Sturm aus Rauch und Asche entdeckt!", + "title": "Feurige Folgen", + "description": "Die umherwirbelnde Asche und Glut haben die Sicht auf fast Null reduziert. Es scheint, als könnte es eine... Quelle geben, die diese Bedingungen verursacht. Aber was könnte hinter einem Phänomen dieser Größe stecken?", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Find the Source", - "tooltip": "(?) Discover the source\n(-) Hard Battle", - "selected": "You push through the storm, and find two {{volcaronaName}}s in the middle of a mating dance!$They don't take kindly to the interruption and attack!" + "label": "Finde die Quelle", + "tooltip": "(?) Entdecke die Quelle\n(-) Schwieriger Kampf", + "selected": "Du hast die Quelle des Sturms gefunden!$Es sind zwei {{volcaronaName}}, die in der Mitte eines Paarungstanzes sind!$Sie nehmen die Unterbrechung nicht gut auf und greifen an!" }, "2": { - "label": "Hunker Down", - "tooltip": "(-) Suffer the effects of the weather", - "selected": "The weather effects cause significant\nharm as you struggle to find shelter!$Your party takes 20% Max HP damage!", - "target_burned": "Your {{burnedPokemon}} also became burned!" + "label": "Sich einigeln", + "tooltip": "(-) Die Folgen des Wetters erleiden", + "selected": "Die Folgen des Wetters sind verheerend!$Deine Pokémon nehmen 20% ihrer maximalen KP als Schaden!", + "target_burned": "Dein {{burnedPokemon}} wurde auch verbrannt!" }, "3": { - "label": "Your Fire Types Help", - "tooltip": "(+) End the conditions\n(+) Gain a Charcoal", - "disabled_tooltip": "You need at least 2 Fire Type Pokémon to choose this", - "selected": "Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two {{volcaronaName}}s are in the middle of a mating dance!$Thankfully, your Pokémon are able to calm them,\nand they depart without issue." + "label": "Dein Feuer-Pokémon hilft", + "tooltip": "(+) Das Wetter klärt auf\n(+) Erhalte ein Holzkohle", + "disabled_tooltip": "Du benötigst mindestens 2 Feuer-Pokémon, um diese Option auszuwählen", + "selected": "Dein {{option3PrimaryName}} und {{option3SecondaryName}} führen dich zu zwei {{volcaronaName}}, die in der Mitte eines Paarungstanzes sind!$Zum Glück können deine Pokémon sie beruhigen,und sie ziehen ohne Probleme ab." } }, - "found_charcoal": "After the weather clears,\nyour {{leadPokemon}} spots something on the ground.$@s{item_fanfare}{{leadPokemon}} gained a Charcoal!" + "found_charcoal": "Nachdem das Wetter aufklart, entdeckt dein {{leadPokemon}} etwas auf dem Boden.$@s{item_fanfare}{{leadPokemon}} erhält eine Holzkohle!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json index 3eb6cb87c165..33ed0d3f95a4 100644 --- a/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json +++ b/src/locales/de/mystery-encounters/fight-or-flight-dialogue.json @@ -1,25 +1,25 @@ { - "intro": "Something shiny is sparkling\non the ground near that Pokémon!", - "title": "Fight or Flight", - "description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but it looks strong. Perhaps you could steal the item, if you have the right Pokémon for the job.", - "query": "What will you do?", + "intro": "Etwas Glänzendes liegt auf dem Boden in der Nähe dieses Pokémons!", + "title": "Kampf oder Flucht", + "description": "Es scheint, als würde ein starkes Pokémon ein Item bewachen. Ein Kampf wäre der direkte Weg, aber es sieht stark aus. Vielleicht könntest du das Item stehlen, wenn du das richtige Pokémon für den Job hast.", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle the Pokémon", - "tooltip": "(-) Hard Battle\n(+) New Item", - "selected": "You approach the\nPokémon without fear.", - "stat_boost": "The {{enemyPokemon}}'s latent strength boosted one of its stats!" + "label": "Kampf beginnen", + "tooltip": "(-) Schwerer Kampf\n(+) Neues Item", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen.", + "stat_boost": "Die Stärke von {{enemyPokemon}} erhöht einen seiner Werte!" }, "2": { - "label": "Steal the Item", - "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", - "tooltip": "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", - "selected": ".@d{32}.@d{32}.@d{32}$Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!$You nabbed the item!" + "label": "Das Item stehlen", + "disabled_tooltip": "Dein Pokémon muss eine bestimmte Attacken beherrschen, um diese Option zu wählen.", + "tooltip": "(+) {{option2PrimaryName}} setzt {{option2PrimaryMove}} ein", + "selected": ".@d{32}.@d{32}.@d{32}$Dein {{option2PrimaryName}} hilft dir und setzt {{option2PrimaryMove}} ein!$Du hast das Item gestohlen!" }, "3": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You leave the strong Pokémon\nwith its prize and continue on." + "label": "Verlassen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du lässt das starke Pokémon mit seinem Item zurück und gehst weiter." } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/fun-and-games-dialogue.json b/src/locales/de/mystery-encounters/fun-and-games-dialogue.json index f5d7d6e8ff8d..9e38d0a45992 100644 --- a/src/locales/de/mystery-encounters/fun-and-games-dialogue.json +++ b/src/locales/de/mystery-encounters/fun-and-games-dialogue.json @@ -1,30 +1,30 @@ { - "intro_dialogue": "Step right up, folks! Try your luck\non the brand new {{wobbuffetName}} Whack-o-matic!", - "speaker": "Showman", - "title": "Fun And Games!", - "description": "You've encountered a traveling show with a prize game! You will have @[TOOLTIP_TITLE]{3 turns} to bring the {{wobbuffetName}} as close to @[TOOLTIP_TITLE]{1 HP} as possible @[TOOLTIP_TITLE]{without KOing it} so it can wind up a huge Counter on the bell-ringing machine.\nBut be careful! If you KO the {{wobbuffetName}}, you'll have to pay for the cost of reviving it!", - "query": "Would you like to play?", + "intro_dialogue": "Kommen Sie näher, meine Damen und Herren!$Versuchen Sie Ihr Glück mit dem brandneuen {{wobbuffetName}}-Hau-den-Lukas!", + "speaker": "Animateur", + "title": "Spaß und Spiele!", + "description": "Du hast ein {{wobbuffetName}} gefunden, das ein Spiel spielt! Du hast @[TOOLTIP_TITLE]{3 Züge}, um das {{wobbuffetName}} so nah wie möglich an @[TOOLTIP_TITLE]{1 KP} heranzubringen, @[TOOLTIP_TITLE]{ohne es zu besiegen}, damit es eine riesige Gegenattacke auf der Glockenmaschine ausführen kann.\nAber sei vorsichtig! Wenn du das {{wobbuffetName}} besiegst, musst du die Kosten für die Wiederbelebung bezahlen!", + "query": "Möchtest du spielen?", "option": { "1": { - "label": "Play the Game", - "tooltip": "(-) Pay {{option1Money, money}}\n(+) Play {{wobbuffetName}} Whack-o-matic", - "selected": "Time to test your luck!" + "label": "Das Spiel spielen", + "tooltip": "(-) Zahle {{option1Money, money}}\n(+) Spiele {{wobbuffetName}} Hau-den-Lukas", + "selected": "Zeit dein Glück herauszufordern!" }, "2": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You hurry along your way,\nwith a slight feeling of regret." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du beeilst dich auf deinem Weg, mit einem leichten Gefühl der Reue." } }, - "ko": "Oh no! The {{wobbuffetName}} fainted!$You lose the game and\nhave to pay for the revive cost...", - "charging_continue": "The Wubboffet keeps charging its counter-swing!", - "turn_remaining_3": "Three turns remaining!", - "turn_remaining_2": "Two turns remaining!", - "turn_remaining_1": "One turn remaining!", - "end_game": "Time's up!$The {{wobbuffetName}} winds up to counter-swing and@d{16}.@d{16}.@d{16}.", - "best_result": "The {{wobbuffetName}} smacks the button so hard\nthe bell breaks off the top!$You win the grand prize!", - "great_result": "The {{wobbuffetName}} smacks the button, nearly hitting the bell!$So close!\nYou earn the second tier prize!", - "good_result": "The {{wobbuffetName}} hits the button hard enough to go midway up the scale!$You earn the third tier prize!", - "bad_result": "The {{wobbuffetName}} barely taps the button and nothing happens...$Oh no!\nYou don't win anything!", - "outro": "That was a fun little game!" + "ko": "Oh nein! Das {{wobbuffetName}} ist ohnmächtig geworden!$Du verlierst das Spiel und musst die Kosten für die Wiederbelebung bezahlen...", + "charging_continue": "Das {{wobbuffetName}} lädt seine Gegenattacke auf!", + "turn_remaining_3": "Drei Runden verbleiben!", + "turn_remaining_2": "Zwei Runden verbleiben!", + "turn_remaining_1": "Nur noch eine Runde!", + "end_game": "Die Zeit ist um!$Das {{wobbuffetName}} holt zum Gegenangriff aus und@d{16}.@d{16}.@d{16}.", + "best_result": "Das {{wobbuffetName}} schlägt so hart auf den Knopf, dass die Glocke vom oberen Teil abbricht!$Du gewinnst den Hauptpreis!", + "great_result": "Das {{wobbuffetName}} schlägt den Knopf so hart, dass die Glocke fast getroffen wird!$So nah! Du gewinnst den zweiten Preis!", + "good_result": "Das {{wobbuffetName}} trifft den Knopf stark genug, um die Hälfte der Skala zu erreichen!$Du verdienst den dritten Preis!", + "bad_result": "Das {{wobbuffetName}} trifft den Knopf kaum und nichts passiert...$Oh nein! Du gewinnst nichts!", + "outro": "Das war ein lustiges kleines Spiel!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/global-trade-system-dialogue.json b/src/locales/de/mystery-encounters/global-trade-system-dialogue.json index 1cc420355b75..f9b2ac605bf4 100644 --- a/src/locales/de/mystery-encounters/global-trade-system-dialogue.json +++ b/src/locales/de/mystery-encounters/global-trade-system-dialogue.json @@ -1,32 +1,32 @@ { - "intro": "It's an interface for the Global Trade System!", - "title": "The GTS", - "description": "Ah, the GTS! A technological wonder, you can connect with anyone else around the globe to trade Pokémon with them! Will fortune smile upon your trade today?", - "query": "What will you do?", + "intro": "Es ist eine Schnittstelle für die Globale Tauschstation, das GTS.", + "title": "Das GTS", + "description": "Ah, das GTS! Ein technologisches Wunder, mit dem du dich mit jedem auf der Welt verbinden kannst, um Pokémon mit ihnen zu tauschen! Wird das Glück dir heute hold sein?", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Check Trade Offers", - "tooltip": "(+) Select a trade offer for one of your Pokémon", - "trade_options_prompt": "Select a Pokémon to receive through trade." + "label": "Tauschangebote prüfen", + "tooltip": "(+) Wähle ein Tauschangebot für eines deiner Pokémon aus", + "trade_options_prompt": "Wähle ein Pokémon aus, das du erhalten möchtest." }, "2": { - "label": "Wonder Trade", - "tooltip": "(+) Send one of your Pokémon to the GTS and get a random Pokémon in return" + "label": "Zaubertausch", + "tooltip": "(+) Seine eine deiner Pokémon an die GTS und erhalte ein zufälliges Pokémon im Austausch" }, "3": { - "label": "Trade an Item", - "trade_options_prompt": "Select an item to send.", - "invalid_selection": "This Pokémon doesn't have legal items to trade.", - "tooltip": "(+) Send one of your Items to the GTS and get a random new Item" + "label": "Tausche ein Item", + "trade_options_prompt": "Wähle ein Item aus, das du senden möchtest.", + "invalid_selection": "Dieses Pokémon hat keine Items die getauscht werden können.", + "tooltip": "(+) Sende eines deiner Items an die GTS und erhalte ein zufälliges Item im Austausch" }, "4": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "No time to trade today!\nYou continue on." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Heute ist keine Zeit zum Tauschen! Du gehst weiter." } }, - "pokemon_trade_selected": "{{tradedPokemon}} will be sent to {{tradeTrainerName}}.", - "pokemon_trade_goodbye": "Goodbye, {{tradedPokemon}}!", - "item_trade_selected": "{{chosenItem}} will be sent to {{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Trade complete!$You received a {{itemName}} from {{tradeTrainerName}}!", - "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} sent over {{received}}!" + "pokemon_trade_selected": "{{tradedPokemon}} wird an {{tradeTrainerName}} gesendet.", + "pokemon_trade_goodbye": "Machs gut, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} wird an {{tradeTrainerName}} gesendet.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Tausch abgeschlossen!$Du hast {{itemName}} von {{tradeTrainerName}} erhalten!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} hat dir {{received}} geschickt!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json index 41709c66799e..3ce269fbee81 100644 --- a/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json +++ b/src/locales/de/mystery-encounters/lost-at-sea-dialogue.json @@ -1,28 +1,28 @@ { - "intro": "Wandering aimlessly through the sea, you've effectively gotten nowhere.", - "title": "Lost at Sea", - "description": "The sea is turbulent in this area, and you're running out of energy.\nThis is bad. Is there a way out of the situation?", - "query": "What will you do?", + "intro": "Du warst auf dem Meer umhergeirrt und effektiv nirgendwohin gekommen.", + "title": "Verloren auf See", + "description": "Die See ist in diesem Gebiet stürmisch und du hast kaum noch Energie. Das ist schlecht. Gibt es einen Ausweg aus der Situation?", + "query": "Was wirst du tun?", "option": { "1": { - "label": "{{option1PrimaryName}} Might Help", - "label_disabled": "Can't {{option1RequiredMove}}", - "tooltip": "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", - "tooltip_disabled": "You have no Pokémon to {{option1RequiredMove}} on", - "selected": "{{option1PrimaryName}} swims ahead, guiding you back on track.${{option1PrimaryName}} seems to also have gotten stronger in this time of need!" + "label": "{{option1PrimaryName}} kann helfen", + "label_disabled": "Kein {{option1RequiredMove}}", + "tooltip": "(+) {{option1PrimaryName}} rettet dich\n(+) {{option1PrimaryName}} erhält etwas EP", + "tooltip_disabled": "Du hast kein Pokémon, das {{option1RequiredMove}} erlernen kann", + "selected": "{{option1PrimaryName}} schwimmt voraus und führt dich zurück auf den richtigen Weg.${{option1PrimaryName}} scheint auch stärker geworden zu sein in dieser Zeit der Not!" }, "2": { - "label": "{{option2PrimaryName}} Might Help", - "label_disabled": "Can't {{option2RequiredMove}}", - "tooltip": "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", - "tooltip_disabled": "You have no Pokémon to {{option2RequiredMove}} with", - "selected": "{{option2PrimaryName}} flies ahead of your boat, guiding you back on track.${{option2PrimaryName}} seems to also have gotten stronger in this time of need!" + "label": "{{option2PrimaryName}} kann helfen", + "label_disabled": "Kein {{option2RequiredMove}}", + "tooltip": "(+) {{option2PrimaryName}} rettet dich\n(+) {{option2PrimaryName}} erhält etwas EP", + "tooltip_disabled": "Du hast kein Pokémon, das {{option2RequiredMove}} erlernen kann", + "selected": "{{option2PrimaryName}} fliegt vor deinem Boot und führt dich zurück auf den richtigen Weg.${{option2PrimaryName}} scheint auch stärker geworden zu sein in dieser Zeit der Not!" }, "3": { - "label": "Wander Aimlessly", - "tooltip": "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", - "selected": "You float about in the boat, steering without direction until you finally spot a landmark you remember.$You and your Pokémon are fatigued from the whole ordeal." + "label": "Umherirren", + "tooltip": "(-) Jedes deiner Pokémon verliert {{damagePercentage}}% seiner maximalen KP", + "selected": "Du treibst im Boot umher, steuerst ohne Richtung, bis du endlich ein Wahrzeichen siehst, das du wiedererkennst.$Du und deine Pokémon sind erschöpft von dem ganzen Vorfall." } }, - "outro": "You are back on track." + "outro": "Du bist wieder auf dem richtigen Weg." } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json index 01f4e6092ebb..040a8c269e05 100644 --- a/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json +++ b/src/locales/de/mystery-encounters/mysterious-challengers-dialogue.json @@ -1,22 +1,22 @@ { - "intro": "Mysterious challengers have appeared!", - "title": "Mysterious Challengers", - "description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?", - "query": "Who will you battle?", + "intro": "Mysteriöse Herausforderer sind aufgetaucht!", + "title": "Mysteriöse Herausforderer", + "description": "Wenn du einen Herausforderer besiegst, könntest du sie beeindrucken und eine Belohnung erhalten. Aber manche sehen ziemlich stark aus. Bist du bereit für die Herausforderung?", + "query": "Wen wirst du bekämpfen?", "option": { "1": { - "label": "A Clever, Mindful Foe", - "tooltip": "(-) Standard Battle\n(+) Move Item Rewards" + "label": "Schlauer Trainer", + "tooltip": "(-) Standardkampf\n(+) TM Belohnungen" }, "2": { - "label": "A Strong Foe", - "tooltip": "(-) Hard Battle\n(+) Good Rewards" + "label": "Starker Trainer", + "tooltip": "(-) Harter Kampf\n(+) Gute Belohnungen" }, "3": { - "label": "The Mightiest Foe", - "tooltip": "(-) Brutal Battle\n(+) Great Rewards" + "label": "Mächtigster Trainer", + "tooltip": "(-) Brutaler Kampf\n(+) Großartige Belohnungen" }, - "selected": "The trainer steps forward..." + "selected": "Der Herausforderer tritt vor..." }, - "outro": "The mysterious challenger was defeated!" + "outro": "Der mysteriöse Herausforderer wurde besiegt!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json index 1de7a5992eda..9dfb05e47e8a 100644 --- a/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json +++ b/src/locales/de/mystery-encounters/mysterious-chest-dialogue.json @@ -1,23 +1,23 @@ { - "intro": "You found...@d{32} a chest?", - "title": "The Mysterious Chest", - "description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", - "query": "Will you open it?", + "intro": "Du hast...@d{32} eine Truhe gefunden?", + "title": "Die mysteriöse Truhe", + "description": "Eine wunderschön verzierte Truhe steht auf dem Boden. Da muss doch etwas Gutes drin sein... oder?", + "query": "Wirst du sie öffnen?", "option": { "1": { - "label": "Open It", - "tooltip": "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}", - "selected": "You open the chest to find...", - "normal": "Just some normal tools and items.", - "good": "Some pretty nice tools and items.", - "great": "A couple great tools and items!", - "amazing": "Whoa! An amazing item!", - "bad": "Oh no!@d{32}\nThe chest was actually a {{gimmighoulName}} in disguise!$Your {{pokeName}} jumps in front of you\nbut is KOed in the process!" + "label": "Öffnen", + "tooltip": "@[SUMMARY_BLUE]{(35%) Etwas Schreckliches}\n@[SUMMARY_GREEN]{(40%) Standard Belohnung}\n@[SUMMARY_GREEN]{(20%) Gute Belohnung}\n@[SUMMARY_GREEN]{(4%) Großartige Belohnung}\n@[SUMMARY_GREEN]{(1%) Erstaunliche Belohnung}", + "selected": "Du öffnest die Truhe und findest...", + "normal": "Einfach ein paar normale Werkzeuge und Gegenstände.", + "good": "Ein paar ziemlich gute Werkzeuge und Gegenstände.", + "great": "Ein paar großartige Werkzeuge und Gegenstände.", + "amazing": "Ein erstaunlichen Gegenstand!", + "bad": "Oh nein!@d{32}\nDie Truhe war tatsächlich ein {{gimmighoulName}}!$Dein {{pokeName}} springt schützend vor dich aber wird dabei besiegt!" }, "2": { - "label": "Too Risky, Leave", - "tooltip": "(-) No Rewards", - "selected": "You hurry along your way,\nwith a slight feeling of regret." + "label": "Zu riskant, weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du gehst schnell weiter, mit einem leichten Gefühl der Reue." } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/part-timer-dialogue.json b/src/locales/de/mystery-encounters/part-timer-dialogue.json index 614f1818e3f8..dd86449092cb 100644 --- a/src/locales/de/mystery-encounters/part-timer-dialogue.json +++ b/src/locales/de/mystery-encounters/part-timer-dialogue.json @@ -1,31 +1,31 @@ { - "intro": "A busy worker flags you down.", - "speaker": "Worker", - "intro_dialogue": "You look like someone with lots of capable Pokémon!$We can pay you if you're able to help us with some part-time work!", - "title": "Part-Timer", - "description": "Looks like there are plenty of tasks that need to be done. Depending how well-suited your Pokémon is to a task, they might earn more or less money.", - "query": "Which job will you choose?", - "invalid_selection": "Pokémon must be healthy enough.", + "intro": "Eine geschäftige Person spricht dich an.", + "speaker": "Arbeitende Person", + "intro_dialogue": "Du siehst aus, als hättest du viele fähige Pokémon!$Wir können dich bezahlen, wenn du uns bei einigen Teilzeitjobs hilfst!", + "title": "Teilzeitjob", + "description": "Es scheint, als gäbe es viele Aufgaben, die erledigt werden müssen. Je besser dein Pokémon für eine Aufgabe geeignet ist, desto mehr Geld kann es verdienen.", + "query": "Welchen Job wählst du?", + "invalid_selection": "Das Pokémon muss genug KP haben.", "option": { "1": { - "label": "Make Deliveries", - "tooltip": "(-) Your Pokémon Uses its Speed\n(+) Earn @[MONEY]{Money}", - "selected": "Your {{selectedPokemon}} works a shift delivering orders to customers." + "label": "Lieferdienst", + "tooltip": "(-) Dein Pokémon nutzt seine Geschwindigkeit\n(+) Verdiene @[MONEY]{Geld}", + "selected": "Dein {{selectedPokemon}} arbeitet eine Schicht lang damit, Bestellungen an Kunden auszuliefern." }, "2": { - "label": "Warehouse Work", - "tooltip": "(-) Your Pokémon Uses its Strength and Endurance\n(+) Earn @[MONEY]{Money}", - "selected": "Your {{selectedPokemon}} works a shift moving items around the warehouse." + "label": "Lagerarbeit", + "tooltip": "(-) Dein Pokémon nutzt seine Stärke und Ausdauer\n(+) Verdiene @[MONEY]{Geld}", + "selected": "Dein {{selectedPokemon}} arbeitet eine Schicht lang damit, Gegenstände im Lager zu bewegen." }, "3": { - "label": "Sales Assistant", - "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", - "disabled_tooltip": "Your Pokémon need to know certain moves for this job", - "selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to attract customers to the business!" + "label": "Verkäufer", + "tooltip": "(-) Dein {{option3PrimaryName}} nutzt {{option3PrimaryMove}}\n(+) Verdiene @[MONEY]{Geld}", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken kennen, um diesen Job zu erledigen", + "selected": "Dein {{option3PrimaryName}} verbringt den Tag damit, {{option3PrimaryMove}} einzusetzen, um Kunden in den Laden zu locken!" } }, - "job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.", - "job_complete_bad": "Your {{selectedPokemon}} helped us out a bit!$Here's your check for the day.", - "pokemon_tired": "Your {{selectedPokemon}} is worn out!\nThe PP of all its moves was reduced to 2!", - "outro": "Come back and help out again sometime!" + "job_complete_good": "Danke für die Hilfe! Dein {{selectedPokemon}} war unglaublich hilfreich!$Hier ist dein Gehalt für den Tag.", + "job_complete_bad": "Dein {{selectedPokemon}} hat uns ein wenig geholfen!$Hier ist dein Gehalt für den Tag.", + "pokemon_tired": "Dein {{selectedPokemon}} ist erschöpft! Die AP aller seiner Attacken wurden auf 2 reduziert!", + "outro": "Komm doch bald wieder und hilf uns erneut!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/safari-zone-dialogue.json b/src/locales/de/mystery-encounters/safari-zone-dialogue.json index 8869f2055e5f..2302833036b9 100644 --- a/src/locales/de/mystery-encounters/safari-zone-dialogue.json +++ b/src/locales/de/mystery-encounters/safari-zone-dialogue.json @@ -1,46 +1,46 @@ { - "intro": "It's a safari zone!", - "title": "The Safari Zone", - "description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\n\nBeware, though. These Pokémon may flee before you're able to catch them!", - "query": "Would you like to enter?", + "intro": "Es ist die Safari-Zone!", + "title": "Die Safari-Zone", + "description": "Es gibt alle Arten von seltenen und besonderen Pokémon, die hier gefunden werden können!\nWenn du dich entscheidest, einzutreten, hast du kannst du in den nächsten 3 Wellen versuchen, besondere Pokémon zu fangen.\nAber sei gewarnt, diese Pokémon können fliehen, bevor du sie fangen kannst!", + "query": "Willst du eintreten?", "option": { "1": { - "label": "Enter", - "tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", - "selected": "Time to test your luck!" + "label": "Eintreten", + "tooltip": "(-) Zahle {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "selected": "Zeit, dein Glück herauszufordern!" }, "2": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You hurry along your way,\nwith a slight feeling of regret." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du gehst deines Weges, mit einem leichten Gefühl der Reue." } }, "safari": { "1": { - "label": "Throw a Pokéball", - "tooltip": "(+) Throw a Pokéball", - "selected": "You throw a Pokéball!" + "label": "Pokéball werfen", + "tooltip": "(+) Werfe einen Pokéball", + "selected": "Du wirfst einen Pokéball!" }, "2": { - "label": "Throw Bait", - "tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", - "selected": "You throw some bait!" + "label": "Köder werfen", + "tooltip": "(+) Erhöht die Fangrate\n(-) Erhöht die Fluchtchance", + "selected": "Du wirfst einen Köder!" }, "3": { - "label": "Throw Mud", - "tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", - "selected": "You throw some mud!" + "label":"Matsch werfen", + "tooltip": "(+) Vermindert die Fluchtchance\n(-) Chance, die Fangrate zu verringern", + "selected": "Du wirst ein wenig Matsch!" }, "4": { - "label": "Flee", - "tooltip": "(?) Flee from this Pokémon" + "label": "Fliehen", + "tooltip": "(?) Fliehe vor diesem Pokémon" }, - "watching": "{{pokemonName}} is watching carefully!", - "eating": "{{pokemonName}} is eating!", - "busy_eating": "{{pokemonName}} is busy eating!", - "angry": "{{pokemonName}} is angry!", - "beside_itself_angry": "{{pokemonName}} is beside itself with anger!", - "remaining_count": "{{remainingCount}} Pokémon remaining!" + "watching": "{{pokemonName}} beobachtet alles aufmerksam!", + "eating": "{{pokemonName}} frisst!", + "busy_eating": "{{pokemonName}} konzentriert sich aufs Futter!", + "angry": "{{pokemonName}} ist wütend!", + "beside_itself_angry": "{{pokemonName}} ist außer sich vor Wut!", + "remaining_count": "{{remainingCount}} Pokémon übrig!" }, - "outro": "That was a fun little excursion!" + "outro": "Das war ein spannendes Abenteuer in der Safari-Zone!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json index d0003de07f1d..6579d94bd2eb 100644 --- a/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json +++ b/src/locales/de/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "A man in a dark coat approaches you.", - "speaker": "Shady Salesman", - "intro_dialogue": ".@d{16}.@d{16}.@d{16}$I've got the goods if you've got the money.$Make sure your Pokémon can handle it though.", - "title": "The Vitamin Dealer", - "description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", - "query": "Which deal will you choose?", + "intro": "Ein Mann in einem dunklen Mantel kommt auf dich zu.", + "speaker": "Zwielichtiger Verkäufer", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$Ich habe die Ware, wenn du das Geld hast.$Aber sei sicher, dass deine Pokémon es vertragen können.", + "title": "Der Nährstoff-Verkäufer", + "description": "Der Mann öffnet seinen Mantel und zeigt dir einige Pokémon-Nährstoffe. Die Preise, die er nennt, scheinen ein wirklich gutes Angebot zu sein. Fast zu gut...\nEr bietet dir zwei Möglichkeiten zur Auswahl an.", + "query": "Welches Angebot wirst du wählen?", "invalid_selection": "Pokémon must be healthy enough.", "option": { "1": { - "label": "The Cheap Deal", - "tooltip": "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins" + "label": "Der billige Deal", + "tooltip": "(-) Zahle {{option1Money, money}}\n(-) Nebenwirkungen?\n(+) Das gewählte Pokémon erhält 2 zufällige Nährstoffe" }, "2": { - "label": "The Pricey Deal", - "tooltip": "(-) Pay {{option2Money, money}}\n(+) Chosen Pokémon Gains 2 Random Vitamins" + "label": "Der teure Deal", + "tooltip": "(-) Zahle {{option2Money, money}}\n(+) Das gewählte Pokémon erhält 2 zufällige Nährstoffe" }, "3": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "Heh, wouldn't have figured you for a coward." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Ey, hätte ich nicht gedacht, dass du ein Feigling bist." }, - "selected": "The man hands you two bottles and quickly disappears.${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!" + "selected": "Der Mann überreicht dir zwei Flaschen und verschwindet schnell.${{selectedPokemon}} erhält {{boost1}} und {{boost2}} Nährstoffe!" }, - "cheap_side_effects": "But the medicine had some side effects!$Your {{selectedPokemon}} takes some damage,\nand its Nature is changed to {{newNature}}!", - "no_bad_effects": "Looks like there were no side-effects from the medicine!" + "cheap_side_effects": "Aber die Medizin hatte Nebenwirkungen!$Dein {{selectedPokemon}} nimmt etwas Schaden,\nund sein Wesen wurde zu {{newNature}} geändert!", + "no_bad_effects": "Es scheint, als hätten die Nährstoffe keine Nebenwirkungen." } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json index cd3bb7465c48..097bf3acd955 100644 --- a/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json +++ b/src/locales/de/mystery-encounters/slumbering-snorlax-dialogue.json @@ -1,25 +1,25 @@ { - "intro": "As you walk down a narrow pathway, you see a towering silhouette blocking your path.$You get closer to see a {{snorlaxName}} sleeping peacefully.\nIt seems like there's no way around it.", - "title": "Slumbering {{snorlaxName}}", - "description": "You could attack it to try and get it to move, or simply wait for it to wake up. Who knows how long that could take, though...", - "query": "What will you do?", + "intro": "Als du einen schmalen Pfad entlang gehst, siehst du eine riesige Silhouette, die deinen Weg blockiert.$Du kommst näher, um zu sehen, dass ein {{snorlaxName}} friedlich schläft.$Es scheint, als gäbe es keinen Weg daran vorbei.", + "title": "Schlafendes {{snorlaxName}}", + "description": "Du könntest es angreifen, um es zum Bewegen zu bringen, oder einfach warten, bis es aufwacht. Wer weiß, wie lange das dauern könnte...", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle It", - "tooltip": "(-) Fight Sleeping {{snorlaxName}}\n(+) Special Reward", - "selected": "You approach the\nPokémon without fear." + "label": "Kampf beginnen", + "tooltip": "(-) Schlafendes {{snorlaxName}} greift an\n(+) Spezielle Belohnung", + "selected": "Du trittst dem Pokémon ohne Furcht entgegen." }, "2": { - "label": "Wait for It to Move", - "tooltip": "(-) Wait a Long Time\n(+) Recover Party", - "selected": ".@d{32}.@d{32}.@d{32}$You wait for a time, but the {{snorlaxName}}'s yawns make your party sleepy...", - "rest_result": "When you all awaken, the {{snorlaxName}} is no where to be found -\nbut your Pokémon are all healed!" + "label":"Warte, bis es sich bewegt", + "tooltip": "(-) Warte eine lange Zeit\n(+) Dein Team wird geheilt", + "selected": ".@d{32}.@d{32}.@d{32}$Du wartest sehr lange, bis das {{snorlaxName}} endlich aufwacht. Dein Team wird schläfrig...", + "rest_result": "Nachdem ihr alle aufgewacht seid, ist das {{snorlaxName}} nirgends zu finden - aber deine Pokémon sind alle geheilt!" }, "3": { - "label": "Steal Its Item", - "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward", - "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", - "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}}!$@s{item_fanfare}It steals Leftovers off the sleeping\n{{snorlaxName}} and you make out like bandits!" + "label": "Klaue seine Items", + "tooltip": "(+) {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein\n(+) Spezielle Belohnung", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken beherrschen, um diese Option zu wählen.", + "selected": "Dein {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein!$@s{item_fanfare}Es stiehlt die Überreste des schlafenden {{snorlaxName}}s und ihr macht euch aus dem Staub!" } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json index c295867f5218..552f082c20d8 100644 --- a/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json +++ b/src/locales/de/mystery-encounters/teleporting-hijinks-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "It's a strange machine, whirring noisily...", - "title": "Teleportating Hijinks", - "description": "The machine has a sign on it that reads:\n \"To use, insert money then step into the capsule.\"\n\nPerhaps it can transport you somewhere...", - "query": "What will you do?", + "intro": "Es ist eine seltsame Maschine, die laut summt...", + "title": "Teleportierende Streiche", + "description": "Die Maschine hat ein Schild, auf dem steht:\n\"Geld einwerfen und in die Kapsel steigen.\"\nVielleicht kann sie dich irgendwohin transportieren...", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Put Money In", - "tooltip": "(-) Pay {{price, money}}\n(?) Teleport to New Biome", - "selected": "You insert some money, and the capsule opens.\nYou step inside..." + "label": "Geld einwerfen", + "tooltip": "(-) Bezahle {{price, money}}\n(?) Teleportiere dich in ein neues Biom", + "selected": "Du wirfst etwas Geld ein, und die Kapsel öffnet sich.\nDu steigst ein..." }, "2": { - "label": "A Pokémon Helps", - "tooltip": "(-) {{option2PrimaryName}} Helps\n(+) {{option2PrimaryName}} gains EXP\n(?) Teleport to New Biome", - "disabled_tooltip": "You need a Steel or Electric Type Pokémon to choose this", - "selected": "{{option2PrimaryName}}'s Type allows it to bypass the machine's paywall!$The capsule opens, and you step inside..." + "label": "Ein Pokémon hilft", + "tooltip": "(-) {{option2PrimaryName}} hilft\n(+) {{option2PrimaryName}} erhält EXP\n(?) Teleportiere dich in ein neues Biom", + "disabled_tooltip": "Du brauchst ein Stahl- oder Elektro-Pokémon, um diese Option zu wählen.", + "selected": "Der Typ von {{option2PrimaryName}} ermöglicht es ihm, die Bezahlschranke der Maschine zu umgehen!$Die Kapsel öffnet sich, und du steigst ein..." }, "3": { - "label": "Inspect the Machine", - "tooltip": "(-) Pokémon Battle", - "selected": "You are drawn in by the blinking lights\nand strange noises coming from the machine...$You don't even notice as a wild\nPokémon sneaks up and ambushes you!" + "label": "Maschine inspizieren", + "tooltip": "(-) Pokémon-Kampf", + "selected": "Du wirst von den blinkenden Lichtern und den seltsamen Geräuschen der Maschine angezogen...$Du bemerkst nicht einmal, wie ein wildes Pokémon sich anschleicht und dich überfällt!" } }, - "transport": "The machine shakes violently,\nmaking all sorts of strange noises!$Just as soon as it had started, it quiets once more.", - "attacked": "You step out into a completely new area, startling a wild Pokémon!$The wild Pokémon attacks!", - "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" + "transport": "Die Maschine zittert heftig und macht seltsame Geräusche!$Kaum hat es begonnen, wird es wieder ruhig.", + "attacked": "Du trittst in eine völlig neue Gegend und erschreckst ein wildes Pokémon!$Das wilde Pokémon greift an!", + "boss_enraged": "Das wilde {{enemyPokemon}} ist wütend geworden!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json index 7e8091bbfff0..1e055fa5ed09 100644 --- a/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json +++ b/src/locales/de/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -1,23 +1,23 @@ { - "intro": "A chipper elderly man approaches you.", - "speaker": "Gentleman", - "intro_dialogue": "Hello there! Have I got a deal just for YOU!", - "title": "The Pokémon Salesman", - "description": "\"This {{purchasePokemon}} is extremely unique and carries an ability not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", - "description_shiny": "\"This {{purchasePokemon}} is extremely unique and has a pigment not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", - "query": "What will you do?", + "intro": "Ein fröhlicher älterer Mann kommt auf dich zu.", + "speaker": "Reicher Mann", + "intro_dialogue": "Hallo! Ich habe ein Angebot, das du nicht ablehnen kannst!", + "title": "Der Pokémon-Verkäufer", + "description": "Dieses {{purchasePokemon}} ist extrem einzigartig und hat eine Fähigkeit, die normalerweise nicht bei seiner Art zu finden ist! Ich lasse dich dieses tolle {{purchasePokemon}} für gerade einmal {{price, money}} haben!\"\n\"Was sagst du dazu?\"", + "description_shiny": "Dieses {{purchasePokemon}} ist extrem einzigartig und hat eine Farbe, die normalerweise nicht bei seiner Art zu finden ist! Ich lasse dich dieses tolle {{purchasePokemon}} für gerade einmal {{price, money}} haben!\"\n\"Was sagst du dazu?\"", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Accept", - "tooltip": "(-) Pay {{price, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability", - "tooltip_shiny": "(-) Pay {{price, money}}\n(+) Gain a shiny {{purchasePokemon}}", - "selected_message": "You paid an outrageous sum and bought the {{purchasePokemon}}.", - "selected_dialogue": "Excellent choice!$I can see you've a keen eye for business.$Oh, yeah...@d{64} Returns not accepted, got that?" + "label": "Akzeptieren", + "tooltip": "(-) Bezahlen {{price, money}}\n(+) Erhalte ein {{purchasePokemon}} mit seiner versteckten Fähigkeit", + "tooltip_shiny": "(-) Bezahlen {{price, money}}\n(+) Erhalte ein schillerndes {{purchasePokemon}}", + "selected_message": "Du bezahlst einen unverschämten Betrag und kaufst das {{purchasePokemon}}.", + "selected_dialogue": "Ausgezeichnete Wahl!$Ich sehe, dass du ein gutes Auge für Geschäfte hast.$Oh, ja...@d{64} Rückgaben werden nicht akzeptiert, hast du das verstanden?" }, "2": { - "label": "Refuse", - "tooltip": "(-) No Rewards", - "selected": "No?@d{32} You say no?$I'm only doing this as a favor to you!" + "label": "Ablehnen", + "tooltip": "(-) Keine Belohnung", + "selected": "Nein?@d{32} Du sagst nein?$Ich mache das nur als Gefallen für dich!" } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json index b5403616c9b5..0fd1a7ad64f7 100644 --- a/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json +++ b/src/locales/de/mystery-encounters/the-strong-stuff-dialogue.json @@ -1,21 +1,21 @@ { - "intro": "It's a massive {{shuckleName}} and what appears\nto be a large stash of... juice?", - "title": "The Strong Stuff", - "description": "The {{shuckleName}} that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe {{shuckleName}} extends its feelers in your direction. It seems like it wants to do something...", - "query": "What will you do?", + "intro": "Es ist ein riesiger {{shuckleName}} und ein riesiger Vorrat an... Saft?", + "title": "Das gute Zeug", + "description": "Das {{shuckleName}} das deinen Weg blockiert, sieht unglaublich stark aus. In der Zwischenzeit strahlt der Saft daneben eine Art Kraft aus.\nDas {{shuckleName}} streckt seine Fühler in deine Richtung aus. Es scheint, als wolle es etwas tun...", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Approach the {{shuckleName}}", - "tooltip": "(?) Something awful or amazing might happen", - "selected": "You black out.", - "selected_2": "@f{150}When you awaken, the {{shuckleName}} is gone\nand juice stash completely drained.${{highBstPokemon1}} and {{highBstPokemon2}}\nfeel a terrible lethargy come over them!$Their base stats were reduced by {{reductionValue}}!$Your remaining Pokémon feel an incredible vigor, though!\nTheir base stats are increased by {{increaseValue}}!" + "label": "Dem {{shuckleName}} näher kommen", + "tooltip": "(?) Etwas Schreckliches oder Wunderbares könnte passieren", + "selected": "Dir wird schwarz vor Augen...", + "selected_2": "@f{150}Als du aufwachst, ist das {{shuckleName}} verschwunden und der Saftvorrat komplett geleert.${{highBstPokemon1}} und {{highBstPokemon2}} fühlen eine schreckliche Lethargie über sich kommen!$Ihre Basiswerte wurden um {{reductionValue}} reduziert!$Deine verbleibenden Pokémon fühlen jedoch eine unglaubliche Vitalität!$Ihre Basiswerte werden um {{increaseValue}} erhöht!" }, "2": { - "label": "Battle the {{shuckleName}}", - "tooltip": "(-) Hard Battle\n(+) Special Rewards", - "selected": "Enraged, the {{shuckleName}} drinks some of its juice and attacks!", - "stat_boost": "The {{shuckleName}}'s juice boosts its stats!" + "label": "Das {{shuckleName}} bekämpfen", + "tooltip": "(-) Schwieriger Kampf\n(+) Spezielle Belohnungen", + "selected": "Das {{shuckleName}} wird wütend und trinkt etwas von seinem Saft, bevor es angreift!", + "stat_boost": "Der Saft des {{shuckleName}} erhöht seine Werte!" } }, - "outro": "What a bizarre turn of events." + "outro": "Was ist hier gerade passiert?" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json index 37807a91667e..e3d0dddd21a4 100644 --- a/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json +++ b/src/locales/de/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -1,22 +1,22 @@ { - "intro": "It's a family standing outside their house!", - "speaker": "The Winstrates", - "intro_dialogue": "We're the Winstrates!$What do you say to taking on our family in a series of Pokémon battles?", - "title": "The Winstrate Challenge", - "description": "The Winstrates are a family of 5 trainers, and they want to battle! If you beat all of them back-to-back, they'll give you a grand prize. But can you handle the heat?", - "query": "What will you do?", + "intro": "Eine Familie steht vor ihrem Haus!", + "speaker": "Die Sihgers", + "intro_dialogue": "Wir sind die Sihgers!$Wie wäre es, wenn du gegen unsere Familie in einer Reihe von Pokémon-Kämpfen antrittst?", + "title": "Die Sihgers-Herausforderung", + "description": "Die Sihgers sind eine Familie von 5 Trainern, und sie wollen kämpfen! Wenn du sie alle hintereinander besiegst, bekommst du einen grandiosen Preis. Aber kannst du die Hitze aushalten?", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Accept the Challenge", - "tooltip": "(-) Brutal Battle\n(+) Special Item Reward", - "selected": "Let the challenge begin!" + "label": "Die Herausforderung annehmen", + "tooltip": "(-) Brutaler Kampf\n(+) Spezielle Belohnung", + "selected": "Lass die Herausforderung beginnen!" }, "2": { - "label": "Refuse the Challenge", - "tooltip": "(+) Full Heal Party\n(+) Gain a Rarer Candy", - "selected": "That's too bad. Say, your team looks worn out, why don't you stay awhile and rest?" + "label": "Die Herausforderung ablehnen", + "tooltip": "(+) Team wird geheilt\n(+) Erhalte ein Supersondererbonbon", + "selected": "Das ist zu schade. Dein Team sieht ziemlich mitgenommen aus, warum ruhst du dich nicht eine Weile aus?" } }, - "victory": "Congratulations on beating our challenge!$First off, we'd like you to have this Voucher.", - "victory_2": "Also, our family uses this Macho Brace to strengthen\nour Pokémon more effectively during training.$You may not need it considering that you beat the whole lot of us, but we hope you'll accept it anyway!" + "victory": "Glückwunsch, du hast unsere Herausforderung gemeistert!$Zuerst möchten wir dir diesen Gutschein geben.", + "victory_2": "Außerdem benutzt unsere Familie diese Machoschiene, um unsere Pokémon effektiver zu tranieren.$Du brauchst es vielleicht nicht, da du uns alle geschlagen hast, aber wir hoffen, dass du es trotzdem annimmst!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/training-session-dialogue.json b/src/locales/de/mystery-encounters/training-session-dialogue.json index f018018fe4e2..f7d22ef6deba 100644 --- a/src/locales/de/mystery-encounters/training-session-dialogue.json +++ b/src/locales/de/mystery-encounters/training-session-dialogue.json @@ -1,33 +1,33 @@ { - "intro": "You've come across some\ntraining tools and supplies.", - "title": "Training Session", - "description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", - "query": "How should you train?", - "invalid_selection": "Pokémon must be healthy enough.", + "intro": "Du stolperst über einige Trainingsutensilien und Vorräte.", + "title": "Traningssitzung", + "description": "Diese Vorräte sehen so aus, als könnten sie verwendet werden, um ein Mitglied deines Teams zu trainieren! Es gibt ein paar Möglichkeiten, wie du dein Pokémon trainieren könntest, indem du gegen es mit dem Rest deines Teams kämpfst.", + "query": "Wie möchtest du trainieren?", + "invalid_selection": "Pokémon muss genügend KP haben.", "option": { "1": { - "label": "Light Training", - "tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its {{stat1}} and {{stat2}} IVs were improved!" + "label": "Leichtes Training", + "tooltip": "(-) Leichter Kampf\n(+) Verbessere 2 zufällige IS-Werte des Pokémon", + "finished": "{{selectedPokemon}} kommt zurück, fühlt sich erschöpft aber zufrieden!$Seine {{stat1}} und {{stat2}} IS-Werte wurden verbessert!" }, "2": { - "label": "Moderate Training", - "tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature", - "select_prompt": "Select a new nature\nto train your Pokémon in.", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its nature was changed to {{nature}}!" + "label": "Moderates Training", + "tooltip": "(-) Moderater Kampf\n(+) Ändere das Wesen des Pokémon", + "select_prompt": "Wähle ein neues Wesen aus, um dein Pokémon zu trainieren.", + "finished": "{{selectedPokemon}} kehrt zurück, fühlt sich erschöpft aber zufrieden!$Es hat nun ein neues Wesen: {{nature}}!" }, "3": { - "label": "Heavy Training", - "tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability", - "select_prompt": "Select a new ability\nto train your Pokémon in.", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its ability was changed to {{ability}}!" + "label": "Schweres Training", + "tooltip": "(-) Harter Kampf\n(+) Ändere die Fähigkeit des Pokémon", + "select_prompt": "Wähle eine neue Fähigkeit aus, um dein Pokémon zu trainieren.", + "finished": "{{selectedPokemon}} kehrt zurück, fühlt sich erschöpft aber zufrieden!$Seine Fähigkeit wurde zu {{ability}} geändert!" }, "4": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You've no time for training.\nTime to move on." + "label": "Weggehen", + "tooltip": "(-) Keine Belohnung", + "selected": "Du hast keine Zeit für Training und gehst weiter." }, - "selected": "{{selectedPokemon}} moves across\nthe clearing to face you..." + "selected": "{{selectedPokemon}} bewegt sich über die Lichtung, um dir gegenüberzutreten..." }, - "outro": "That was a successful training session!" + "outro": "Das war eine erfolgreiche Trainingssitzung!" } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json index ae6e63ed8007..da744ccb697e 100644 --- a/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json +++ b/src/locales/de/mystery-encounters/trash-to-treasure-dialogue.json @@ -1,19 +1,20 @@ { - "intro": "It's a massive pile of garbage!\nWhere did this come from?", - "title": "Trash to Treasure", - "description": "The garbage heap looms over you, and you can spot some items of value buried amidst the refuse. Are you sure you want to get covered in filth to get them, though?", - "query": "What will you do?", + "intro":"Ein riesieger Haufen Müll. Wo kommt der auf einmal her?", + "title": "Vom Müllhaufen zum Schatzhaufen", + "description": "Der Müllberg ragt über dir auf und du kannst einige wertvolle Gegenstände im Müll entdecken. Bist du sicher, dass du dich in den Dreck wälzen willst, um sie zu bekommen?", + "query": "Was willst du tun?", "option": { "1": { - "label": "Dig for Valuables", - "tooltip": "(-) Lose Healing Items in Shops\n(+) Gain Amazing Items", - "selected": "You wade through the garbage pile, becoming mired in filth.$There's no way any respectable shopkeepers\nwill sell you anything in your grimy state!$You'll just have to make do without shop healing items.$However, you found some incredible items in the garbage!" + "label": "Nach Wertsachen suchen", + "tooltip": "(-) Heilitems kosten ab jetzt das Dreifache\n(+) Erhalte tolle Items", + "selected": "Du arbeitest dich durch den Müllhaufen und wirst von Dreck überzogen.$Kein respektabler Ladenbesitzer wird dir in deinem schmutzigen Zustand etwas verkaufen!$Aber es gibt ja auch andere... weniger respektable.$Natürlich verlangen sie höhere Preise.$Aber du hast einige unglaubliche Items im Müll gefunden!" + }, "2": { - "label": "Investigate Further", - "tooltip": "(?) Find the Source of the Garbage", - "selected": "You wander around the heap, searching for any indication as to how this might have appeared here...", - "selected_2": "Suddenly, the garbage shifts! It wasn't just garbage, it was a Pokémon!" + "label": "Genauer untersuchen", + "tooltip": "(?) Finde die Quelle des Mülls", + "selected": "Du wanderst um den Müllhaufen herum und suchst nach Hinweisen, wie dieser hier gelandet sein könnte...", + "selected_2": "Der Müll bewegt sich! Es war nicht nur Müll, es war ein Pokémon!" } } -} \ No newline at end of file +} diff --git a/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json index e6f5b3d3fcd5..efd5ff52e16a 100644 --- a/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json +++ b/src/locales/de/mystery-encounters/uncommon-breed-dialogue.json @@ -1,26 +1,26 @@ { - "intro": "That isn't just an ordinary Pokémon!", - "title": "Uncommon Breed", - "description": "That {{enemyPokemon}} looks special compared to others of its kind. @[TOOLTIP_TITLE]{Perhaps it knows a special move?} You could battle and catch it outright, but there might also be a way to befriend it.", - "query": "What will you do?", + "intro": "Das ist kein gewöhnliches Pokémon!", + "title": "Ungewöhnliche Züchtung", + "description": "Das {{enemyPokemon}} sieht im Vergleich zu anderen seiner Art besonders aus. @[TOOLTIP_TITLE]{Vielleicht kennt es einen besondere Attacke?} Du könntest es einfach bekämpfen und fangen, aber es gibt vielleicht auch eine Möglichkeit, es zu befreunden.", + "query": "Was wirst du tun?", "option": { "1": { - "label": "Battle the Pokémon", - "tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe", - "selected": "You approach the\n{{enemyPokemon}} without fear.", - "stat_boost": "The {{enemyPokemon}}'s heightened abilities boost its stats!" + "label": "Kampf beginnen", + "tooltip": "(-) Schwieriger Kampf\n(+) Starkes fangbares Pokémon", + "selected": "Du stellst dich dem {{enemyPokemon}} ohne Furcht.", + "stat_boost": "Die gesteigerten Fähigkeiten des {{enemyPokemon}} erhöhen seine Werte!" }, "2": { - "label": "Give It Food", - "disabled_tooltip": "You need 4 berry items to choose this", - "tooltip": "(-) Give 4 Berries\n(+) The {{enemyPokemon}} Likes You", - "selected": "You toss the berries at the {{enemyPokemon}}!$It eats them happily!$The {{enemyPokemon}} wants to join your party!" + "label": "Ihm Futter geben", + "disabled_tooltip": "Du brauchst 4 Beeren, um diese Option zu wählen", + "tooltip": "(-) Gib 4 Beeren\n(+) Das {{enemyPokemon}} mag dich", + "selected": "Du wirfst die Beeren zu {{enemyPokemon}}!$Es frisst sie glücklich!$Das {{enemyPokemon}} möchte sich dir anschließen!" }, "3": { - "label": "Befriend It", - "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", - "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) The {{enemyPokemon}} Likes You", - "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}} to charm the {{enemyPokemon}}!$The {{enemyPokemon}} wants to join your party!" + "label": "Es befreunden", + "disabled_tooltip": "Dein Pokémon muss bestimmte Attacken kennen, um diese Option zu wählen", + "tooltip": "(+) {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein\n(+) Das {{enemyPokemon}} mag dich", + "selected": "Dein {{option3PrimaryName}} setzt {{option3PrimaryMove}} ein, um das {{enemyPokemon}} zu bezaubern!$Das {{enemyPokemon}} möchte sich dir anschließen!" } } } \ No newline at end of file diff --git a/src/locales/de/mystery-encounters/weird-dream-dialogue.json b/src/locales/de/mystery-encounters/weird-dream-dialogue.json index 44acde84002c..11fe3b4078ce 100644 --- a/src/locales/de/mystery-encounters/weird-dream-dialogue.json +++ b/src/locales/de/mystery-encounters/weird-dream-dialogue.json @@ -1,22 +1,22 @@ { - "intro": "A shadowy woman blocks your path.\nSomething about her is unsettling...", - "speaker": "Woman", - "intro_dialogue": "I have seen your futures, your pasts...$Child, do you see them too?", + "intro": "Eine schemenhafte Frau versperrt dir den Weg. Irgendetwas an ihr ist beunruhigend...", + "speaker": "Frau", + "intro_dialogue": "Ich habe deine Zukünfte gesehen, deine Vergangenheiten...$Siehst du sie auch?", "title": "???", - "description": "The woman's words echo in your head. It wasn't just a singular voice, but a vast multitude, from all timelines and realities. You begin to feel dizzy, the question lingering on your mind...\n\n@[TOOLTIP_TITLE]{\"I have seen your futures, your pasts... Child, do you see them too?\"}", - "query": "What will you do?", + "description": "Die Worte der Frau hallen in deinem Kopf wider. Es war nicht nur eine einzelne Stimme, sondern eine unendliche Vielzahl aus allen Zeiten und Realitäten. Dir wird schwindelig, die Frage bleibt in deinem Kopf hängen...\n@[TOOLTIP_TITLE]{\"Ich habe deine Zukünfte gesehen, deine Vergangenheiten...Siehst du sie auch?\"}", + "query": "Was wirst du tun?", "option": { "1": { - "label": "\"I See Them\"", - "tooltip": "@[SUMMARY_GREEN]{(?) Affects your Pokémon}", - "selected": "Her hand reaches out to touch you,\nand everything goes black.$Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.$Everything that has made you,\neverything you will become...@d{64}", - "cutscene": "You see your Pokémon,@d{32} converging from\nevery reality to become something new...@d{64}", - "dream_complete": "When you awaken, the woman - was it a woman or a ghost? - is gone...$.@d{32}.@d{32}.@d{32}$Your Pokémon team has changed...\nOr is it the same team you've always had?" + "label": "\"Ich sehe sie\"", + "tooltip": "@[SUMMARY_GREEN]{(?) Beeinflusst deine Pokémon}", + "selected": "Ihre Hand berührt dich und alles wird schwarz.$Dann...@d{64} Du siehst alles. Jede Zeitlinie, all deine verschiedenen Ichs, Vergangenheit und Zukunft.$Alles, was dich ausmacht, alles, was du sein wirst...@d{64}", + "cutscene": "Du siehst deine Pokémon,@d{32} wie sie sich aus jeder Realität vereinen, um etwas Neues zu werden...@d{64}", + "dream_complete": "Als du erwachst, ist die Frau - war es eine Frau oder ein Geist? - verschwunden...$.@d{32}.@d{32}.@d{32}$Dein Pokémon-Team hat sich verändert... Oder ist es das gleiche Team, das du schon immer hattest?" }, "2": { - "label": "Quickly Leave", - "tooltip": "(-) Affects your Pokémon", - "selected": "You tear your mind from a numbing grip, and hastily depart.$When you finally stop to collect yourself, you check the Pokémon in your team.$For some reason, all of their levels have decreased!" + "label": "Schnell wegrennen", + "tooltip": "(-) Beeinflusst deine Pokémon", + "selected": "Du reißt deinen Geist aus einem betäubenden Griff und fliehst hastig.$Als du schließlich anhältst, um dich zu sammeln, überprüfst du die Pokémon in deinem Team.$Aus irgendeinem Grund hat sich das Level aller Pokémon verringert!" } } } \ No newline at end of file diff --git a/src/locales/de/party-ui-handler.json b/src/locales/de/party-ui-handler.json index 520c5850b032..8c662ef2b789 100644 --- a/src/locales/de/party-ui-handler.json +++ b/src/locales/de/party-ui-handler.json @@ -15,7 +15,7 @@ "UNPAUSE_EVOLUTION": "Entwicklung fortsetzen", "REVIVE": "Wiederbeleben", "RENAME": "Umbenennen", - "SELECT": "Select", + "SELECT": "Auswählen", "choosePokemon": "Wähle ein Pokémon.", "doWhatWithThisPokemon": "Was soll mit diesem Pokémon geschehen?", "noEnergy": "{{pokemonName}} ist nicht fit genug, um zu kämpfen!", diff --git a/src/locales/de/trainer-names.json b/src/locales/de/trainer-names.json index 6199e252eb8b..2a6ecf91324b 100644 --- a/src/locales/de/trainer-names.json +++ b/src/locales/de/trainer-names.json @@ -163,15 +163,15 @@ "piers_marnie_double": "Nezz & Mary", "marnie_piers_double": "Mary & Nezz", - "buck": "Buck", - "cheryl": "Cheryl", - "marley": "Marley", - "mira": "Mira", - "riley": "Riley", - "victor": "Victor", - "victoria": "Victoria", - "vivi": "Vivi", + "buck": "Avenaro", + "cheryl": "Raissa", + "marley": "Charlie", + "mira": "Orisa", + "riley": "Urs", + "victor": "Viktor", + "victoria": "Viktoria", + "vivi": "Sieglinde", "vicky": "Vicky", - "vito": "Vito", - "bug_type_superfan": "Bug-Type Superfan" + "vito": "Paul", + "bug_type_superfan": "Käfersammler-Superfan" } diff --git a/src/locales/de/trainer-titles.json b/src/locales/de/trainer-titles.json index a0d2eca348ee..6f6bace7fc85 100644 --- a/src/locales/de/trainer-titles.json +++ b/src/locales/de/trainer-titles.json @@ -35,5 +35,5 @@ "skull_admin": "Team Skull Vorstand", "macro_admin": "Vizepräsidentin von Macro Cosmos", - "the_winstrates": "The Winstrates'" + "the_winstrates": "Sihgers" } diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index 217c77422d19..d04dd3eac1f7 100644 --- a/src/locales/en/battle.json +++ b/src/locales/en/battle.json @@ -14,6 +14,10 @@ "moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} was caught!", + "pokemonObtained": "You got {{pokemonName}}!", + "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", + "pokemonFled": "The wild {{pokemonName}} fled!", + "playerFled": "You fled from the {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} has been\nadded as a starter!", "partyFull": "Your party is full.\nRelease a Pokémon to make room for {{pokemonName}}?", "pokemon": "Pokémon", @@ -52,6 +56,7 @@ "noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!", "noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!", "noPokeballStrong": "The target Pokémon is too strong to be caught!\nYou need to weaken it first!", + "noPokeballMysteryEncounter": "You aren't able to\ncatch this Pokémon!", "noEscapeForce": "An unseen force\nprevents escape.", "noEscapeTrainer": "You can't run\nfrom a trainer battle!", "noEscapePokemon": "{{pokemonName}}'s {{moveName}}\nprevents {{escapeVerb}}!", @@ -99,5 +104,6 @@ "unlockedSomething": "{{unlockedThing}}\nhas been unlocked.", "congratulations": "Congratulations!", "beatModeFirstTime": "{{speciesName}} beat {{gameMode}} Mode for the first time!\nYou received {{newModifier}}!", - "ppReduced": "It reduced the PP of {{targetName}}'s\n{{moveName}} by {{reduction}}!" + "ppReduced": "It reduced the PP of {{targetName}}'s\n{{moveName}} by {{reduction}}!", + "mysteryEncounterAppeared": "What's this?" } \ No newline at end of file diff --git a/src/locales/en/bgm-name.json b/src/locales/en/bgm-name.json index 8838942c8a64..c2babed4dffb 100644 --- a/src/locales/en/bgm-name.json +++ b/src/locales/en/bgm-name.json @@ -146,5 +146,10 @@ "encounter_youngster": "BW Trainers' Eyes Meet (Youngster)", "heal": "BW Pokémon Heal", "menu": "PMD EoS Welcome to the World of Pokémon!", - "title": "PMD EoS Top Menu Theme" + "title": "PMD EoS Top Menu Theme", + + "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" } diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 024f7f101083..c033e1852253 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -53,7 +53,48 @@ import terrain from "./terrain.json"; import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import moveTriggers from "./move-trigger.json"; import runHistory from "./run-history.json"; +import mysteryEncounterMessages from "./mystery-encounter-messages.json"; +import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json"; +import mysteriousChest from "./mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengers from "./mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDeal from "./mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSale from "./mystery-encounters/department-store-sale-dialogue.json"; +import fieldTrip from "./mystery-encounters/field-trip-dialogue.json"; +import fieryFallout from "./mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlight from "./mystery-encounters/fight-or-flight-dialogue.json"; +import safariZone from "./mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealer from "./mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlax from "./mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSession from "./mystery-encounters/training-session-dialogue.json"; +import theStrongStuff from "./mystery-encounters/the-strong-stuff-dialogue.json"; +import pokemonSalesman from "./mystery-encounters/the-pokemon-salesman-dialogue.json"; +import offerYouCantRefuse from "./mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdy from "./mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvarice from "./mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTest from "./mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasure from "./mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAbound from "./mystery-encounters/berries-abound-dialogue.json"; +import clowningAround from "./mystery-encounters/clowning-around-dialogue.json"; +import partTimer from "./mystery-encounters/part-timer-dialogue.json"; +import dancingLessons from "./mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDream from "./mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallenge from "./mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinks from "./mystery-encounters/teleporting-hijinks-dialogue.json"; +import bugTypeSuperfan from "./mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGames from "./mystery-encounters/fun-and-games-dialogue.json"; +import uncommonBreed from "./mystery-encounters/uncommon-breed-dialogue.json"; +import globalTradeSystem from "./mystery-encounters/global-trade-system-dialogue.json"; +/** + * Dialogue/Text token injection patterns that can be used: + * - `$` will be treated as a new line for Message and Dialogue strings. + * - `@d{}` will add a time delay to text animation for Message and Dialogue strings. + * - `@s{}` will play a specified sound effect for Message and Dialogue strings. + * - `@f{}` will fade the screen to black for the given duration, then fade back in for Message and Dialogue strings. + * - `{{}}` (MYSTERY ENCOUNTERS ONLY) will auto-inject the matching dialogue token value that is stored in {@link IMysteryEncounter.dialogueTokens}. + * - (see [i18next interpolations](https://www.i18next.com/translation-function/interpolation)) for more details. + * - `@[]{}` (STATIC TEXT ONLY, NOT USEABLE WITH {@link UI.showText()} OR {@link UI.showDialogue()}) will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`). + */ export const enConfig = { ability, abilityTriggers, @@ -109,5 +150,40 @@ export const enConfig = { partyUiHandler, modifierSelectUiHandler, moveTriggers, - runHistory + runHistory, + mysteryEncounter: { + // DO NOT REMOVE + "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + mysteriousChallengers, + mysteriousChest, + darkDeal, + fightOrFlight, + slumberingSnorlax, + trainingSession, + departmentStoreSale, + shadyVitaminDealer, + fieldTrip, + safariZone, + lostAtSea, + fieryFallout, + theStrongStuff, + pokemonSalesman, + offerYouCantRefuse, + delibirdy, + absoluteAvarice, + aTrainersTest, + trashToTreasure, + berriesAbound, + clowningAround, + partTimer, + dancingLessons, + weirdDream, + theWinstrateChallenge, + teleportingHijinks, + bugTypeSuperfan, + funAndGames, + uncommonBreed, + globalTradeSystem + }, + mysteryEncounterMessages }; diff --git a/src/locales/en/dialogue.json b/src/locales/en/dialogue.json index 5565d2258c29..39a4238355c4 100644 --- a/src/locales/en/dialogue.json +++ b/src/locales/en/dialogue.json @@ -985,6 +985,116 @@ "1": "I suppose it must seem that I am doing something terrible. I don't expect you to understand.\n$But I must provide the Galar region with limitless energy to ensure everlasting prosperity." } }, + "stat_trainer_buck": { + "encounter": { + "1": "...I'm telling you right now. I'm seriously tough. Act surprised!", + "2": "I can feel my Pokémon shivering inside their Pokéballs!" + }, + "victory": { + "1": "Heeheehee!\nSo hot, you!", + "2": "Heeheehee!\nSo hot, you!" + }, + "defeat": { + "1": "Whoa! You're all out of gas, I guess.", + "2": "Whoa! You're all out of gas, I guess." + } + }, + "stat_trainer_cheryl": { + "encounter": { + "1": "My Pokémon have been itching for a battle.", + "2": "I should warn you, my Pokémon can be quite rambunctious." + }, + "victory": { + "1": "Striking the right balance of offense and defense... It's not easy to do.", + "2": "Striking the right balance of offense and defense... It's not easy to do." + }, + "defeat": { + "1": "Do your Pokémon need any healing?", + "2": "Do your Pokémon need any healing?" + } + }, + "stat_trainer_marley": { + "encounter": { + "1": "... OK.\nI'll do my best.", + "2": "... OK.\nI... won't lose...!" + }, + "victory": { + "1": "... Awww.", + "2": "... Awww." + }, + "defeat": { + "1": "... Goodbye.", + "2": "... Goodbye." + } + }, + "stat_trainer_mira": { + "encounter": { + "1": "You will be shocked by Mira!", + "2": "Mira will show you that Mira doesn't get lost anymore!" + }, + "victory": { + "1": "Mira wonders if she can get very far in this land.", + "2": "Mira wonders if she can get very far in this land." + }, + "defeat": { + "1": "Mira knew she would win!", + "2": "Mira knew she would win!" + } + }, + "stat_trainer_riley": { + "encounter": { + "1": "Battling is our way of greeting!", + "2": "We're pulling out all the stops to put your Pokémon down." + }, + "victory": { + "1": "At times we battle, and sometimes we team up...$It's great how Trainers can interact.", + "2": "At times we battle, and sometimes we team up...$It's great how Trainers can interact." + }, + "defeat": { + "1": "You put up quite the display.\nBetter luck next time.", + "2": "You put up quite the display.\nBetter luck next time." + } + }, + "winstrates_victor": { + "encounter": { + "1": "That's the spirit! I like you!" + }, + "victory": { + "1": "A-ha! You're stronger than I thought!" + } + }, + "winstrates_victoria": { + "encounter": { + "1": "My goodness! Aren't you young?$You must be quite the trainer to beat my husband, though.$Now I suppose it's my turn to battle!" + }, + "victory": { + "1": "Uwah! Just how strong are you?!" + } + }, + "winstrates_vivi": { + "encounter": { + "1": "You're stronger than Mom? Wow!$But I'm strong, too!\nReally! Honestly!" + }, + "victory": { + "1": "Huh? Did I really lose?\nSnivel... Grandmaaa!" + } + }, + "winstrates_vicky": { + "encounter": { + "1": "How dare you make my precious\ngranddaughter cry!$I see I need to teach you a lesson.\nPrepare to feel the sting of defeat!" + }, + "victory": { + "1": "Whoa! So strong!\nMy granddaughter wasn't lying." + } + }, + "winstrates_vito": { + "encounter": { + "1": "I trained together with my whole family,\nevery one of us!$I'm not losing to anyone!" + }, + "victory": { + "1": "I was better than everyone in my family.\nI've never lost before..." + } + }, "brock": { "encounter": { "1": "My expertise on Rock-type Pokémon will take you down! Come on!", diff --git a/src/locales/en/egg.json b/src/locales/en/egg.json index 8a5e061d883d..d6b352fca1e2 100644 --- a/src/locales/en/egg.json +++ b/src/locales/en/egg.json @@ -11,6 +11,7 @@ "gachaTypeLegendary": "Legendary Rate Up", "gachaTypeMove": "Rare Egg Move Rate Up", "gachaTypeShiny": "Shiny Rate Up", + "eventType": "Mystery Event", "selectMachine": "Select a machine.", "notEnoughVouchers": "You don't have enough vouchers!", "tooManyEggs": "You have too many eggs!", diff --git a/src/locales/en/modifier-select-ui-handler.json b/src/locales/en/modifier-select-ui-handler.json index bc49ce259310..15c930fb65e0 100644 --- a/src/locales/en/modifier-select-ui-handler.json +++ b/src/locales/en/modifier-select-ui-handler.json @@ -8,5 +8,7 @@ "lockRaritiesDesc": "Lock item rarities on reroll (affects reroll cost).", "checkTeamDesc": "Check your team or use a form changing item.", "rerollCost": "₽{{formattedMoney}}", - "itemCost": "₽{{formattedMoney}}" + "itemCost": "₽{{formattedMoney}}", + "continueNextWaveButton": "Continue", + "continueNextWaveDescription": "Continue to the next wave." } \ No newline at end of file diff --git a/src/locales/en/modifier-type.json b/src/locales/en/modifier-type.json index babad57b81b7..c362b3f30d46 100644 --- a/src/locales/en/modifier-type.json +++ b/src/locales/en/modifier-type.json @@ -68,6 +68,20 @@ "BaseStatBoosterModifierType": { "description": "Increases the holder's base {{stat}} by 10%. The higher your IVs, the higher the stack limit." }, + "PokemonBaseStatTotalModifierType": { + "name": "Shuckle Juice", + "description": "{{increaseDecrease}} all of the holder's base stats by {{statValue}}. You were {{blessCurse}} by the Shuckle.", + "extra": { + "increase": "Increases", + "decrease": "Decreases", + "blessed": "blessed", + "cursed": "cursed" + } + }, + "PokemonBaseStatFlatModifierType": { + "name": "Old Gateau", + "description": "Increases the holder's {{stats}} base stats by {{statValue}}. Found after a strange dream." + }, "AllPokemonFullHpRestoreModifierType": { "description": "Restores 100% HP for all Pokémon." }, @@ -226,6 +240,8 @@ "TOXIC_ORB": { "name": "Toxic Orb", "description": "It's a bizarre orb that exudes toxins when touched and will badly poison the holder during battle." }, "FLAME_ORB": { "name": "Flame Orb", "description": "It's a bizarre orb that gives off heat when touched and will affect the holder with a burn during battle." }, + "EVOLUTION_TRACKER_GIMMIGHOUL": { "name": "Treasures", "description": "This Pokémon loves treasure! Keep collecting treasure and something might happen!"}, + "BATON": { "name": "Baton", "description": "Allows passing along effects when switching Pokémon, which also bypasses traps." }, "SHINY_CHARM": { "name": "Shiny Charm", "description": "Dramatically increases the chance of a wild Pokémon being Shiny." }, @@ -247,7 +263,13 @@ "ENEMY_ATTACK_BURN_CHANCE": { "name": "Burn Token" }, "ENEMY_STATUS_EFFECT_HEAL_CHANCE": { "name": "Full Heal Token", "description": "Adds a 2.5% chance every turn to heal a status condition." }, "ENEMY_ENDURE_CHANCE": { "name": "Endure Token" }, - "ENEMY_FUSED_CHANCE": { "name": "Fusion Token", "description": "Adds a 1% chance that a wild Pokémon will be a fusion." } + "ENEMY_FUSED_CHANCE": { "name": "Fusion Token", "description": "Adds a 1% chance that a wild Pokémon will be a fusion." }, + + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Shuckle Juice" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Black Sludge", "description": "The stench is so powerful that shops will only sell you items at a steep cost increase." }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Macho Brace", "description": "Defeating a Pokémon grants the holder a Macho Brace stack. Each stack slightly boosts stats, with an extra bonus at max stacks." }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Old Gateau", "description": "Increases the holder's {{stats}} stats by {{statValue}}." }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Imbues the owner with luck to find Bug Type Pokémon more often. Has a strange heft to it." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { "name": "Light Ball", "description": "It's a mysterious orb that boosts Pikachu's Attack and Sp. Atk stats." }, @@ -310,6 +332,21 @@ "TART_APPLE": "Tart Apple", "STRAWBERRY_SWEET": "Strawberry Sweet", "UNREMARKABLE_TEACUP": "Unremarkable Teacup", + "UPGRADE": "Upgrade", + "DUBIOUS_DISC": "Dubious Disc", + "DRAGON_SCALE": "Dragon Scale", + "PRISM_SCALE": "Prism Scale", + "RAZOR_CLAW": "Razor Claw", + "RAZOR_FANG": "Razor Fang", + "REAPER_CLOTH": "Reaper Cloth", + "ELECTIRIZER": "Electirizer", + "MAGMARIZER": "Magmarizer", + "PROTECTOR": "Protector", + "SACHET": "Sachet", + "WHIPPED_DREAM": "Whipped Dream", + "LEADERS_CREST": "Leader's Crest", + "SUN_FLUTE": "Sun Flute", + "MOON_FLUTE": "Moon Flute", "CHIPPED_POT": "Chipped Pot", "BLACK_AUGURITE": "Black Augurite", diff --git a/src/locales/en/mystery-encounter-messages.json b/src/locales/en/mystery-encounter-messages.json new file mode 100644 index 000000000000..3b81c8e46f0a --- /dev/null +++ b/src/locales/en/mystery-encounter-messages.json @@ -0,0 +1,7 @@ +{ + "paid_money": "You paid ₽{{amount, number}}.", + "receive_money": "You received ₽{{amount, number}}!", + "affects_pokedex": "Affects Pokédex Data", + "cancel_option": "Return to encounter option select.", + "view_party_button": "View Party" +} diff --git a/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json new file mode 100644 index 000000000000..c96c0d5f3277 --- /dev/null +++ b/src/locales/en/mystery-encounters/a-trainers-test-dialogue.json @@ -0,0 +1,47 @@ +{ + "intro": "An extremely strong trainer approaches you...", + "buck": { + "intro_dialogue": "Yo, trainer! My name's Buck.$I have a super awesome proposal\nfor a strong trainer such as yourself!$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer egg!", + "accept": "Whoooo, I'm getting fired up!", + "decline": "Darn, it looks like your\nteam isn't in peak condition.$Here, let me help with that." + }, + "cheryl": { + "intro_dialogue": "Hello, my name's Cheryl.$I have a particularly interesting request,\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you can prove your strength as a trainer to me,\nI'll give you the rarer Egg!", + "accept": "I hope you're ready!", + "decline": "I understand, it looks like your team\nisn't in the best condition at the moment.$Here, let me help with that." + }, + "marley": { + "intro_dialogue": "...@d{64} I'm Marley.$I have an offer for you...$I'm carrying two Pokémon Eggs with me,\nbut I'd like someone else to care for one.$If you're stronger than me,\nI'll give you the rarer Egg.", + "accept": "... I see.", + "decline": "... I see.$Your Pokémon look hurt...\nLet me help." + }, + "mira": { + "intro_dialogue": "Hi! I'm Mira!$Mira has a request\nfor a strong trainer like you!$Mira has two rare Pokémon Eggs,\nbut Mira wants someone else to take one!$If you show Mira that you're strong,\nMira will give you the rarer Egg!", + "accept": "You'll battle Mira?\nYay!", + "decline": "Aww, no battle?\nThat's okay!$Here, Mira will heal your team!" + }, + "riley": { + "intro_dialogue": "I'm Riley.$I have an odd proposal\nfor a strong trainer such as yourself.$I'm carrying two rare Pokémon Eggs with me,\nbut I'd like to give one to another trainer.$If you can prove your strength to me,\nI'll give you the rarer Egg!", + "accept": "That look you have...\nLet's do this.", + "decline": "I understand, your team looks beat up.$Here, let me help with that." + }, + "title": "A Trainer's Test", + "description": "It seems this trainer is willing to give you an Egg regardless of your decision. However, if you can manage to defeat this strong trainer, you'll receive a much rarer Egg.", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept the Challenge", + "tooltip": "(-) Tough Battle\n(+) Gain a @[TOOLTIP_TITLE]{Very Rare Egg}" + }, + "2": { + "label": "Refuse the Challenge", + "tooltip": "(+) Full Heal Party\n(+) Gain an @[TOOLTIP_TITLE]{Egg}" + } + }, + "eggTypes": { + "rare": "a Rare Egg", + "epic": "an Epic Egg", + "legendary": "a Legendary Egg" + }, + "outro": "{{statTrainerName}} gave you {{eggType}}!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json new file mode 100644 index 000000000000..1d675d936609 --- /dev/null +++ b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "A {{greedentName}} ambushes you\nand steals your party's berries!", + "title": "Absolute Avarice", + "description": "The {{greedentName}} has caught you totally off guard now all your berries are gone!\n\nThe {{greedentName}} looks like it's about to eat them when it pauses to look at you, interested.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Tough Battle\n(+) Rewards from its Berry Hoard", + "selected": "The {{greedentName}} stuffs its cheeks\nand prepares for battle!", + "boss_enraged": "{{greedentName}}'s fierce love for food has it incensed!", + "food_stash": "It looks like the {{greedentName}} was guarding an enormous stash of food!$@s{item_fanfare}Each Pokémon in your party gains a {{foodReward}}!" + }, + "2": { + "label": "Reason with It", + "tooltip": "(+) Regain Some Lost Berries", + "selected": "Your pleading strikes a chord with the {{greedentName}}.$It doesn't give all your berries back, but still tosses a few in your direction." + }, + "3": { + "label": "Let It Have the Food", + "tooltip": "(-) Lose All Berries\n(?) The {{greedentName}} Will Like You", + "selected": "The {{greedentName}} devours the entire\nstash of berries in a flash!$Patting its stomach,\nit looks at you appreciatively.$Perhaps you could feed it\nmore berries on your adventure...$@s{level_up_fanfare}The {{greedentName}} wants to join your party!" + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000000..6dd54d302abe --- /dev/null +++ b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -0,0 +1,26 @@ +{ + "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!", + "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?", + "option": { + "1": { + "label": "Accept the Deal", + "tooltip": "(-) Lose {{strongestPokemon}}\n(+) Gain a @[TOOLTIP_TITLE]{Shiny Charm}\n(+) Gain {{price, money}}", + "selected": "Wonderful!@d{32} Come along, {{strongestPokemon}}!$It's time to show you off to everyone at the yacht club!$They'll be so jealous!" + }, + "2": { + "label": "Extort the Kid", + "tooltip": "(+) {{option2PrimaryName}} uses {{moveOrAbility}}\n(+) Gain {{price, money}}", + "tooltip_disabled": "Your Pokémon need to have certain moves or abilities to choose this", + "selected": "My word, we're being robbed, {{liepardName}}!$You'll be hearing from my lawyers for this!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/berries-abound-dialogue.json b/src/locales/en/mystery-encounters/berries-abound-dialogue.json new file mode 100644 index 000000000000..26eae2c6b880 --- /dev/null +++ b/src/locales/en/mystery-encounters/berries-abound-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "There's a huge berry bush\nnear that Pokémon!", + "title": "Berries Abound", + "description": "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but it looks strong. Perhaps a fast Pokémon could grab some berries without getting caught?", + "query": "What will you do?", + "berries": "Berries!", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Hard Battle\n(+) Gain Berries", + "selected": "You approach the\nPokémon without fear." + }, + "2": { + "label": "Race to the Bush", + "tooltip": "(-) {{fastestPokemon}} Uses its Speed\n(+) Gain Berries", + "selected": "Your {{fastestPokemon}} races for the berry bush!$It manages to nab {{numBerries}} before the {{enemyPokemon}} can react!$You quickly retreat with your newfound prize.", + "selected_bad": "Your {{fastestPokemon}} races for the berry bush!$Oh no! The {{enemyPokemon}} was faster and blocked off the approach!", + "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You leave the strong Pokémon\nwith its prize and continue on." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json new file mode 100644 index 000000000000..7df01326aed5 --- /dev/null +++ b/src/locales/en/mystery-encounters/bug-type-superfan-dialogue.json @@ -0,0 +1,38 @@ +{ + "intro": "An unusual trainer with all kinds of Bug paraphernalia blocks your way!", + "intro_dialogue": "Hey, trainer! I'm on a mission to find the rarest Bug Pokémon in existence!$You must love Bug Pokémon too, right?\nEveryone loves Bug Pokémon!", + "title": "The Bug-Type Superfan", + "speaker": "Bug-Type Superfan", + "description": "The trainer prattles, not even waiting for a response...\n\nIt seems the only way to get out of this situation is by catching the trainer's attention!", + "query": "What will you do?", + "option": { + "1": { + "label": "Offer to Battle", + "tooltip": "(-) Challenging Battle\n(+) Teach a Pokémon a Bug Type Move", + "selected": "A challenge, eh?\nMy bugs are more than ready for you!" + }, + "2": { + "label": "Show Your Bug Types", + "tooltip": "(+) Receive a Gift Item", + "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!" + }, + "3": { + "label": "Gift a Bug Item", + "tooltip": "(-) Give the trainer a {{requiredBugItems}}\n(+) Receive a Gift Item", + "disabled_tooltip": "You need to have a {{requiredBugItems}} to select this.", + "select_prompt": "Select an item to give.", + "invalid_selection": "Pokémon doesn't have that kind of item.", + "selected": "You hand the trainer a {{selectedItem}}.", + "selected_dialogue": "Whoa! A {{selectedItem}}, for me?\nYou're not so bad, kid!$As a token of my appreciation,\nI want you to have this special gift!$It's been passed all through my family, and now I want you to have it!" + } + }, + "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!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/clowning-around-dialogue.json b/src/locales/en/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 000000000000..177812408387 --- /dev/null +++ b/src/locales/en/mystery-encounters/clowning-around-dialogue.json @@ -0,0 +1,34 @@ +{ + "intro": "It's...@d{64} a clown?", + "speaker": "Clown", + "intro_dialogue": "Bumbling buffoon, brace for a brilliant battle!\nYou'll be beaten by this brawling busker!", + "title": "Clowning Around", + "description": "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe {{blacephalonName}} is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Clown", + "tooltip": "(-) Strange Battle\n(?) Affects Pokémon Abilities", + "selected": "Your pitiful Pokémon are poised for a pathetic performance!", + "apply_ability_dialogue": "A sensational showcase!\nYour savvy suits a sensational skill as spoils!", + "apply_ability_message": "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!", + "ability_prompt": "Would you like to permanently teach a Pokémon the {{ability}} ability?", + "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!" + }, + "2": { + "label": "Remain Unprovoked", + "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Items", + "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + "selected_2": "The clown's {{blacephalonName}} uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!", + "selected_3": "Flustered fool, fall for my flawless deception!" + }, + "3": { + "label": "Return the Insults", + "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Types", + "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + "selected_2": "The clown's {{blacephalonName}} uses a strange move!\nAll of your team's types were randomly swapped!", + "selected_3": "Flustered fool, fall for my flawless deception!" + } + }, + "outro": "The clown and his cohorts\ndisappear in a puff of smoke." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json new file mode 100644 index 000000000000..8e2883ecb161 --- /dev/null +++ b/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "An {{oricorioName}} dances sadly alone, without a partner.", + "title": "Dancing Lessons", + "description": "The {{oricorioName}} doesn't seem aggressive, if anything it seems sad.\n\nMaybe it just wants someone to dance with...", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Tough Battle\n(+) Gain a Baton", + "selected": "The {{oricorioName}} is distraught and moves to defend itself!", + "boss_enraged": "The {{oricorioName}}'s fear boosted its stats!" + }, + "2": { + "label": "Learn Its Dance", + "tooltip": "(+) Teach a Pokémon Revelation Dance", + "selected": "You watch the {{oricorioName}} closely as it performs its dance...$@s{level_up_fanfare}Your {{selectedPokemon}} learned from the {{oricorioName}}!" + }, + "3": { + "label": "Show It a Dance", + "tooltip": "(-) Teach the {{oricorioName}} a Dance Move\n(+) The {{oricorioName}} Will Like You", + "disabled_tooltip": "Your Pokémon need to know a Dance move for this.", + "select_prompt": "Select a Dance type move to use.", + "selected": "The {{oricorioName}} watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!$It loves the display!$@s{level_up_fanfare}The {{oricorioName}} wants to join your party!" + } + }, + "invalid_selection": "This Pokémon doesn't know a Dance move" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/dark-deal-dialogue.json b/src/locales/en/mystery-encounters/dark-deal-dialogue.json new file mode 100644 index 000000000000..3086ebb0f9b7 --- /dev/null +++ b/src/locales/en/mystery-encounters/dark-deal-dialogue.json @@ -0,0 +1,24 @@ + + +{ + "intro": "A strange man in a tattered coat\nstands in your way...", + "speaker": "Shady Guy", + "intro_dialogue": "Hey, you!$I've been working on a new device\nto bring out a Pokémon's latent power!$It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.$Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.", + "title": "Dark Deal", + "description": "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept", + "tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", + "selected_dialogue": "Let's see, that {{pokeName}} will do nicely!$Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...", + "selected_message": "The man hands you 5 Rogue Balls.${{pokeName}} hops into the strange machine...$Flashing lights and weird noises\nstart coming from the machine!$...@d{96} Something emerges\nfrom the device, raging wildly!" + }, + "2": { + "label": "Refuse", + "tooltip": "(-) No Rewards", + "selected": "Not gonna help a poor fellow out?\nPah!" + } + }, + "outro": "After the harrowing encounter,\nyou collect yourself and depart." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/delibirdy-dialogue.json b/src/locales/en/mystery-encounters/delibirdy-dialogue.json new file mode 100644 index 000000000000..ca1fefa3a39c --- /dev/null +++ b/src/locales/en/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "A pack of {{delibirdName}} have appeared!", + "title": "Delibir-dy", + "description": "The {{delibirdName}}s are looking at you expectantly, as if they want something. Perhaps giving them an item or some money would satisfy them?", + "query": "What will you give them?", + "invalid_selection": "Pokémon doesn't have that kind of item.", + "option": { + "1": { + "label": "Give Money", + "tooltip": "(-) Give the {{delibirdName}}s {{money, money}}\n(+) Receive a Gift Item", + "selected": "You toss the money to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + }, + "2": { + "label": "Give Food", + "tooltip": "(-) Give the {{delibirdName}}s a Berry or Reviver Seed\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + }, + "3": { + "label": "Give an Item", + "tooltip": "(-) Give the {{delibirdName}}s a Held Item\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the {{delibirdName}}s,\nwho chatter amongst themselves excitedly.$They turn back to you and happily give you a present!" + } + }, + "outro": "The {{delibirdName}} pack happily waddles off into the distance.$What a curious little exchange!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/department-store-sale-dialogue.json b/src/locales/en/mystery-encounters/department-store-sale-dialogue.json new file mode 100644 index 000000000000..d651f32665a1 --- /dev/null +++ b/src/locales/en/mystery-encounters/department-store-sale-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "It's a lady with a ton of shopping bags.", + "speaker": "Shopper", + "intro_dialogue": "Hello! Are you here for\nthe amazing sales too?$There's a special coupon that you can\nredeem for a free item during the sale!$I have an extra one. Here you go!", + "title": "Department Store Sale", + "description": "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!", + "query": "Which counter will you go to?", + "option": { + "1": { + "label": "TM Counter", + "tooltip": "(+) TM Shop" + }, + "2": { + "label": "Vitamin Counter", + "tooltip": "(+) Vitamin Shop" + }, + "3": { + "label": "Battle Item Counter", + "tooltip": "(+) X Item Shop" + }, + "4": { + "label": "Pokéball Counter", + "tooltip": "(+) Pokéball Shop" + } + }, + "outro": "What a deal! You should shop there more often." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/field-trip-dialogue.json b/src/locales/en/mystery-encounters/field-trip-dialogue.json new file mode 100644 index 000000000000..61900d56cd7c --- /dev/null +++ b/src/locales/en/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "It's a teacher and some school children!", + "speaker": "Teacher", + "intro_dialogue": "Hello, there! Would you be able to\nspare a minute for my students?$I'm teaching them about Pokémon moves\nand would love to show them a demonstration.$Would you mind showing us one of\nthe moves your Pokémon can use?", + "title": "Field Trip", + "description": "A teacher is requesting a move demonstration from a Pokémon. Depending on the move you choose, she might have something useful for you in exchange.", + "query": "Which move category will you show off?", + "option": { + "1": { + "label": "A Physical Move", + "tooltip": "(+) Physical Item Rewards" + }, + "2": { + "label": "A Special Move", + "tooltip": "(+) Special Item Rewards" + }, + "3": { + "label": "A Status Move", + "tooltip": "(+) Status Item Rewards" + }, + "selected": "{{pokeName}} shows off an awesome display of {{move}}!" + }, + "second_option_prompt": "Choose a move for your Pokémon to use.", + "incorrect": "...$That isn't a {{moveCategory}} move!\nI'm sorry, but I can't give you anything.$Come along children, we'll\nfind a better demonstration elsewhere.", + "incorrect_exp": "Looks like you learned a valuable lesson?$Your Pokémon also gained some experience.", + "correct": "Thank you so much for your kindness!\nI hope these items might be of use to you!", + "correct_exp": "{{pokeName}} also gained some valuable experience!", + "status": "Status", + "physical": "Physical", + "special": "Special" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json new file mode 100644 index 000000000000..a1644d89a3fb --- /dev/null +++ b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "You encounter a blistering storm of smoke and ash!", + "title": "Fiery Fallout", + "description": "The whirling ash and embers have cut visibility to nearly zero. It seems like there might be some... source that is causing these conditions. But what could be behind a phenomenon of this magnitude?", + "query": "What will you do?", + "option": { + "1": { + "label": "Find the Source", + "tooltip": "(?) Discover the source\n(-) Hard Battle", + "selected": "You push through the storm, and find two {{volcaronaName}}s in the middle of a mating dance!$They don't take kindly to the interruption and attack!" + }, + "2": { + "label": "Hunker Down", + "tooltip": "(-) Suffer the effects of the weather", + "selected": "The weather effects cause significant\nharm as you struggle to find shelter!$Your party takes 20% Max HP damage!", + "target_burned": "Your {{burnedPokemon}} also became burned!" + }, + "3": { + "label": "Your Fire Types Help", + "tooltip": "(+) End the conditions\n(+) Gain a Charcoal", + "disabled_tooltip": "You need at least 2 Fire Type Pokémon to choose this", + "selected": "Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two {{volcaronaName}}s are in the middle of a mating dance!$Thankfully, your Pokémon are able to calm them,\nand they depart without issue." + } + }, + "found_charcoal": "After the weather clears,\nyour {{leadPokemon}} spots something on the ground.$@s{item_fanfare}{{leadPokemon}} gained a Charcoal!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json new file mode 100644 index 000000000000..3eb6cb87c165 --- /dev/null +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "Something shiny is sparkling\non the ground near that Pokémon!", + "title": "Fight or Flight", + "description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but it looks strong. Perhaps you could steal the item, if you have the right Pokémon for the job.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Hard Battle\n(+) New Item", + "selected": "You approach the\nPokémon without fear.", + "stat_boost": "The {{enemyPokemon}}'s latent strength boosted one of its stats!" + }, + "2": { + "label": "Steal the Item", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "tooltip": "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", + "selected": ".@d{32}.@d{32}.@d{32}$Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!$You nabbed the item!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You leave the strong Pokémon\nwith its prize and continue on." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/fun-and-games-dialogue.json b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 000000000000..f5d7d6e8ff8d --- /dev/null +++ b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Step right up, folks! Try your luck\non the brand new {{wobbuffetName}} Whack-o-matic!", + "speaker": "Showman", + "title": "Fun And Games!", + "description": "You've encountered a traveling show with a prize game! You will have @[TOOLTIP_TITLE]{3 turns} to bring the {{wobbuffetName}} as close to @[TOOLTIP_TITLE]{1 HP} as possible @[TOOLTIP_TITLE]{without KOing it} so it can wind up a huge Counter on the bell-ringing machine.\nBut be careful! If you KO the {{wobbuffetName}}, you'll have to pay for the cost of reviving it!", + "query": "Would you like to play?", + "option": { + "1": { + "label": "Play the Game", + "tooltip": "(-) Pay {{option1Money, money}}\n(+) Play {{wobbuffetName}} Whack-o-matic", + "selected": "Time to test your luck!" + }, + "2": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + }, + "ko": "Oh no! The {{wobbuffetName}} fainted!$You lose the game and\nhave to pay for the revive cost...", + "charging_continue": "The Wubboffet keeps charging its counter-swing!", + "turn_remaining_3": "Three turns remaining!", + "turn_remaining_2": "Two turns remaining!", + "turn_remaining_1": "One turn remaining!", + "end_game": "Time's up!$The {{wobbuffetName}} winds up to counter-swing and@d{16}.@d{16}.@d{16}.", + "best_result": "The {{wobbuffetName}} smacks the button so hard\nthe bell breaks off the top!$You win the grand prize!", + "great_result": "The {{wobbuffetName}} smacks the button, nearly hitting the bell!$So close!\nYou earn the second tier prize!", + "good_result": "The {{wobbuffetName}} hits the button hard enough to go midway up the scale!$You earn the third tier prize!", + "bad_result": "The {{wobbuffetName}} barely taps the button and nothing happens...$Oh no!\nYou don't win anything!", + "outro": "That was a fun little game!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/global-trade-system-dialogue.json b/src/locales/en/mystery-encounters/global-trade-system-dialogue.json new file mode 100644 index 000000000000..1cc420355b75 --- /dev/null +++ b/src/locales/en/mystery-encounters/global-trade-system-dialogue.json @@ -0,0 +1,32 @@ +{ + "intro": "It's an interface for the Global Trade System!", + "title": "The GTS", + "description": "Ah, the GTS! A technological wonder, you can connect with anyone else around the globe to trade Pokémon with them! Will fortune smile upon your trade today?", + "query": "What will you do?", + "option": { + "1": { + "label": "Check Trade Offers", + "tooltip": "(+) Select a trade offer for one of your Pokémon", + "trade_options_prompt": "Select a Pokémon to receive through trade." + }, + "2": { + "label": "Wonder Trade", + "tooltip": "(+) Send one of your Pokémon to the GTS and get a random Pokémon in return" + }, + "3": { + "label": "Trade an Item", + "trade_options_prompt": "Select an item to send.", + "invalid_selection": "This Pokémon doesn't have legal items to trade.", + "tooltip": "(+) Send one of your Items to the GTS and get a random new Item" + }, + "4": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "No time to trade today!\nYou continue on." + } + }, + "pokemon_trade_selected": "{{tradedPokemon}} will be sent to {{tradeTrainerName}}.", + "pokemon_trade_goodbye": "Goodbye, {{tradedPokemon}}!", + "item_trade_selected": "{{chosenItem}} will be sent to {{tradeTrainerName}}.$.@d{64}.@d{64}.@d{64}\n@s{level_up_fanfare}Trade complete!$You received a {{itemName}} from {{tradeTrainerName}}!", + "trade_received": "@s{evolution_fanfare}{{tradeTrainerName}} sent over {{received}}!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 000000000000..41709c66799e --- /dev/null +++ b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json @@ -0,0 +1,28 @@ +{ + "intro": "Wandering aimlessly through the sea, you've effectively gotten nowhere.", + "title": "Lost at Sea", + "description": "The sea is turbulent in this area, and you're running out of energy.\nThis is bad. Is there a way out of the situation?", + "query": "What will you do?", + "option": { + "1": { + "label": "{{option1PrimaryName}} Might Help", + "label_disabled": "Can't {{option1RequiredMove}}", + "tooltip": "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", + "tooltip_disabled": "You have no Pokémon to {{option1RequiredMove}} on", + "selected": "{{option1PrimaryName}} swims ahead, guiding you back on track.${{option1PrimaryName}} seems to also have gotten stronger in this time of need!" + }, + "2": { + "label": "{{option2PrimaryName}} Might Help", + "label_disabled": "Can't {{option2RequiredMove}}", + "tooltip": "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", + "tooltip_disabled": "You have no Pokémon to {{option2RequiredMove}} with", + "selected": "{{option2PrimaryName}} flies ahead of your boat, guiding you back on track.${{option2PrimaryName}} seems to also have gotten stronger in this time of need!" + }, + "3": { + "label": "Wander Aimlessly", + "tooltip": "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", + "selected": "You float about in the boat, steering without direction until you finally spot a landmark you remember.$You and your Pokémon are fatigued from the whole ordeal." + } + }, + "outro": "You are back on track." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json new file mode 100644 index 000000000000..01f4e6092ebb --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "Mysterious challengers have appeared!", + "title": "Mysterious Challengers", + "description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?", + "query": "Who will you battle?", + "option": { + "1": { + "label": "A Clever, Mindful Foe", + "tooltip": "(-) Standard Battle\n(+) Move Item Rewards" + }, + "2": { + "label": "A Strong Foe", + "tooltip": "(-) Hard Battle\n(+) Good Rewards" + }, + "3": { + "label": "The Mightiest Foe", + "tooltip": "(-) Brutal Battle\n(+) Great Rewards" + }, + "selected": "The trainer steps forward..." + }, + "outro": "The mysterious challenger was defeated!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json new file mode 100644 index 000000000000..1de7a5992eda --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "You found...@d{32} a chest?", + "title": "The Mysterious Chest", + "description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", + "query": "Will you open it?", + "option": { + "1": { + "label": "Open It", + "tooltip": "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}", + "selected": "You open the chest to find...", + "normal": "Just some normal tools and items.", + "good": "Some pretty nice tools and items.", + "great": "A couple great tools and items!", + "amazing": "Whoa! An amazing item!", + "bad": "Oh no!@d{32}\nThe chest was actually a {{gimmighoulName}} in disguise!$Your {{pokeName}} jumps in front of you\nbut is KOed in the process!" + }, + "2": { + "label": "Too Risky, Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/part-timer-dialogue.json b/src/locales/en/mystery-encounters/part-timer-dialogue.json new file mode 100644 index 000000000000..614f1818e3f8 --- /dev/null +++ b/src/locales/en/mystery-encounters/part-timer-dialogue.json @@ -0,0 +1,31 @@ +{ + "intro": "A busy worker flags you down.", + "speaker": "Worker", + "intro_dialogue": "You look like someone with lots of capable Pokémon!$We can pay you if you're able to help us with some part-time work!", + "title": "Part-Timer", + "description": "Looks like there are plenty of tasks that need to be done. Depending how well-suited your Pokémon is to a task, they might earn more or less money.", + "query": "Which job will you choose?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "Make Deliveries", + "tooltip": "(-) Your Pokémon Uses its Speed\n(+) Earn @[MONEY]{Money}", + "selected": "Your {{selectedPokemon}} works a shift delivering orders to customers." + }, + "2": { + "label": "Warehouse Work", + "tooltip": "(-) Your Pokémon Uses its Strength and Endurance\n(+) Earn @[MONEY]{Money}", + "selected": "Your {{selectedPokemon}} works a shift moving items around the warehouse." + }, + "3": { + "label": "Sales Assistant", + "tooltip": "(-) Your {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Earn @[MONEY]{Money}", + "disabled_tooltip": "Your Pokémon need to know certain moves for this job", + "selected": "Your {{option3PrimaryName}} spends the day using {{option3PrimaryMove}} to attract customers to the business!" + } + }, + "job_complete_good": "Thanks for the assistance!\nYour {{selectedPokemon}} was incredibly helpful!$Here's your check for the day.", + "job_complete_bad": "Your {{selectedPokemon}} helped us out a bit!$Here's your check for the day.", + "pokemon_tired": "Your {{selectedPokemon}} is worn out!\nThe PP of all its moves was reduced to 2!", + "outro": "Come back and help out again sometime!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/safari-zone-dialogue.json b/src/locales/en/mystery-encounters/safari-zone-dialogue.json new file mode 100644 index 000000000000..8869f2055e5f --- /dev/null +++ b/src/locales/en/mystery-encounters/safari-zone-dialogue.json @@ -0,0 +1,46 @@ +{ + "intro": "It's a safari zone!", + "title": "The Safari Zone", + "description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\n\nBeware, though. These Pokémon may flee before you're able to catch them!", + "query": "Would you like to enter?", + "option": { + "1": { + "label": "Enter", + "tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "selected": "Time to test your luck!" + }, + "2": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + }, + "safari": { + "1": { + "label": "Throw a Pokéball", + "tooltip": "(+) Throw a Pokéball", + "selected": "You throw a Pokéball!" + }, + "2": { + "label": "Throw Bait", + "tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", + "selected": "You throw some bait!" + }, + "3": { + "label": "Throw Mud", + "tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", + "selected": "You throw some mud!" + }, + "4": { + "label": "Flee", + "tooltip": "(?) Flee from this Pokémon" + }, + "watching": "{{pokemonName}} is watching carefully!", + "eating": "{{pokemonName}} is eating!", + "busy_eating": "{{pokemonName}} is busy eating!", + "angry": "{{pokemonName}} is angry!", + "beside_itself_angry": "{{pokemonName}} is beside itself with anger!", + "remaining_count": "{{remainingCount}} Pokémon remaining!" + }, + "outro": "That was a fun little excursion!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json new file mode 100644 index 000000000000..d0003de07f1d --- /dev/null +++ b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "A man in a dark coat approaches you.", + "speaker": "Shady Salesman", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$I've got the goods if you've got the money.$Make sure your Pokémon can handle it though.", + "title": "The Vitamin Dealer", + "description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", + "query": "Which deal will you choose?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "The Cheap Deal", + "tooltip": "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins" + }, + "2": { + "label": "The Pricey Deal", + "tooltip": "(-) Pay {{option2Money, money}}\n(+) Chosen Pokémon Gains 2 Random Vitamins" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "Heh, wouldn't have figured you for a coward." + }, + "selected": "The man hands you two bottles and quickly disappears.${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!" + }, + "cheap_side_effects": "But the medicine had some side effects!$Your {{selectedPokemon}} takes some damage,\nand its Nature is changed to {{newNature}}!", + "no_bad_effects": "Looks like there were no side-effects from the medicine!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json new file mode 100644 index 000000000000..cd3bb7465c48 --- /dev/null +++ b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "As you walk down a narrow pathway, you see a towering silhouette blocking your path.$You get closer to see a {{snorlaxName}} sleeping peacefully.\nIt seems like there's no way around it.", + "title": "Slumbering {{snorlaxName}}", + "description": "You could attack it to try and get it to move, or simply wait for it to wake up. Who knows how long that could take, though...", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle It", + "tooltip": "(-) Fight Sleeping {{snorlaxName}}\n(+) Special Reward", + "selected": "You approach the\nPokémon without fear." + }, + "2": { + "label": "Wait for It to Move", + "tooltip": "(-) Wait a Long Time\n(+) Recover Party", + "selected": ".@d{32}.@d{32}.@d{32}$You wait for a time, but the {{snorlaxName}}'s yawns make your party sleepy...", + "rest_result": "When you all awaken, the {{snorlaxName}} is no where to be found -\nbut your Pokémon are all healed!" + }, + "3": { + "label": "Steal Its Item", + "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Special Reward", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}}!$@s{item_fanfare}It steals Leftovers off the sleeping\n{{snorlaxName}} and you make out like bandits!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json new file mode 100644 index 000000000000..c295867f5218 --- /dev/null +++ b/src/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "It's a strange machine, whirring noisily...", + "title": "Teleportating Hijinks", + "description": "The machine has a sign on it that reads:\n \"To use, insert money then step into the capsule.\"\n\nPerhaps it can transport you somewhere...", + "query": "What will you do?", + "option": { + "1": { + "label": "Put Money In", + "tooltip": "(-) Pay {{price, money}}\n(?) Teleport to New Biome", + "selected": "You insert some money, and the capsule opens.\nYou step inside..." + }, + "2": { + "label": "A Pokémon Helps", + "tooltip": "(-) {{option2PrimaryName}} Helps\n(+) {{option2PrimaryName}} gains EXP\n(?) Teleport to New Biome", + "disabled_tooltip": "You need a Steel or Electric Type Pokémon to choose this", + "selected": "{{option2PrimaryName}}'s Type allows it to bypass the machine's paywall!$The capsule opens, and you step inside..." + }, + "3": { + "label": "Inspect the Machine", + "tooltip": "(-) Pokémon Battle", + "selected": "You are drawn in by the blinking lights\nand strange noises coming from the machine...$You don't even notice as a wild\nPokémon sneaks up and ambushes you!" + } + }, + "transport": "The machine shakes violently,\nmaking all sorts of strange noises!$Just as soon as it had started, it quiets once more.", + "attacked": "You step out into a completely new area, startling a wild Pokémon!$The wild Pokémon attacks!", + "boss_enraged": "The opposing {{enemyPokemon}} has become enraged!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json new file mode 100644 index 000000000000..7e8091bbfff0 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -0,0 +1,23 @@ +{ + "intro": "A chipper elderly man approaches you.", + "speaker": "Gentleman", + "intro_dialogue": "Hello there! Have I got a deal just for YOU!", + "title": "The Pokémon Salesman", + "description": "\"This {{purchasePokemon}} is extremely unique and carries an ability not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", + "description_shiny": "\"This {{purchasePokemon}} is extremely unique and has a pigment not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept", + "tooltip": "(-) Pay {{price, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability", + "tooltip_shiny": "(-) Pay {{price, money}}\n(+) Gain a shiny {{purchasePokemon}}", + "selected_message": "You paid an outrageous sum and bought the {{purchasePokemon}}.", + "selected_dialogue": "Excellent choice!$I can see you've a keen eye for business.$Oh, yeah...@d{64} Returns not accepted, got that?" + }, + "2": { + "label": "Refuse", + "tooltip": "(-) No Rewards", + "selected": "No?@d{32} You say no?$I'm only doing this as a favor to you!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json new file mode 100644 index 000000000000..b5403616c9b5 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "It's a massive {{shuckleName}} and what appears\nto be a large stash of... juice?", + "title": "The Strong Stuff", + "description": "The {{shuckleName}} that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe {{shuckleName}} extends its feelers in your direction. It seems like it wants to do something...", + "query": "What will you do?", + "option": { + "1": { + "label": "Approach the {{shuckleName}}", + "tooltip": "(?) Something awful or amazing might happen", + "selected": "You black out.", + "selected_2": "@f{150}When you awaken, the {{shuckleName}} is gone\nand juice stash completely drained.${{highBstPokemon1}} and {{highBstPokemon2}}\nfeel a terrible lethargy come over them!$Their base stats were reduced by {{reductionValue}}!$Your remaining Pokémon feel an incredible vigor, though!\nTheir base stats are increased by {{increaseValue}}!" + }, + "2": { + "label": "Battle the {{shuckleName}}", + "tooltip": "(-) Hard Battle\n(+) Special Rewards", + "selected": "Enraged, the {{shuckleName}} drinks some of its juice and attacks!", + "stat_boost": "The {{shuckleName}}'s juice boosts its stats!" + } + }, + "outro": "What a bizarre turn of events." +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json new file mode 100644 index 000000000000..37807a91667e --- /dev/null +++ b/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "It's a family standing outside their house!", + "speaker": "The Winstrates", + "intro_dialogue": "We're the Winstrates!$What do you say to taking on our family in a series of Pokémon battles?", + "title": "The Winstrate Challenge", + "description": "The Winstrates are a family of 5 trainers, and they want to battle! If you beat all of them back-to-back, they'll give you a grand prize. But can you handle the heat?", + "query": "What will you do?", + "option": { + "1": { + "label": "Accept the Challenge", + "tooltip": "(-) Brutal Battle\n(+) Special Item Reward", + "selected": "Let the challenge begin!" + }, + "2": { + "label": "Refuse the Challenge", + "tooltip": "(+) Full Heal Party\n(+) Gain a Rarer Candy", + "selected": "That's too bad. Say, your team looks worn out, why don't you stay awhile and rest?" + } + }, + "victory": "Congratulations on beating our challenge!$First off, we'd like you to have this Voucher.", + "victory_2": "Also, our family uses this Macho Brace to strengthen\nour Pokémon more effectively during training.$You may not need it considering that you beat the whole lot of us, but we hope you'll accept it anyway!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/training-session-dialogue.json b/src/locales/en/mystery-encounters/training-session-dialogue.json new file mode 100644 index 000000000000..f018018fe4e2 --- /dev/null +++ b/src/locales/en/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,33 @@ +{ + "intro": "You've come across some\ntraining tools and supplies.", + "title": "Training Session", + "description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", + "query": "How should you train?", + "invalid_selection": "Pokémon must be healthy enough.", + "option": { + "1": { + "label": "Light Training", + "tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its {{stat1}} and {{stat2}} IVs were improved!" + }, + "2": { + "label": "Moderate Training", + "tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature", + "select_prompt": "Select a new nature\nto train your Pokémon in.", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its nature was changed to {{nature}}!" + }, + "3": { + "label": "Heavy Training", + "tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability", + "select_prompt": "Select a new ability\nto train your Pokémon in.", + "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its ability was changed to {{ability}}!" + }, + "4": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You've no time for training.\nTime to move on." + }, + "selected": "{{selectedPokemon}} moves across\nthe clearing to face you..." + }, + "outro": "That was a successful training session!" +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json new file mode 100644 index 000000000000..fe2cb54f5b13 --- /dev/null +++ b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.json @@ -0,0 +1,19 @@ +{ + "intro": "It's a massive pile of garbage!\nWhere did this come from?", + "title": "Trash to Treasure", + "description": "The garbage heap looms over you, and you can spot some items of value buried amidst the refuse. Are you sure you want to get covered in filth to get them, though?", + "query": "What will you do?", + "option": { + "1": { + "label": "Dig for Valuables", + "tooltip": "(-) Items in Shops Cost 3x\n(+) Gain Amazing Items", + "selected": "You wade through the garbage pile, becoming mired in filth.$There's no way any respectable shopkeeper would\nsell you items at the normal rate in your grimy state!$You'll have to pay extra for items now.$However, you found some incredible items in the garbage!" + }, + "2": { + "label": "Investigate Further", + "tooltip": "(?) Find the Source of the Garbage", + "selected": "You wander around the heap, searching for any indication as to how this might have appeared here...", + "selected_2": "Suddenly, the garbage shifts! It wasn't just garbage, it was a Pokémon!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 000000000000..e6f5b3d3fcd5 --- /dev/null +++ b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json @@ -0,0 +1,26 @@ +{ + "intro": "That isn't just an ordinary Pokémon!", + "title": "Uncommon Breed", + "description": "That {{enemyPokemon}} looks special compared to others of its kind. @[TOOLTIP_TITLE]{Perhaps it knows a special move?} You could battle and catch it outright, but there might also be a way to befriend it.", + "query": "What will you do?", + "option": { + "1": { + "label": "Battle the Pokémon", + "tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe", + "selected": "You approach the\n{{enemyPokemon}} without fear.", + "stat_boost": "The {{enemyPokemon}}'s heightened abilities boost its stats!" + }, + "2": { + "label": "Give It Food", + "disabled_tooltip": "You need 4 berry items to choose this", + "tooltip": "(-) Give 4 Berries\n(+) The {{enemyPokemon}} Likes You", + "selected": "You toss the berries at the {{enemyPokemon}}!$It eats them happily!$The {{enemyPokemon}} wants to join your party!" + }, + "3": { + "label": "Befriend It", + "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", + "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) The {{enemyPokemon}} Likes You", + "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}} to charm the {{enemyPokemon}}!$The {{enemyPokemon}} wants to join your party!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/mystery-encounters/weird-dream-dialogue.json b/src/locales/en/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 000000000000..44acde84002c --- /dev/null +++ b/src/locales/en/mystery-encounters/weird-dream-dialogue.json @@ -0,0 +1,22 @@ +{ + "intro": "A shadowy woman blocks your path.\nSomething about her is unsettling...", + "speaker": "Woman", + "intro_dialogue": "I have seen your futures, your pasts...$Child, do you see them too?", + "title": "???", + "description": "The woman's words echo in your head. It wasn't just a singular voice, but a vast multitude, from all timelines and realities. You begin to feel dizzy, the question lingering on your mind...\n\n@[TOOLTIP_TITLE]{\"I have seen your futures, your pasts... Child, do you see them too?\"}", + "query": "What will you do?", + "option": { + "1": { + "label": "\"I See Them\"", + "tooltip": "@[SUMMARY_GREEN]{(?) Affects your Pokémon}", + "selected": "Her hand reaches out to touch you,\nand everything goes black.$Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.$Everything that has made you,\neverything you will become...@d{64}", + "cutscene": "You see your Pokémon,@d{32} converging from\nevery reality to become something new...@d{64}", + "dream_complete": "When you awaken, the woman - was it a woman or a ghost? - is gone...$.@d{32}.@d{32}.@d{32}$Your Pokémon team has changed...\nOr is it the same team you've always had?" + }, + "2": { + "label": "Quickly Leave", + "tooltip": "(-) Affects your Pokémon", + "selected": "You tear your mind from a numbing grip, and hastily depart.$When you finally stop to collect yourself, you check the Pokémon in your team.$For some reason, all of their levels have decreased!" + } + } +} \ No newline at end of file diff --git a/src/locales/en/party-ui-handler.json b/src/locales/en/party-ui-handler.json index 9c2b3f30e5ef..8e6e8046c7e1 100644 --- a/src/locales/en/party-ui-handler.json +++ b/src/locales/en/party-ui-handler.json @@ -13,8 +13,10 @@ "ALL": "All", "PASS_BATON": "Pass Baton", "UNPAUSE_EVOLUTION": "Unpause Evolution", + "PAUSE_EVOLUTION": "Pause Evolution", "REVIVE": "Revive", "RENAME": "Rename", + "SELECT": "Select", "choosePokemon": "Choose a Pokémon.", "doWhatWithThisPokemon": "Do what with this Pokémon?", "noEnergy": "{{pokemonName}} has no energy\nleft to battle!", @@ -23,6 +25,7 @@ "tooManyItems": "{{pokemonName}} has too many\nof this item!", "anyEffect": "It won't have any effect.", "unpausedEvolutions": "Evolutions have been unpaused for {{pokemonName}}.", + "pausedEvolutions": "Evolutions have been paused for {{pokemonName}}.", "unspliceConfirmation": "Do you really want to unsplice {{fusionName}}\nfrom {{pokemonName}}? {{fusionName}} will be lost.", "wasReverted": "{{fusionName}} was reverted to {{pokemonName}}.", "releaseConfirmation": "Do you really want to release {{pokemonName}}?", diff --git a/src/locales/en/trainer-names.json b/src/locales/en/trainer-names.json index 50a2ce18f347..467ed03e0446 100644 --- a/src/locales/en/trainer-names.json +++ b/src/locales/en/trainer-names.json @@ -160,5 +160,17 @@ "alder_iris_double": "Alder & Iris", "iris_alder_double": "Iris & Alder", "marnie_piers_double": "Marnie & Piers", - "piers_marnie_double": "Piers & Marnie" + "piers_marnie_double": "Piers & Marnie", + + "buck": "Buck", + "cheryl": "Cheryl", + "marley": "Marley", + "mira": "Mira", + "riley": "Riley", + "victor": "Victor", + "victoria": "Victoria", + "vivi": "Vivi", + "vicky": "Vicky", + "vito": "Vito", + "bug_type_superfan": "Bug-Type Superfan" } diff --git a/src/locales/en/trainer-titles.json b/src/locales/en/trainer-titles.json index b9c919022be9..7ef715d115f2 100644 --- a/src/locales/en/trainer-titles.json +++ b/src/locales/en/trainer-titles.json @@ -34,5 +34,7 @@ "flare_admin_female": "Team Flare Admin", "aether_admin": "Aether Foundation Admin", "skull_admin": "Team Skull Admin", - "macro_admin": "Macro Cosmos" + "macro_admin": "Macro Cosmos", + + "the_winstrates": "The Winstrates'" } diff --git a/src/locales/es/menu.json b/src/locales/es/menu.json index ef1ae93dd823..a35025819faf 100644 --- a/src/locales/es/menu.json +++ b/src/locales/es/menu.json @@ -51,7 +51,7 @@ "renamePokemon": "Renombrar Pokémon.", "rename": "Renombrar", "nickname": "Apodo", - "errorServerDown": "¡Ups! Ha habido un problema al contactar con el servidor.\n\nPuedes mantener esta ventana abierta, el juego se reconectará automáticamente.", + "errorServerDown": "¡Ups! Ha habido un problema al contactar con el servidor.\n\nPuedes mantener esta ventana abierta,\nel juego se reconectará automáticamente.", "noSaves": "No tienes ninguna partida guardada registrada!", "tooManySaves": "¡Tienes demasiadas partidas guardadas registradas!" } diff --git a/src/locales/es/mystery-encounter-messages.json b/src/locales/es/mystery-encounter-messages.json index 2fcd3e893fb1..8d7d98bdd0f4 100644 --- a/src/locales/es/mystery-encounter-messages.json +++ b/src/locales/es/mystery-encounter-messages.json @@ -3,5 +3,5 @@ "receive_money": "¡Recibiste {{amount, number}}₽!", "affects_pokedex": "Afecta los datos de la Pokédex", "cancel_option": "Volver a la selección de opciones", - "view_party_button": "View Party" + "view_party_button": "Ver equipo" } diff --git a/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json b/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json index 5341755531d8..ec781abcd5a2 100644 --- a/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json +++ b/src/locales/es/mystery-encounters/a-trainers-test-dialogue.json @@ -31,11 +31,11 @@ "option": { "1": { "label": "Aceptar el Desafío", - "tooltip": "(-) Batalla Difícil\n(+) Obtén un @[TOOLTIP_TITLE]{Very Rare Egg}" + "tooltip": "(-) Batalla Ardua\n(+) Obtén un @[TOOLTIP_TITLE]{Huevo muy raro}" }, "2": { "label": "Rechazar el Desafío", - "tooltip": "(+) Equipo Curado\n(+) Obtén un @[TOOLTIP_TITLE]{Egg}" + "tooltip": "(+) Equipo Curado\n(+) Obtén un @[TOOLTIP_TITLE]{Huevo}" } }, "eggTypes": { diff --git a/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json index c288c3d7bdca..f3700c1d60b3 100644 --- a/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json +++ b/src/locales/es/mystery-encounters/absolute-avarice-dialogue.json @@ -6,7 +6,7 @@ "option": { "1": { "label": "Combatir", - "tooltip": "(-) Batalla Difícil\n(+) Recompensas de su Alijo de Bayas", + "tooltip": "(-) Batalla Ardua\n(+) Recompensas de su Alijo de Bayas", "selected": "El {{greedentName}} llena sus mejillas y se prepara para la batalla!", "boss_enraged": "¡El feroz amor de {{greedentName}} por la comida lo tiene enfurecido!", "food_stash": "¡Parece que el {{greedentName}} estaba protegiendo un enorme alijo de comida!$@s{item_fanfare}¡Cada Pokémon en tu grupo obtiene una {{foodReward}}!" diff --git a/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 36f9a2f5a8d3..07396cbc3b2b 100644 --- a/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/es/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -1,14 +1,14 @@ { "intro": "Te detiene un chico de aspecto rico.", "speaker": "Niño Bien", - "intro_dialogue": "Buenos días a usted.$¡No puedo evitar notar que tu\n{{strongestPokemon}} se ve absolutamente divino!$¡Siempre he querido tener una mascota así!$¡Te pagaría generosamente,\n también te daría este viejo abalorio!", + "intro_dialogue": "Buenos días a usted.$¡No puedo evitar notar que tu\n{{strongestPokemon}} se ve absolutamente divino!$¡Siempre he querido tener un Pokémon así!$¡Te pagaría generosamente,\n también te daría este viejo abalorio!", "title": "Una oferta que no puedes rechazar", - "description": "Te están ofreciendo @[TOOLTIP_TITLE]{Shiny Charm} y {{price, money}} por tu {{strongestPokemon}}!¡Es un trato extremadamente bueno, pero ¿realmente puedes soportar separarte de un miembro tan fuerte de tu equipo?”", + "description": "Te están ofreciendo @[TOOLTIP_TITLE]{Amuleto Iris} y {{price, money}} por tu {{strongestPokemon}}!¡Es un trato extremadamente bueno, pero ¿realmente puedes soportar separarte de un miembro tan fuerte de tu equipo?", "query": "¿Qué harás?", "option": { "1": { "label": "Aceptar el trato", - "tooltip": "(-) Pierdes a {{strongestPokemon}}\n(+) Obtén un @[TOOLTIP_TITLE]{Shiny Charm}\n(+) Obtén {{price, money}}", + "tooltip": "(-) Pierdes a {{strongestPokemon}}\n(+) Obtén un @[TOOLTIP_TITLE]{Amuleto Iris}\n(+) Obtén {{price, money}}", "selected": "¡Maravilloso!@d{32} ¡Ven, John!, {{strongestPokemon}}!$¡Es hora de mostrarte a todos en el club náutico!$¡Estarán tan celosos!" }, "2": { diff --git a/src/locales/es/mystery-encounters/berries-abound-dialogue.json b/src/locales/es/mystery-encounters/berries-abound-dialogue.json index 62e7bf6c7c17..4b9da99fbebb 100644 --- a/src/locales/es/mystery-encounters/berries-abound-dialogue.json +++ b/src/locales/es/mystery-encounters/berries-abound-dialogue.json @@ -1,14 +1,14 @@ { "intro": "¡Hay un gran arbusto de bayas cerca de ese Pokémon!", "title": "Bayas Abundantes", - "description": "Parece que hay un Pokémon fuerte protegiendo un arbusto de bayas. Luchar es el enfoque directo, pero parece fuerte. Quizás un Pokémon rápido podría agarrar algunas bayas sin ser descubierto?", - "query": "¿Que harás?", + "description": "Parece que hay un Pokémon fuerte protegiendo un arbusto de bayas. Luchar es el enfoque directo, pero parece fuerte. ¿Quizás un Pokémon rápido podría agarrar algunas bayas sin ser descubierto?", + "query": "¿Qué harás?", "berries": "¡Bayas!", "option": { "1": { "label": "Enfréntate al Pokémon", "tooltip": "(-) Batalla Difícil\n(+) Obtén bayas", - "selected": "You approach the\nPokémon without fear." + "selected": "Te acercas al\nPokémon sin miedo." }, "2": { "label": "Corre hacia el arbusto", diff --git a/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json index 5e4e8be90e79..6b88fb767238 100644 --- a/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json +++ b/src/locales/es/mystery-encounters/bug-type-superfan-dialogue.json @@ -1,38 +1,38 @@ { - "intro": "An unusual trainer with all kinds of Bug paraphernalia blocks your way!", - "intro_dialogue": "Hey, trainer! I'm on a mission to find the rarest Bug Pokémon in existence!$You must love Bug Pokémon too, right?\nEveryone loves Bug Pokémon!", - "title": "The Bug-Type Superfan", - "speaker": "Bug-Type Superfan", - "description": "The trainer prattles, not even waiting for a response...\n\nIt seems the only way to get out of this situation is by catching the trainer's attention!", - "query": "What will you do?", + "intro": "¡Un entrenador inusual con todo tipo de parafernalia de bichos bloquea tu camino!", + "intro_dialogue": "¡Hola, entrenador! ¡Estoy en una misión para encontrar el Pokémon Bicho más raro que existe!$¿A ti también te encantan los Pokémon Bicho, verdad? ¡A todos les encantan los Pokémon Bicho!", + "title": "El Superfan de los Pokémon Bicho", + "speaker": "Superfan de los Pokémon Bicho", + "description": "El entrenador parlotea, sin siquiera esperar una respuesta...\n\n¡Parece que la única forma de salir de esta situación es captando la atención del entrenador!", + "query": "¿Qué harás?", "option": { "1": { - "label": "Offer to Battle", - "tooltip": "(-) Challenging Battle\n(+) Teach a Pokémon a Bug Type Move", - "selected": "A challenge, eh?\nMy bugs are more than ready for you!" + "label": "Proponer a luchar", + "tooltip": "(-) Batalla Desafiante\n(+) Enseña un movimiento de tipo Bicho a un Pokémon", + "selected": "¿Un desafío, eh?\n¡Mis Pokémon Bicho están más que preparados para ti!" }, "2": { - "label": "Show Your Bug Types", - "tooltip": "(+) Receive a Gift Item", - "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!" + "label": "Muestra tus tipos Bicho", + "tooltip": "(+) ¡Recibe un objeto de regalo!", + "disabled_tooltip": "Necesitas al menos 1 Pokémon de tipo Bicho en tu equipo para seleccionar esto.", + "selected": "Le muestras al entrenador todos tus Pokémon de tipo Bicho...", + "selected_0_to_1": "¿Eh? Solo tienes {{numBugTypes}}...$Supongo que estoy perdiendo el tiempo con alguien como tú...", + "selected_2_to_3": "¡Oye, tienes {{numBugTypes}} Pokémon de tipo Bicho! No está mal.$Aquí, esto podría ayudarte en tu viaje para atrapar más.", + "selected_4_to_5": "¿Qué? ¿Tienes {{numBugTypes}} Bug Types?\nNice!$No estás a mi nivel, pero puedo ver destellos de mí en ti.$¡Toma esto, mi joven aprendiz!", + "selected_6": "¡Vaya! {{numBugTypes}}Pokémon de tipo Bicho!$¡Debes amar a los Pokémon de tipo Bicho casi tanto como yo!$Aquí, toma esto como un símbolo de nuestra camaradería." }, "3": { - "label": "Gift a Bug Item", - "tooltip": "(-) Give the trainer a {{requiredBugItems}}\n(+) Receive a Gift Item", - "disabled_tooltip": "You need to have a {{requiredBugItems}} to select this.", - "select_prompt": "Select an item to give.", - "invalid_selection": "Pokémon doesn't have that kind of item.", - "selected": "You hand the trainer a {{selectedItem}}.", - "selected_dialogue": "Whoa! A {{selectedItem}}, for me?\nYou're not so bad, kid!$As a token of my appreciation,\nI want you to have this special gift!$It's been passed all through my family, and now I want you to have it!" + "label": "Regala un objeto de tipo Bicho", + "tooltip": "(-) Dale al entrenador un {{requiredBugItems}}\n(+) Recibe un item de regalo", + "disabled_tooltip": "Necesitas tener un {{requiredBugItems}} para seleccionar esto.", + "select_prompt": "Selecciona un objeto para dar", + "invalid_selection": "El Pokémon no tiene ese tipe de objeto.", + "selected": "Le entregas al entrenador un{{selectedItem}}.", + "selected_dialogue": "¡Vaya! ¿Un {{selectedItem}}, para mí? ¡No eres tan malo, chico!$Como muestra de mi agradecimiento, quiero que tengas este regalo especial.$Ha pasado por toda mi familia, y ahora quiero que lo tengas tú." } }, - "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!" + "battle_won": "¡Tu conocimiento y habilidad fueron perfectos para explotar nuestras debilidades!$A cambio de la valiosa lección, permíteme enseñarle a uno de tus Pokémon un movimiento de tipo Bicho.", + "teach_move_prompt": "Selecciona un movimiento para enseñar a un Pokémon.", + "confirm_no_teach": "¿Estás seguro de que no quieres aprender uno de estos excellentes movimientos?", + "outro": "¡Veo grandes Pokémon de tipo Bicho en tu futuro! ¡Que nuestros caminos se crucen de nuevo! ¡Bicho fuera!" } diff --git a/src/locales/es/mystery-encounters/clowning-around-dialogue.json b/src/locales/es/mystery-encounters/clowning-around-dialogue.json index 177812408387..5199c8d64324 100644 --- a/src/locales/es/mystery-encounters/clowning-around-dialogue.json +++ b/src/locales/es/mystery-encounters/clowning-around-dialogue.json @@ -1,34 +1,33 @@ { - "intro": "It's...@d{64} a clown?", - "speaker": "Clown", - "intro_dialogue": "Bumbling buffoon, brace for a brilliant battle!\nYou'll be beaten by this brawling busker!", - "title": "Clowning Around", - "description": "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe {{blacephalonName}} is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}", - "query": "What will you do?", + "intro": "¿Es un...@d{64} payaso?", + "speaker": "Payaso", + "intro_dialogue": "¡Bufón torpe, prepárate para una batalla brillante! ¡Serás derrotado por este trovador peleador!", + "description": "Algo no esta bien en este encuentro. El payaso parece ansioso por provocarte a una batalla, ¿pero con qué fin? El {{blacephalonName}} es especialmente extraño, como si tuviera @[TOOLTIP_TITLE]{tipos y habilidades raros.}", + "query": "¿Qué harás?", "option": { "1": { - "label": "Battle the Clown", - "tooltip": "(-) Strange Battle\n(?) Affects Pokémon Abilities", - "selected": "Your pitiful Pokémon are poised for a pathetic performance!", - "apply_ability_dialogue": "A sensational showcase!\nYour savvy suits a sensational skill as spoils!", - "apply_ability_message": "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!", - "ability_prompt": "Would you like to permanently teach a Pokémon the {{ability}} ability?", - "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!" + "label": "Enfrentarse al Payaso", + "tooltip": "(-) Batalla extraña\n(?) Afecta las habilidades de los Pokémon", + "selected": "¡Tus patéticos Pokémon están listos para una actuación patética!", + "apply_ability_dialogue": "¡Una exhibición sensacional! ¡Tu astucia se adapta a una habilidad sensacional como recompensa!", + "apply_ability_message": "¡El payaso está ofreciendo intercambiar permanentemente la habilidad de uno de tus Pokémon por {{ability}}!", + "ability_prompt": "¿Te gustaría enseñar permanentemente a un Pokémon la habilidad {{ability}}?", + "ability_gained": "¡@s{level_up_fanfare}{{chosenPokemon}} obtenió la habilidad {{ability}}!" }, "2": { - "label": "Remain Unprovoked", - "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Items", - "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", - "selected_2": "The clown's {{blacephalonName}} uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!", - "selected_3": "Flustered fool, fall for my flawless deception!" + "label": "No involucrarse", + "tooltip": "(-) Molesta al payaso\n(?) Afecta los objetos de los Pokémon", + "selected": "¡Cobarde desdichado, niegas un exquisito duelo?\n ¡Siente mi furia!", + "selected_2": "¡El {{blacephalonName}} del payaso usa Truco! ¡Todos los objetos de tu {{switchPokemon}} fueron intercambiados al azar!", + "selected_3": "¡Tonto desconcertado, cae en mi engaño impecable!" }, "3": { - "label": "Return the Insults", - "tooltip": "(-) Upsets the Clown\n(?) Affects Pokémon Types", - "selected": "Dismal dodger, you deny a delightful duel?\nFeel my fury!", - "selected_2": "The clown's {{blacephalonName}} uses a strange move!\nAll of your team's types were randomly swapped!", - "selected_3": "Flustered fool, fall for my flawless deception!" + "label": "Devolver los insultos", + "tooltip": "(-) Molesta al payaso\n(?) Afecta los objetos de los Pokémon", + "selected": "¡Cobarde desdichado, niegas un exquisito duelo?\n ¡Siente mi furia!", + "selected_2": "¡El {{blacephalonName}} del payaso usa un movimiento extraño! ¡Todos los tipos de tu equipo fueron intercambiados al azar!", + "selected_3": "¡Tonto desconcertado, cae en mi engaño impecable!" } }, - "outro": "The clown and his cohorts\ndisappear in a puff of smoke." + "outro": "El payaso y sus secuaces\ndesaparecen en una nube de humo." } \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json index 8e2883ecb161..c4494e3efa8d 100644 --- a/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json +++ b/src/locales/es/mystery-encounters/dancing-lessons-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "An {{oricorioName}} dances sadly alone, without a partner.", - "title": "Dancing Lessons", - "description": "The {{oricorioName}} doesn't seem aggressive, if anything it seems sad.\n\nMaybe it just wants someone to dance with...", - "query": "What will you do?", + "intro": "Un {{oricorioName}} baila tristemente solo, sin pareja.", + "title": "Clases de baile", + "description": "El {{oricorioName}} no parece agresivo, más bien parece triste.\nTal vez solo quiera alguien con quien bailar...", + "query": "¿Qué harás?", "option": { "1": { - "label": "Battle It", - "tooltip": "(-) Tough Battle\n(+) Gain a Baton", - "selected": "The {{oricorioName}} is distraught and moves to defend itself!", - "boss_enraged": "The {{oricorioName}}'s fear boosted its stats!" + "label": "Enfrentarse", + "tooltip": "(-) Batalla Ardua\n(+) Obtén el objeto Relevo", + "selected": "¡El {{oricorioName}} está angustiado e intenta defenderse!", + "boss_enraged": "¡El miedo del {{oricorioName}} aumentó sus estadísticas!" }, "2": { - "label": "Learn Its Dance", - "tooltip": "(+) Teach a Pokémon Revelation Dance", - "selected": "You watch the {{oricorioName}} closely as it performs its dance...$@s{level_up_fanfare}Your {{selectedPokemon}} learned from the {{oricorioName}}!" + "label": "Aprende su danza", + "tooltip": "(+) Enseña a un Pokémon Danza despertar", + "selected": "Observas atentamente al {{oricorioName}} mientras realiza su danza…$@s{level_up_fanfare}¡Tu {{selectedPokemon}} aprendió del {{oricorioName}}!" }, "3": { - "label": "Show It a Dance", - "tooltip": "(-) Teach the {{oricorioName}} a Dance Move\n(+) The {{oricorioName}} Will Like You", - "disabled_tooltip": "Your Pokémon need to know a Dance move for this.", - "select_prompt": "Select a Dance type move to use.", - "selected": "The {{oricorioName}} watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!$It loves the display!$@s{level_up_fanfare}The {{oricorioName}} wants to join your party!" + "label": "Muéstrale una danza", + "tooltip": "(-) Enseña al {{oricorioName}} un movimiento de danza\n(+) Le gustaras al {{oricorioName}}", + "disabled_tooltip": "Tus Pokémon necesitan conocer un movimiento de danza para esto.", + "select_prompt": "Selecciona un movimiento de tipo danza para usar.", + "selected": "¡El {{oricorioName}} observa fascinado mientras\n{{selectedPokemon}} muestra {{selectedMove}}!$¡Le encanta la exhibición!$@s{level_up_fanfare}¡El {{oricorioName}} quiere unirse a tu equipo!" } }, - "invalid_selection": "This Pokémon doesn't know a Dance move" + "invalid_selection": "Este Pokémon no conoce ningún movimiento de danza" } \ No newline at end of file diff --git a/src/locales/es/mystery-encounters/dark-deal-dialogue.json b/src/locales/es/mystery-encounters/dark-deal-dialogue.json index 3086ebb0f9b7..19b5c5c5b780 100644 --- a/src/locales/es/mystery-encounters/dark-deal-dialogue.json +++ b/src/locales/es/mystery-encounters/dark-deal-dialogue.json @@ -1,24 +1,24 @@ { - "intro": "A strange man in a tattered coat\nstands in your way...", - "speaker": "Shady Guy", - "intro_dialogue": "Hey, you!$I've been working on a new device\nto bring out a Pokémon's latent power!$It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.$Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.", - "title": "Dark Deal", - "description": "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"", - "query": "What will you do?", + "intro": "Un hombre extraño con un abrigo andrajoso se interpone en tu camino...", + "speaker": "Tipo sombrío", + "intro_dialogue": "¡Oye, tú!$He estado trabajando en un nuevo dispositivo\npara sacar el poder latente de un Pokémon!$Reorganiza completamente los átomos del Pokémon\na nivel molecular en una forma mucho más poderosa.$Jeje…@d{64} Solo necesito algunos sac-@d{32}\nEh, sujetos de prueba, para demostrar que funciona.", + "title": "Pacto Oscuro", + "description": "El tipo inquietante sostiene unas Pokéballs.\n\"¡Te lo compensaré! Puedes tener estas Pokéballs fuertes como pago. ¡Todo lo que necesito es un Pokémon de tu equipo! Jeje...", + "query": "¿Qué harás?", "option": { "1": { - "label": "Accept", - "tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", - "selected_dialogue": "Let's see, that {{pokeName}} will do nicely!$Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...", - "selected_message": "The man hands you 5 Rogue Balls.${{pokeName}} hops into the strange machine...$Flashing lights and weird noises\nstart coming from the machine!$...@d{96} Something emerges\nfrom the device, raging wildly!" + "label": "Acceptar", + "tooltip": "(+) 5 Rogue Balls\n(?) Mejora un Pokémon aleatorio", + "selected_dialogue": "Veamos, ¡Ese {{pokeName}} servirá muy bien!$Recuerda, no soy responsable\nsi algo malo sucede!@d{32} Jeje...", + "selected_message": "El hombre te entrega 5 Rogue Balls.${{pokeName}} entra dentro de la máquina...$¡Luces intermitentes y ruidos extraños\ncomienzan a salir de la máquina!$...@d{96} Algo emerge\ndel dispositivo, ¡furiosamente!" }, "2": { - "label": "Refuse", - "tooltip": "(-) No Rewards", - "selected": "Not gonna help a poor fellow out?\nPah!" + "label": "Rechazar", + "tooltip": "(-) Ninguna Recompensa", + "selected": "¿No vas a ayudar a un pobre hombre?\n¡Bah!" } }, - "outro": "After the harrowing encounter,\nyou collect yourself and depart." + "outro": "Después del encuentro angustioso, te recuperas y te marchas." } \ No newline at end of file diff --git a/src/locales/fr/battle.json b/src/locales/fr/battle.json index c1b762560e18..8b6bddb745d1 100644 --- a/src/locales/fr/battle.json +++ b/src/locales/fr/battle.json @@ -48,7 +48,10 @@ "moveNotImplemented": "{{moveName}} n’est pas encore implémenté et ne peut pas être sélectionné.", "moveNoPP": "Il n’y a plus de PP pour\ncette capacité !", "moveDisabled": "{{moveName}} est sous entrave !", + "canOnlyUseMove": "{{pokemonName}} ne peut utiliser\nque la capacité {{moveName}} !", + "moveCannotBeSelected": "La capacité {{moveName}}\nne peut pas être choisie !", "disableInterruptedMove": "Il y a une entrave sur la capacité {{moveName}}\nde{{pokemonNameWithAffix}} !", + "throatChopInterruptedMove": "Exécu-Son empêche {{pokemonName}}\nd’utiliser la capacité !", "noPokeballForce": "Une force mystérieuse\nempêche l’utilisation des Poké Balls.", "noPokeballTrainer": "Le Dresseur détourne la Ball\nVoler, c’est mal !", "noPokeballMulti": "Impossible ! On ne peut pas viser\nquand il y a deux Pokémon !", diff --git a/src/locales/fr/dialogue.json b/src/locales/fr/dialogue.json index 5325760eb652..a0621e7d77ae 100644 --- a/src/locales/fr/dialogue.json +++ b/src/locales/fr/dialogue.json @@ -509,8 +509,8 @@ "2": "Hé, hé, hé !\nTu mets le feu !" }, "defeat": { - "1": "Oh ? En panne de carburant je suppose ?", - "2": "Oh ? En panne de carburant je suppose ?" + "1": "Oh ?\nEn panne de carburant je suppose ?", + "2": "Oh ?\nEn panne de carburant je suppose ?" } }, "stat_trainer_cheryl": { @@ -571,7 +571,7 @@ }, "winstrates_victor": { "encounter": { - "1": "Bon esprit ! J’aime ça !" + "1": "Bon esprit !\nJ’aime ça !" }, "victory": { "1": "Mince !\nTu as un meilleur niveau que je ne le pensais !" @@ -579,7 +579,7 @@ }, "winstrates_victoria": { "encounter": { - "1": "Oh, ciel ! Ce que tu es jeune !$Mais si tu as battu mon mari, c’est que tu sais t’y prendre.$À nous deux, maintenant !" + "1": "Oh, ciel !\nCe que tu es jeune !$Mais si tu as battu mon mari,\nc’est que tu sais t’y prendre.$À nous deux, maintenant !" }, "victory": { "1": "Ciel ! Cette force !\nJ’en suis toute retournée !" @@ -595,7 +595,7 @@ }, "winstrates_vicky": { "encounter": { - "1": "Comment oses-tu faire pleurer mon adorable petite-fille !$Tu vas me le payer !\nTes Pokémon vont mordre la poussière !" + "1": "Comment oses-tu faire pleurer\nmon adorable petite-fille !$Tu vas me le payer !\nTes Pokémon vont mordre la poussière !" }, "victory": { "1": "Ouh! Quelle puissance…\nMa petite-fille avait raison…" @@ -603,7 +603,7 @@ }, "winstrates_vito": { "encounter": { - "1": "On s’entraine tous ensemble, avec les membres de ma famille !$Je ne perds contre personne !" + "1": "On s’entraine tous ensemble,\navec les membres de ma famille !$Je ne perds contre personne !" }, "victory": { "1": "J’ai toujours été le meilleur de la famille.\nJe n’avais encore jamais perdu…" diff --git a/src/locales/fr/menu.json b/src/locales/fr/menu.json index 277b0f5fd047..35cd06606a70 100644 --- a/src/locales/fr/menu.json +++ b/src/locales/fr/menu.json @@ -46,7 +46,7 @@ "yes": "Oui", "no": "Non", "disclaimer": "AVERTISSEMENT", - "disclaimerDescription": "Ce jeu n’est pas un produit fini et peut contenir des problèmes de jouabilité, dont de possibles pertes de sauvegardes,\ndes modifications sans avertissement et pourrait ou non encore être mis à jour ou terminé.", + "disclaimerDescription": "Ce jeu n’est pas un produit fini.\nIl peut contenir des problèmes de jouabilité, dont de possibles pertes de sauvegardes,\ndes modifications sans avertissement et pourrait à tout moment cesser d’être mis à jour.", "choosePokemon": "Sélectionnez un Pokémon.", "renamePokemon": "Renommer le Pokémon", "rename": "Renommer", diff --git a/src/locales/fr/modifier-type.json b/src/locales/fr/modifier-type.json index a97a146d4979..a4bd7cabe3c2 100644 --- a/src/locales/fr/modifier-type.json +++ b/src/locales/fr/modifier-type.json @@ -72,7 +72,7 @@ "name": "Jus de Caratroc", "description": "{{increaseDecrease}} toutes les stats de son porteur de {{statValue}} cran. Caratroc vous a {{blessCurse}}.", "extra": { - "increase": "Augumente", + "increase": "Augmente", "decrease": "Baisse", "blessed": "accordé sa bénédiction", "cursed": "jeté une malédiction" diff --git a/src/locales/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 3704bc90718f..6f9d9d4dd630 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -7,6 +7,7 @@ "switchedStat": "{{pokemonName}} et sa cible échangent leur {{stat}} !", "sharedGuard": "{{pokemonName}} additionne sa garde à celle de sa cible et redistribue le tout équitablement !", "sharedPower": "{{pokemonName}} additionne sa force à celle de sa cible et redistribue le tout équitablement !", + "shiftedStats": "{{pokemonName}} échange {{statToSwitch}} et {{statToSwitchWith}} !", "goingAllOutForAttack": "{{pokemonName}} a pris\ncette capacité au sérieux !", "regainedHealth": "{{pokemonName}}\nrécupère des PV !", "keptGoingAndCrashed": "{{pokemonName}}\ns’écrase au sol !", @@ -67,5 +68,7 @@ "swapArenaTags": "Les effets affectant chaque côté du terrain\nont été échangés par {{pokemonName}} !", "exposedMove": "{{targetPokemonName}} est identifié\npar {{pokemonName}} !", "safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !", + "substituteOnOverlap": "{{pokemonName}} a déjà\nun clone !", + "substituteNotEnoughHp": "Mais il est trop faible\npour créer un clone !", "afterYou": "{{pokemonName}} accepte\navec joie !" } diff --git a/src/locales/fr/move.json b/src/locales/fr/move.json index a48e17b3fd9f..957895b5db93 100644 --- a/src/locales/fr/move.json +++ b/src/locales/fr/move.json @@ -364,8 +364,8 @@ "effect": "Le lanceur creuse au premier tour et frappe au second." }, "toxic": { - "name": "Fil Toxique", - "effect": "Tisse un fil imprégné de venin. Empoisonne la cible et baisse sa Vitesse." + "name": "Toxik", + "effect": "Le lanceur empoisonne gravement la cible. Les dégâts dus au poison augmentent à chaque tour." }, "confusion": { "name": "Choc Mental", diff --git a/src/locales/fr/mystery-encounter-messages.json b/src/locales/fr/mystery-encounter-messages.json index 7641243dfc49..a4394d7d503f 100644 --- a/src/locales/fr/mystery-encounter-messages.json +++ b/src/locales/fr/mystery-encounter-messages.json @@ -1,5 +1,5 @@ { - "paid_money": "Vous avez payé {{amount, number}} ₽.", + "paid_money": "Vous payez {{amount, number}} ₽.", "receive_money": "Vous recevez {{amount, number}} ₽ !", "affects_pokedex": "Affecte les données du Pokédex", "cancel_option": "Retour au choix des options.", diff --git a/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json b/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json index a4f91c01087f..47c1889227df 100644 --- a/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json +++ b/src/locales/fr/mystery-encounters/absolute-avarice-dialogue.json @@ -8,18 +8,18 @@ "label": "L’affronter", "tooltip": "(-) Combat difficile\n(+) Récompenses de sa planque à Baies", "selected": "Le {{greedentName}} gonfle ses joues\net se prépare au combat !", - "boss_enraged": "L’insatiable amour de {{greedentName}} pour la nourriture le rend furieux !", - "food_stash": "Il semblerait que {{greedentName}} gardait une pile de nourriture colossale !$@s{item_fanfare}Tous les Pokémon de votre équipe remportent une {{foodReward}} !" + "boss_enraged": "L’insatiable amour de {{greedentName}}\npour la nourriture le rend fou !", + "food_stash": "Il semblerait que {{greedentName}}\ngardait une pile de nourriture colossale !$@s{item_fanfare}Tous les Pokémon de votre équipe\nremportent une {{foodReward}} !" }, "2": { "label": "Négocier", "tooltip": "(+) Regagner quelques Baies", - "selected": "Vos arguments ont touché {{greedentName}} sur sa corde sensible.$Il ne vous rend pas toutes vos Baies, mais vous en balance quelques-unes." + "selected": "Vos arguments ont touché {{greedentName}}\nsur sa corde sensible.$Il accepte malgré tout de vous balancer\nquelques Baies et se garde le reste." }, "3": { "label": "Lui laisser les Baies", "tooltip": "(-) Baies définitement perdues\n(?) {{greedentName}} vous apprécie", - "selected": "Le {{greedentName}} engloutit l’intégralité\ndes Baies en un éclair !$Il vous regarde avec tendresse\nen se tapotant le ventre.$Peut-être pourriez-vous lui\nen donner encore pendant votre périple…$@s{level_up_fanfare}Le {{greedentName}} veut rejoindre votre équipe !" + "selected": "Le {{greedentName}} engloutit l’intégralité\ndes Baies en un éclair !$Il vous regarde avec tendresse\nen se tapotant le ventre.$Peut-être pourriez-vous lui en donner encore\nplus pendant votre périple…$@s{level_up_fanfare}Le {{greedentName}} veut rejoindre votre équipe !" } } } diff --git a/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 4ed1fd262a3b..e87cbc4dfbcd 100644 --- a/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/fr/mystery-encounters/an-offer-you-cant-refuse-dialogue.json @@ -1,15 +1,15 @@ { "intro": "Un jeune garçon aux airs très bougeois vous arrête.", "speaker": "Richard", - "intro_dialogue": "Bonchour-haann !$Je ne puis carrément pas ignorer que votre\n{{strongestPokemon}} m’a l’air fa-bu-leux !$J’ai toujours désiré posséder un tel compagnon !$Je peux vous payer grassement,\nainsi que cette petite babiole !", + "intro_dialogue": "Bonchour-haann !$Je ne puis carrément pas ignorer que votre\n{{strongestPokemon}} m’a l’air fa-bu-leux-han !$J’ai toujours désiré posséder un tel Pokémon !$Je peux vous payer grassement,\nainsi que vous donner petite babiole-han !", "title": "L’affaire du siècle", - "description": "Un fils à papa vous offre un @[TOOLTIP_TITLE]{Shiny Charm} et {{price, money}} en échange de votre {{strongestPokemon}} !\n\nÇa semble être une bonne affaire, mais pourrez vous supporter de priver votre équipe d’un tel atout ?", + "description": "Un fils à papa vous offre un @[TOOLTIP_TITLE]{Charme Chroma} et {{price, money}} en échange de votre {{strongestPokemon}} !\n\nÇa semble être une bonne affaire, mais pourrez vous supporter de priver votre équipe d’un tel atout ?", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Accepter l’offre", - "tooltip": "(-) Vous perdez {{strongestPokemon}}\n(+) Gain d’un @[TOOLTIP_TITLE]{Shiny Charm}\n(+) Gain de {{price, money}}", - "selected": "Fa-bu-leux!@d{32} Par ici, {{strongestPokemon}} !$Viens que je te montre fièrement au club de yacht !$Ils vont trooop avoir le seum-haann !" + "tooltip": "(-) Vous perdez {{strongestPokemon}}\n(+) Gain d’un @[TOOLTIP_TITLE]{Charme Chroma}\n(+) Gain de {{price, money}}", + "selected": "Fa-bu-leux-han !@d{32} Par ici, {{strongestPokemon}} !$Viens que je te montre fièrement au club de yacht !$Ils vont trooop avoir le seum-haann !" }, "2": { "label": "Le racketter", @@ -20,7 +20,7 @@ "3": { "label": "Partir", "tooltip": "(-) Aucune récompense", - "selected": "Quelle journée pourrie…$Viens {{liepardName}}. On retourne au club de yacht…" + "selected": "Quelle journée pourrie…$Viens {{liepardName}}.\nOn retourne au club de yacht…" } } } diff --git a/src/locales/fr/mystery-encounters/berries-abound-dialogue.json b/src/locales/fr/mystery-encounters/berries-abound-dialogue.json index 127c022c76f5..e97519b88a1e 100644 --- a/src/locales/fr/mystery-encounters/berries-abound-dialogue.json +++ b/src/locales/fr/mystery-encounters/berries-abound-dialogue.json @@ -1,12 +1,12 @@ { "intro": "Il y a un gros buisson à Baies\nprès de ce Pokémon !", "title": "Baies à gogo", - "description": "Un Pokémon a l’air de monter la garde sur ce buissons à Baies. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Un Pokémon rapide pourrait peut-être en attraper quelques-unes sans se faire prendre ?", + "description": "Un Pokémon a l’air de monter la garde de ce buisson à Baies. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Un Pokémon rapide pourrait peut-être en attraper quelques-unes sans se faire prendre ?", "query": "Que voulez-vous faire ?", "berries": "Des Baies !", "option": { "1": { - "label": "Combattre le Pokémon", + "label": "L’affronter", "tooltip": "(-) Combat difficile\n(+) Gain de Baies", "selected": "Vous approchez\nle Pokémon sans frémir." }, @@ -19,7 +19,7 @@ }, "3": { "label": "Partir", - "tooltip": "(-) Aucun récompense", + "tooltip": "(-) Aucune récompense", "selected": "Vous renoncez à ce Pokémon avec\nson butin et continuez votre route." } } diff --git a/src/locales/fr/mystery-encounters/clowning-around-dialogue.json b/src/locales/fr/mystery-encounters/clowning-around-dialogue.json index faf7a4249d6c..cd606478fa31 100644 --- a/src/locales/fr/mystery-encounters/clowning-around-dialogue.json +++ b/src/locales/fr/mystery-encounters/clowning-around-dialogue.json @@ -3,14 +3,14 @@ "speaker": "Clown", "intro_dialogue": "T’as l’air clownesque, prépare-toi pour un combat magistral !$Je vais te montrer ce que sont les arts de la rue !", "title": "Bouffonneries", - "description": "Quelque chose semble louche. Ce Clown semble très motivé de vous provoquer en combat, mais dans quel but ?\n\nLe {{blacephalonName}} est très étrange, comme s’il possédait @[TOOLTIP_TITLE]{des types et un talent inhabituels.}", + "description": "Quelque chose semble louche. Ce Clown a l’air très motivé de vous provoquer en combat, mais dans quel but ?\n\nLe {{blacephalonName}} est très étrange, comme s’il possédait @[TOOLTIP_TITLE]{des types et un talent inhabituels.}", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Affronter le Clown", "tooltip": "(-) Combat étrange\n(?) Affecte les talents des Pokémon", "selected": "Vos Pokémon sont prêts pour cette performance pathétique !", - "apply_ability_dialogue": "Un spectacle sensationnel !\nVotre savoir-faire est en harmonie avec votre compétences !", + "apply_ability_dialogue": "Un spectacle sensationnel !\nVotre savoir-faire est en harmonie avec vos compétences !", "apply_ability_message": "Le Clown vous propose d’Échanger définitivement le talent d’un de vos Pokémon contre {{ability}} !", "ability_prompt": "Voulez-vous définitivement donner le talent {{ability}} à un Pokémon ?", "ability_gained": "@s{level_up_fanfare}{{chosenPokemon}} obtient le talent {{ability}} !" diff --git a/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json b/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json index 01aaccb79482..61a5049a7c7c 100644 --- a/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json +++ b/src/locales/fr/mystery-encounters/dancing-lessons-dialogue.json @@ -1,22 +1,22 @@ { - "intro": "Un {{oricorioName}} dance tristement seul, sans partenaire.", - "title": "Lessons de danse", - "description": "Ce {{oricorioName}} ne semble pas agressif, mais triste tout au plus.\n\nPeut-être a-t-il juste besoin de quelqu’un avec qui danser…", + "intro": "Un {{oricorioName}} danse tristement seul,\nsans partenaire pour l’accompagner.", + "title": "Leçons de danse", + "description": "Ce {{oricorioName}} ne semble pas hostile, mais a tout au plus juste l’air triste.\n\nPeut-être a-t-il juste besoin d’un ou d’une partenaire pour l’accompagner ?…", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "L’affronter", "tooltip": "(-) Combat difficile\n(+) Gain d’un Bâton", - "selected": "Le {{oricorioName}} est desemparé et tente de se défendre !", - "boss_enraged": "La peur de {{oricorioName}} augumente beaucoup ses stats !" + "selected": "Le {{oricorioName}} est desemparé\net tente de se défendre !", + "boss_enraged": "La peur de {{oricorioName}} augmente beaucoup ses stats !" }, "2": { "label": "Apprendre sa danse", "tooltip": "(+) Apprendre Danse Éveil à un Pokémon", - "selected": "Vos scrutez {{oricorioName}} de près pendant sa danse…$@s{level_up_fanfare}Votre {{selectedPokemon}} apprend Danse Éveil de {{oricorioName}} !" + "selected": "Vos scrutez {{oricorioName}} de près pendant sa danse…$@s{level_up_fanfare}Votre {{selectedPokemon}} apprend la capacité\nDanse Éveil de {{oricorioName}} !" }, "3": { - "label": "Lui montrer une danse", + "label": "Montrer une danse", "tooltip": "(-) Apprendre à {{oricorioName}} une capacité dansante\n(+) Le {{oricorioName}} vous apprécie", "disabled_tooltip": "Votre Pokémon doit connaitre une capacité dansante pour choisir cette option.", "select_prompt": "Sélectionnez une capacité dansante.", diff --git a/src/locales/fr/mystery-encounters/dark-deal-dialogue.json b/src/locales/fr/mystery-encounters/dark-deal-dialogue.json index 5de58697ec93..3d419eedf8e1 100644 --- a/src/locales/fr/mystery-encounters/dark-deal-dialogue.json +++ b/src/locales/fr/mystery-encounters/dark-deal-dialogue.json @@ -1,23 +1,23 @@ { - "intro": "Un homme suspect vêtu d’un manteau en lambeaux\nse tient au milieu du chemin…", - "speaker": "Type chelou", - "intro_dialogue": "Hé, toi!$Je travaille sur un dispositif qui permet\nd’éveiller la puissance d’un Pokémon !$Il restructure complètement les atomes du Pokémon\nen une forme bien plus puissante.$Héhé…@d{64} Je n’ai besoin que de sac-@d{32}\nEuuh, sujets tests, pour prouver son fonctionnement.", + "intro": "Un homme suspect vêtu d’une blouse en lambeaux\nse tient au milieu du chemin…", + "speaker": "Savant fou", + "intro_dialogue": "Hé, toi !$Je travaille sur un dispositif qui permet d’éveiller\nla vraie puissance d’un Pokémon !$Il restructure complètement les atomes du Pokémon\npour en faire une version bien plus puissante.$Héhé…@d{64} Je n’ai besoin que de sac-@d{32}\nEuuh, sujets tests, pour prouver son fonctionnement.", "title": "L’Expérience interdite", - "description": "Le type chelou tient dans ses mains quelques Poké Balls.\n« T’inquites pas, je gère ! Voilà quelques Poké Balls plutôt efficaces en caution, tout ce dont j’ai besoin, c’est un de tes Pokémon ! Héhé… »", + "description": "Ce scientifique à l’air un peu taré tient dans ses mains quelques Poké Balls.\n« T’inquites pas, je gère ! Voilà quelques Poké Balls plutôt efficaces en caution, tout ce dont j’ai besoin, c’est un de tes Pokémon ! Héhé… »", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Accepter", - "tooltip": "(+) 5 Rogue Balls\n(?) Améliorer un Pokémon au hasard", - "selected_dialogue": "Ah bien, ce {{pokeName}} fera parfaitement l’affaire !$Je précise que si quelque chose tourne mal,\nje ne suis pas responsable !@d{32} Héhé…", + "tooltip": "(+) 5 Rogue Balls\n(?) Améliore un Pokémon au hasard", + "selected_dialogue": "Ah bien, ton {{pokeName}} fera parfaitement l’affaire !$Je précise que si quelque chose tourne mal,\nje ne suis pas responsable !@d{32} Héhé…", "selected_message": "L’homme vous remet 5 Rogue Balls.$Votre {{pokeName}} saute dans l’étrange dispositif…$Le dispositif émet des lumières\nclignotantes et des bruits douteux !$…@d{96} Quelque chose sort du dispositif avec fureur !" }, "2": { "label": "Refuser", "tooltip": "(-) Aucune récompense", - "selected": "On a même plus le droit d’aider maintenant ?\nPfff !" + "selected": "On a même plus le droit de rendre service maintenant ?\nPfff !" } }, "outro": "Après cette épuisante rencontre,\nvous reprenez vos esprits et repartez." diff --git a/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json b/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json index 43ef57ba549c..cb1763c6015e 100644 --- a/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json +++ b/src/locales/fr/mystery-encounters/department-store-sale-dialogue.json @@ -1,27 +1,27 @@ { "intro": "Il y a une dame avec des tas de sacs de courses.", "speaker": "Cliente", - "intro_dialogue": "Bonjour !\nToi aussi t’es là pour les incroyables promos ?$Il y a un coupon spécial que tu peux utiliser en échange\nd’un objet gratuit pendant toute la durée de la promo !$J’en ai un en trop.\nTiens, prends-le!", + "intro_dialogue": "Bonjour !\nToi aussi t’es là pour les incroyables promos ?$Il y a un coupon spécial que tu peux utiliser en échange\nd’un objet gratuit pendant toute la durée de la promo !$J’en ai un en trop.\nTiens, prends-le !", "title": "Promos au Centre Commercial", "description": "Vous voyez des produits où que vous regardez ! Il y a 4 comptoirs auprès desquels vous pouvez dépenser ce coupon contre une grande variété d’objets. Que de choix !", "query": "À quel comptoir se rendre ?", "option": { "1": { - "label": "Comptoir de CT", + "label": "CT", "tooltip": "(+) Boutique de CT" }, "2": { - "label": "Comptoir de Vitamines", - "tooltip": "(+) Boutique de Vitamines" + "label": "Accélérateurs", + "tooltip": "(+) Boutique d’Accélérateurs" }, "3": { - "label": "Comptoir d’Objets de Combat", + "label": "Objets de Combat", "tooltip": "(+) Boutique d’objets de boost" }, "4": { - "label": "Comptoir de Poké Balls", + "label": "Poké Balls", "tooltip": "(+) Boutique de Poké Balls" } }, - "outro": "Quelle affaire ! Vous devriez revenir y faire vos achats plus souvent." + "outro": "Quelle affaire !\nVous devriez revenir y faire vos achats plus souvent." } diff --git a/src/locales/fr/mystery-encounters/field-trip-dialogue.json b/src/locales/fr/mystery-encounters/field-trip-dialogue.json index 2d1ef1ad80a3..601a46c069af 100644 --- a/src/locales/fr/mystery-encounters/field-trip-dialogue.json +++ b/src/locales/fr/mystery-encounters/field-trip-dialogue.json @@ -1,27 +1,27 @@ { "intro": "C’est une enseignante avec ses élèves !", "speaker": "Enseignante", - "intro_dialogue": "Hé, bonjour ! Aurais-tu une minute à accorder à mes élèves ?$Je leur apprends ce que sont les capacités et\nj’adorerais que tu leur fasse une démosntration.$Tu serais d’accord pour nous montrer ça avec un de tes Pokémon ?", - "title": "En situation réelle", - "description": "Une enseignante vous demande de faire la démonstration d’une capacité d’un de vos Pokémon. En fonction de la capacité, elle pourrait vous donner quelque chose d’utile.", - "query": "Utiliser une capacité de quelle catégorie ?", + "intro_dialogue": "Hé, bonjour !\nAurais-tu une minute à accorder à mes élèves ?$Je leur apprends ce que sont les capacités et\nj’adorerais que tu leur fasse une démosntration.$Tu serais d’accord pour nous montrer ça\navec un de tes Pokémon ?", + "title": "Sortie scolaire", + "description": "Une enseignante vous demande de faire la démonstration d’une capacité d’un de vos Pokémon.\nEn fonction de la capacité, elle pourrait vous donner quelque chose d’utile.", + "query": "Utiliser quelle catégorie de capacité ?", "option": { "1": { "label": "Physique", - "tooltip": "(+) Récompense d’objets Physiques" + "tooltip": "(+) Objets de boost axés sur le Physique à la prochaine boutique" }, "2": { "label": "Spéciale", - "tooltip": "(+) Récompense d’objets Spéciaux" + "tooltip": "(+) Objets de boost axés sur le Spécial à la prochaine boutique" }, "3": { "label": "De Statut", - "tooltip": "(+) Récompense d’objets de Statut" + "tooltip": "(+) Objets de boost axés sur le Statut à la prochaine boutique" }, "selected": "{{pokeName}} fait une incroyable démonstration de sa capacité {{move}} !" }, "second_option_prompt": "Choisissez une capacité à utiliser.", - "incorrect": "…$Ce n’est pas une capacité {{moveCategory}} !\nJe suis désolée, mais je ne peux rien te donner.$Venez les enfants, on va chercher mieux ailleurs.", + "incorrect": "…$Ce n’est pas une capacité {{moveCategory}} !\nJe suis désolée, mais je ne peux rien te donner.$Venez les enfants, on va trouver mieux ailleurs.", "incorrect_exp": "Il semble que vous avez appris une précieuse leçon ?$Votre Pokémon a gagné un peu d’expérience.", "correct": "Merci beaucoup de nous avoir accordé de ton temps !\nJ’espère que ces quelques objets de seront utiles !", "correct_exp": "{{pokeName}} a aussi gagné un beaucoup d’expérience !", diff --git a/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json b/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json index bb72b3b53131..f85f64830ad7 100644 --- a/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json +++ b/src/locales/fr/mystery-encounters/fiery-fallout-dialogue.json @@ -1,13 +1,13 @@ { - "intro": "Un vent incandescent de fumées et de cendres arrive sur vous !", - "title": "Fiery Fallout", - "description": "La visiblité est quasi nulle à cause des cendres et les braises tourbillonnantes. Il doit forcément y avoir une quelconque… source responsable de ces conditions. Mais que pourrait causer un phénomène d’une telle amplitude ?", + "intro": "Un vent incandescent de fumées et de cendres\nvous fonce dessus !", + "title": "Fait chaud là, non ?", + "description": "La visiblité est quasi nulle à cause des cendres et les braises tourbillonnantes. Il doit forcément y avoir une quelconque source responsable de ces conditions. Mais que pourrait causer un phénomène d’une telle ampleur ?", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Chercher la source", "tooltip": "(?) Trouver la source\n(-) Combat difficile", - "selected": "Vous pénétrez tant bien que mal dans la tempête et trouvez deux {{volcaronaName}} en pleine parade nuptiale !$Ils n’apprécient guère d’avoir été interrompus et vous attaquent !" + "selected": "Vous pénétrez tant bien que mal dans la tempête et y trouvez deux {{volcaronaName}} en pleine parade nuptiale !$Ils n’apprécient guère d’avoir été interrompus\net vous attaquent !" }, "2": { "label": "S’accroupir", @@ -19,7 +19,7 @@ "label": "Un type Feu à l’aide", "tooltip": "(+) Met fin à la météo\n(+) Gain d’un Charbon", "disabled_tooltip": "Vous avez besoin d’au moins 2 Pokémon Feu pour choisir cette option", - "selected": "Vos {{option3PrimaryName}} et {{option3SecondaryName}} vous guident et découvez deux {{volcaronaName}} en pleine parade nuptiale !$Heureusement, vos Pokémon parviennent à les calmer, et passent leur chemin sans causer de problèmes." + "selected": "Vos {{option3PrimaryName}} et {{option3SecondaryName}} vous guident et tombez sur deux {{volcaronaName}} en pleine parade nuptiale !$Heureusement, vos Pokémon parviennent à les calmer, et passent leur chemin sans causer de problèmes." } }, "found_charcoal": "Alors que la météo se calme, votre {{leadPokemon}} repère quelque chose au sol.$@s{item_fanfare}{{leadPokemon}} obtient\nun Charobn !" diff --git a/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json index 03452d126676..d5c6bb7513de 100644 --- a/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json +++ b/src/locales/fr/mystery-encounters/fight-or-flight-dialogue.json @@ -1,14 +1,14 @@ { "intro": "Quelque chose d’une lueur éclatante\nbrille sur sol près de ce Pokémon !", "title": "Voler ou s’envoler", - "description": "Un puissant Pokémon semble monter la garde d’un objet. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Le bon Pokémon pourrait peut-être voler l’objet sans se faire prendre ?", + "description": "Un puissant Pokémon semble monter la garde d’un objet. Vous pourriez aller frontalement au combat, mais il a l’air costaud. Avec le bon Pokémon, vous pourriez peut-être voler l’objet sans vous faire prendre ?", "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "Affronter le Pokémon", + "label": "L’affronter", "tooltip": "(-) Combat difficile\n(+) Nouvel objet", "selected": "Vous approchez\nle Pokémon sans frémir.", - "stat_boost": "Les forces dormantes du {{enemyPokemon}} augmentent une de ses stats !" + "stat_boost": "Les forces dormantes de {{enemyPokemon}}\naugmentent une de ses stats !" }, "2": { "label": "Voler l’objet", diff --git a/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json b/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json index a55ab1720f0d..cda660393c4e 100644 --- a/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json +++ b/src/locales/fr/mystery-encounters/fun-and-games-dialogue.json @@ -1,14 +1,14 @@ { - "intro_dialogue": "Approchez mesdames et messieurs ! Tentez votre chance au tout nouveau Ripost-o-matic de {{wobbuffetName}} !", + "intro_dialogue": "Approchez mesdames et messieurs !$Tentez votre chance au tout nouveau\nRipost-o-matic de {{wobbuffetName}} !", "speaker": "Animateur", "title": "Du rire et des jeux !", - "description": "Vous rencontrez un forain avec un jeu de hasard ! Vous avez @[TOOLTIP_TITLE]{3 tours} pour amener {{wobbuffetName}} le plus près possible de @[TOOLTIP_TITLE]{1 PV}, mais @[TOOLTIP_TITLE]{sans le mettre K.O.} afin qu’il puisse charger la Riposte la plus forte possible sur la cloche de la machine.\nMais attention ! Si {{wobbuffetName}} est mis K.O., vous devrez payer pour la ranimer !", + "description": "Vous rencontrez un forain avec une mailloche ! Vous disposez de @[TOOLTIP_TITLE]{3 tours} pour amener {{wobbuffetName}} le plus près possible de @[TOOLTIP_TITLE]{1 PV} pour ainsi charger la Riposte la plus puissante possible, @[TOOLTIP_TITLE]{sans le mettre K.O.} .\nMais attention ! Si {{wobbuffetName}} est mis K.O., vous devrez payer pour le ranimer !", "query": "Voulez-vous jouer ?", "option": { "1": { "label": "Jouer", "tooltip": "(-) Payer {{option1Money, money}}\n(+) Jouer au Ripost-o-matic", - "selected": "Tentez votre chance !" + "selected": "Vous tentez votre chance !" }, "2": { "label": "Partir", @@ -17,14 +17,14 @@ } }, "ko": "Oh non ! Le {{wobbuffetName}} est K.O. !$Vous avez perdu et devez maintenant\npayer pour le ranimer…", - "charging_continue": "Le Qulbutoké charge sa Riposte !", - "turn_remaining_3": "Trois tour restants !", - "turn_remaining_2": "Deux tour restants !", + "charging_continue": "Le Qulbutoké charge\nsa Riposte !", + "turn_remaining_3": "Trois tours restants !", + "turn_remaining_2": "Deux tours restants !", "turn_remaining_1": "Un tour restant !", - "end_game": "Temps écoulé !$Le {{wobbuffetName}} relâche sa Riposte et@d{16}.@d{16}.@d{16}.", - "best_result": "Le {{wobbuffetName}} éclate le bouton si fort que la cloche crève le plafond !$Vous remportez le premier prix !", - "great_result": "Le {{wobbuffetName}} éclate le bouton, et touche quasi la cloche !$Presque !\nVous remportez le deuxième prix !", - "good_result": "Le {{wobbuffetName}} tape le bouton assez fort pour atteindre la moitié !$Vous remportez le troisième prix !", - "bad_result": "Le {{wobbuffetName}} touche à peine le bouton et rien ne se passe…$Oh non!\nVous repartez les mains vides !", + "end_game": "Tous vos tours sont écoulés !$Le {{wobbuffetName}} relâche\nsa Riposte et@d{16}.@d{16}.@d{16}.", + "best_result": "Le {{wobbuffetName}} éclate le bouton si fort\nque la cloche crève le plafond !$Vous remportez le premier prix !", + "great_result": "Le {{wobbuffetName}} éclate le bouton,\net touche presque la cloche !$Mince, à deux doigts !\nVous remportez le deuxième prix !", + "good_result": "Le {{wobbuffetName}} tape le bouton assez fort\npour atteindre la moitié !$Vous remportez le troisième prix !", + "bad_result": "Le {{wobbuffetName}} touche à peine le bouton et rien ne se passe…$Oh non, dommage !\nVous repartez les mains vides !", "outro": "Ouais, ça va, c’était sympa comme jeu !" } diff --git a/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json b/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json index b2bcda7316fb..6caafc3c82d2 100644 --- a/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json +++ b/src/locales/fr/mystery-encounters/global-trade-system-dialogue.json @@ -1,12 +1,12 @@ { "intro": "C’est une interface d’accès à la GTS !", "title": "La GTS", - "description": "Ah, la GTS ! Une révolution technologique permettant de connecter le monde au travers des échanges de Pokémon !\nLa chance va-t-elle vous sourire aujourd’hui ?", + "description": "Ah, la GTS ! Une révolution technologique permettant de connecter le monde au travers des échanges de Pokémon !\n\nLa chance va-t-elle vous sourire aujourd’hui ?", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Consulter les offres", - "tooltip": "(+) Sélectionner une offre d’échange pour un de vos Pokémon.", + "tooltip": "(+) Sélectionner une offre d’échange pour un de vos Pokémon", "trade_options_prompt": "Choisissez un Pokémon à recevoir en échange." }, "2": { @@ -15,14 +15,14 @@ }, "3": { "label": "Échanger un objet", - "trade_options_prompt": "Choisissez un objet à envoyer.", + "trade_options_prompt": "Choisissez l’objet à envoyer.", "invalid_selection": "Ce Pokémon n’a aucun objet légal à échanger.", "tooltip": "(+) Envoyer un de vos objets à la GTS et en recevoir un au hasard" }, "4": { "label": "Partir", "tooltip": "(-) Aucune récompense", - "selected": "Pas le temps pour ça aujourd’hui!\nVous reprenez votre chemin." + "selected": "Pas le temps pour ça aujourd’hui !\nVous reprenez votre chemin." } }, "pokemon_trade_selected": "{{tradedPokemon}} est envoyé\nà {{tradeTrainerName}}.", diff --git a/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json b/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json index f1e6d16264a0..9d74266cdcf8 100644 --- a/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json +++ b/src/locales/fr/mystery-encounters/lost-at-sea-dialogue.json @@ -1,27 +1,27 @@ { - "intro": "Vous errez sans but en pleine mer et n’arrivez nulle part.", + "intro": "Vous errez sans but en pleine mer\net n’arrivez à rien.", "title": "Un cap pas clair", - "description": "La mer n’est pas très clémente dans cette zone et vous commencez à faiblir.\nÇa sent le roussi. Est-il possible de se sortir de cette situation ?", + "description": "La mer n’est pas très clémente dans cette zone et vous vous sentez faiblir à petit feu.\nÇa commence à sentir le roussi.\nEst-il au moins possible de se sortir de cette situation ?", "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "{{option1PrimaryName}} pourrait aider", + "label": "{{option1PrimaryName}} à l’aide", "label_disabled": "{{option1RequiredMove}} impossible", "tooltip": "(+) {{option1PrimaryName}} vous sauve\n(+) {{option1PrimaryName}} gagne un peu d’Exp", "tooltip_disabled": "Vous n’avez aucun Pokémon avec {{option1RequiredMove}}", - "selected": "{{option1PrimaryName}} nage en tête et vous guide.${{option1PrimaryName}} semble également ressorti plus fort de cette épreuve difficile !" + "selected": "{{option1PrimaryName}} nage en tête et vous guide.${{option1PrimaryName}} semble ressorti plus fort de cette épreuve difficile !" }, "2": { - "label": "{{option2PrimaryName}} pourrait aider", + "label": "{{option2PrimaryName}} à l’aide", "label_disabled": "{{option2RequiredMove}} impossible", "tooltip": "(+) {{option2PrimaryName}} vous sauve\n(+) {{option2PrimaryName}} gagne un peu d’Exp", "tooltip_disabled": "Vous n’avez aucun Pokémon avec {{option2RequiredMove}}", - "selected": "{{option2PrimaryName}} vole au dessus de votre embarcation et vous guide.${{option2PrimaryName}} semble également ressorti plus fort de cette épreuve difficile !" + "selected": "{{option2PrimaryName}} vole au dessus de votre embarcation et vous guide.${{option2PrimaryName}} semble ressorti plus fort de cette épreuve difficile !" }, "3": { "label": "Naviguer au jugé", "tooltip": "(-) Votre équipe perd {{damagePercentage}}% de ses PV totaux", - "selected": "Vous vous laissez porter, sans but, quand soudainement vous remarquez un lieu famillier.$Vous et vos Pokémon êtes compètement rincés de fatigue par cette épreuve." + "selected": "Vous vous laissez porter sans but par les vagues, quand soudainement vous remarquez un lieu famillier.$Vous et vos Pokémon êtes compètement rincés\nde fatigue par cette épreuve." } }, "outro": "Vous retrouvez un cap clair." diff --git a/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json b/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json index d607c8ea463c..f43fdfa6db05 100644 --- a/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json +++ b/src/locales/fr/mystery-encounters/mysterious-challengers-dialogue.json @@ -1,11 +1,11 @@ { "intro": "De mystérieux adversaires apparissent !", - "title": "Mystérieux adversaires", - "description": "Si vous défaites un de ces adversaires, vous pourriez peut-être recevoir une récompense si vous les impressionnez. Cependant certains ont l’air coriaces, accepterez-vous le défi ?", + "title": "De mystérieux adversaires", + "description": "Si vous défaites un de ces adversaires, vous pourriez peut-être recevoir une récompense si vous les avez impressionnés.\nCertains ont cependant l’air bien coriaces. Acceptez-vous le défi ?", "query": "Qui voulez-vous affronter ?", "option": { "1": { - "label": "Adversaire clairvoyant", + "label": "Adversaire lucide", "tooltip": "(-) Combat normal\n(+) Objets de capacités" }, "2": { @@ -13,8 +13,8 @@ "tooltip": "(-) Combat difficile\n(+) Bonnes récompenses" }, "3": { - "label": "Adversaire imbattable", - "tooltip": "(-) Combat Brutal\n(+) Super récompenses" + "label": "Adversaire puissant", + "tooltip": "(-) Combat brutal\n(+) Super récompenses" }, "selected": "Votre adversaire s’avance…" }, diff --git a/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json b/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json index bb5b64c3c1ae..6f56d0bb5a2f 100644 --- a/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json +++ b/src/locales/fr/mystery-encounters/mysterious-chest-dialogue.json @@ -1,7 +1,7 @@ { "intro": "Vous tombez sur…@d{32} un coffre ?", "title": "Un étrange coffre", - "description": "Un mangifique coffre orné est posé en travers du chemin. Il doit forcément contenir quelque chose… pas vrai ?", + "description": "Un mangifique coffre orné est posé en travers du chemin.\nIl doit forcément contenir quelque chose… pas vrai ?", "query": "Voulez-vous ouvrir le coffre ?", "option": { "1": { diff --git a/src/locales/fr/mystery-encounters/part-timer-dialogue.json b/src/locales/fr/mystery-encounters/part-timer-dialogue.json index 93b3ee950c1f..4867c42c0ed1 100644 --- a/src/locales/fr/mystery-encounters/part-timer-dialogue.json +++ b/src/locales/fr/mystery-encounters/part-timer-dialogue.json @@ -1,31 +1,31 @@ { - "intro": "Un Ouvrier coulant sous les tâches vous interpelle.", - "speaker": "Ouvrier", - "intro_dialogue": "T’as l’air d’avoir pas mal de Pokémon compétents avec toi !$Si tu acceptes de nous aider pour quelques tâches, on peut te payer !", + "intro": "Un Ouvrière croulant sous le travail\nvous interpelle.", + "speaker": "Ouvrière", + "intro_dialogue": "T’as l’air d’avoir pas mal de Pokémon compétents\navec toi !$Si t’acceptes de nous aider pour quelques tâches,\non peut te payer !", "title": "L’intérimaire", - "description": "La liste de tâches à effectuer a l’air incroyablement longue. En fonction des compétences de vos Pokémon à une tâche donnée, vous gagnerez plus ou moins d’argent.", + "description": "La liste de tâches à effectuer a l’air incroyablement longue.\nEn fonction des compétences du Pokémon choisi à une tâche donnée, vous gagnerez plus ou moins d’argent.", "query": "Quel poste vous intéresse le plus ?", "invalid_selection": "Le Pokémon doit être en bonne santé.", "option": { "1": { "label": "Livreur", - "tooltip": "(-) Votre Pokémon utilise sa Vitesse\n(+) Payé @[MONEY]{Money}", - "selected": "Votre {{selectedPokemon}} passe tout un shift à livrer des clients." + "tooltip": "(-) Votre Pokémon utilise sa Vitesse\n(+) Payé avec un @[MONEY]{salaire}", + "selected": "Votre {{selectedPokemon}} passe tout un shift\nà livrer des clients." }, "2": { "label": "Manutentionnaire", - "tooltip": "(-) Votre Pokémon utilise sa Force et son Endurance\n(+) Payé @[MONEY]{Money}", - "selected": "Votre {{selectedPokemon}} passe tout un shift à porter des charges à travers le hangar." + "tooltip": "(-) Votre Pokémon utilise sa Force et son Endurance\n(+) Payé avec un @[MONEY]{salaire}", + "selected": "Votre {{selectedPokemon}} passe tout un shift\nà porter des charges à travers le hangar." }, "3": { "label": "Assistant vendeur", - "tooltip": "(-) Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Payé @[MONEY]{Money}", + "tooltip": "(-) Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Payé avec un @[MONEY]{salaire}", "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir ce poste", "selected": "Votre {{option3PrimaryName}} passe sa journée à utiliser {{option3PrimaryMove}} pour attirer des clients !" } }, - "job_complete_good": "Merci pour votre aide !\nVotre {{selectedPokemon}} a été d’une grande aide !$Voici votre paye.", - "job_complete_bad": "Votre {{selectedPokemon}} nous a plutot bien aidé !$Voici votre paye.", + "job_complete_good": "Merci pour votre aide !\nVotre {{selectedPokemon}} nous a été d’un grand renfort !$Voici votre salaire.", + "job_complete_bad": "Votre {{selectedPokemon}} nous a plutot bien aidé !$Voici votre salaire.", "pokemon_tired": "Votre {{selectedPokemon}} est épuisé !\nLes PP de toutes ses capacités baissent de 2 !", "outro": "Reviens nous aider à l’occasion !" } diff --git a/src/locales/fr/mystery-encounters/safari-zone-dialogue.json b/src/locales/fr/mystery-encounters/safari-zone-dialogue.json index a572a8a1a48b..24834d128c0f 100644 --- a/src/locales/fr/mystery-encounters/safari-zone-dialogue.json +++ b/src/locales/fr/mystery-encounters/safari-zone-dialogue.json @@ -7,7 +7,7 @@ "1": { "label": "Entrer", "tooltip": "(-) Payer {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Parc Safari}", - "selected": "Tentez votre chance !" + "selected": "Vous tentez votre chance !" }, "2": { "label": "Partir", @@ -23,24 +23,24 @@ }, "2": { "label": "Lancer un appât", - "tooltip": "(+) Augumente le taux de capture\n(-) Augumente le risque de fuite", + "tooltip": "(+) Augmente le taux de capture\n(-) Augmente le risque de fuite", "selected": "Vous lancez quelques appâts !" }, "3": { "label": "Lancer de la boue", "tooltip": "(+) Diminue le risque de fuite\n(-) Diminue le taux de capture", - "selected": "Vous lancez un pe ude boue !" + "selected": "Vous lancez un peu de boue !" }, "4": { "label": "Fuite", "tooltip": "(?) Fuir ce Pokémon" }, - "watching": "{{pokemonName}} vous observe attentivement.", - "eating": "{{pokemonName}} est en train de manger.", - "busy_eating": "{{pokemonName}} se concentre sur sa nourriture.", + "watching": "{{pokemonName}} vous observe\nattentivement.", + "eating": "{{pokemonName}} est en train\nde manger.", + "busy_eating": "{{pokemonName}} se concentre\nsur sa nourriture.", "angry": "{{pokemonName}} se fâche !", - "beside_itself_angry": "{{pokemonName}} laisse exploser sa colère !", - "remaining_count": "Il rest {{remainingCount}} Pokémon !" + "beside_itself_angry": "{{pokemonName}} laisse\nexploser sa colère !", + "remaining_count": "Il reste {{remainingCount}} Pokémon !" }, "outro": "Cette excursion était fun !" } diff --git a/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json b/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json index 61c8185297c4..5c5972d7b1c3 100644 --- a/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json +++ b/src/locales/fr/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -1,27 +1,27 @@ { "intro": "Un homme en manteau noir vous aborde.", "speaker": "Dealer", - "intro_dialogue": ".@d{16}.@d{16}.@d{16}$J’ai de la bonne came, mais seulement si t’as les thunes.$Assure-toi quand même que tes Pokémon puissent encaisser.", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}$J’ai de la bonne came pour toi, mais seulement\nsi t’as les thunes.$Assure-toi quand même que tes Pokémon\npuissent encaisser.", "title": "Le Dealer d’Accélérateurs", - "description": "L’homme ouvre son manteau et vous laisse apercevoir des Accélérateurs pour Pokémon. Le tarif qu’il annonce semble être une bonne affaire. Peut-être même un peu trop…\nIl vous laisse le choix entre deux offres.", - "query": "Que choisissez-vous ?", + "description": "L’homme ouvre son manteau et vous laisse apercevoir des Accélérateurs pour Pokémon. Le tarif qu’il annonce semble être une bonne affaire. Peut-être même un peu trop belle…\nIl vous laisse le choix entre deux offres.", + "query": "Laquelle choisissez-vous ?", "invalid_selection": "Le Pokémon doit être en bonne santé.", "option": { "1": { - "label": "Deal douteux", - "tooltip": "(-) Payer {{option1Money, money}}\n(-) Effets secondaires ?\n(+) Le Pokémon gagne 2 Accélérateurs au hasard" + "label": "Offre douteuse", + "tooltip": "(-) Payer {{option1Money, money}}\n(-) Effets secondaires ?\n(+) Le Pokémon choisi gagne 2 Accélérateurs au hasard" }, "2": { - "label": "Deal honnête", - "tooltip": "(-) Payer {{option2Money, money}}\n(+) le Pokémon choisi 2 Accélérateurs au hasard" + "label": "Offre honnête", + "tooltip": "(-) Payer {{option2Money, money}}\n(+) Le Pokémon choisi gagne 2 Accélérateurs au hasard" }, "3": { "label": "Partir", "tooltip": "(-) Aucune récompense", - "selected": "Eh bien, j’aurais pas cru que tu ferais preuve d’une telle lâcheté." + "selected": "Eh bien, j’aurais pas cru que\ntu ferais preuve d’une telle lâcheté." }, - "selected": "L’homme vous remet deux Accélérateurs et s’évapore.${{selectedPokemon}} reçoit un gain en {{boost1}} et en {{boost2}} !" + "selected": "L’homme vous remet deux Accélérateurs et s’évapore.${{selectedPokemon}} reçoit 1 {{boost1}} et 1 {{boost2}} !" }, - "cheap_side_effects": "Ah ! Des effets secondaires se manifestent !$Votre {{selectedPokemon}} prend des dégâts,\net sa nature se change en {{newNature}} !", + "cheap_side_effects": "Ah !\nDes effets secondaires se manifestent !$Votre {{selectedPokemon}} prend des dégâts,\net sa nature se change en {{newNature}} !", "no_bad_effects": "Tout semble bien se passer, aucun effet indésirable à signaler !" } diff --git a/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json b/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json index b66a797ff878..876bb6be2b2a 100644 --- a/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json +++ b/src/locales/fr/mystery-encounters/slumbering-snorlax-dialogue.json @@ -1,7 +1,7 @@ { - "intro": "Alors que vous vous aventurez dans un passage étroit, une silhouette imposante bloque le passage.$En vous approchant, vous apercevez un {{snorlaxName}}\nendormi paisiblement.$Il semble n’y avoir aucun chemin alternatif.", + "intro": "Alors que vous vous aventurez dans un passage étroit, une silhouette imposante bloque le passage.$En vous approchant, vous apercevez un {{snorlaxName}}\nendormi paisiblement.$Vous regardez autour de vous, mais n’apercevez aucun chemin alternatif.", "title": "{{snorlaxName}} au Bois dormant", - "description": "Vous pourriez soit tenter de le faire bouger en l’attaquant, soit attendre son réveil. Mais impossible de savoir combien de temps ça prendrait…", + "description": "Vous pourriez soit tenter de le faire bouger en l’attaquant, soit attendre son réveil.\nMais impossible de savoir combien de temps cette dernière option prendrait…", "query": "Que voulez-vous faire ?", "option": { "1": { diff --git a/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json b/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json index 84b7b8d3f1fd..7a819f0bb37e 100644 --- a/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json +++ b/src/locales/fr/mystery-encounters/teleporting-hijinks-dialogue.json @@ -1,27 +1,27 @@ { "intro": "Cette machine un peu étrange émet un lourd ronronnement…", - "title": "Teleportating Hijinks", + "title": "Le télé-torpeur", "description": "Vous pouvez lire sur la machine :\n « Notice d’utilisation : Insérez de l’argent et pénétrez dans la capsule. »\n\nPeut-être qu’elle vous emmènera quelque part…", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Insérer de l’argent", - "tooltip": "(-) Payer {{price, money}}\n(?) Teleportation vers un nouveau biome", + "tooltip": "(-) Payer {{price, money}}\n(?) Téléportation vers un nouveau biome", "selected": "Vous insérez un peu d’argent et la capsule s’ouvre.\nVous y pénétrez…" }, "2": { "label": "Un Pokémon à l’aide", - "tooltip": "(-) {{option2PrimaryName}} aide\n(+) Gain d’Exp pour {{option2PrimaryName}}\n(?) Téléportation vers un nouveau biome", + "tooltip": "(-) {{option2PrimaryName}} vous aide\n(+) Gain d’Exp pour {{option2PrimaryName}}\n(?) Téléportation vers un nouveau biome", "disabled_tooltip": "Vous avez besoin d’un Pokémon de type Électrik ou Acier pour choisir cette option", "selected": "Le type de {{option2PrimaryName}} vous permet de frauder la machine !$La capsule s’ouvre et y pénétrez…" }, "3": { "label": "Inspecter la machine", "tooltip": "(-) Combat Pokémon", - "selected": "Tous ces bruits et lumières clignotantes\némis par la machine titillent vos sens…$Mais vous manquez d’apercevoir un Pokémon\nsauvage et vous tend une emuscade !" + "selected": "Tous ces bruits et lumières clignotantes\némis par la machine titillent vos sens…$Mais vous manquez d’apercevoir un Pokémon\nsauvage et qui tend une embuscade !" } }, - "transport": "La machine vibre violemment,\nen émattant toutes sortes de bruits !$À peine la machine lancée, elle revient soudainement très silencieuse.", + "transport": "Elle vibre violemment, émettant\ntoutes sortes de bruits !$À peine la machine lancée, elle redevient\nsubitement très silencieuse…", "attacked": "Vous en ressortez en un lieu complètement nouveau, effrayant au passage un Pokémon sauvage !$Le Pokémon sauvage vous attaque !", "boss_enraged": "Le {{enemyPokemon}} ennemi s’énerve !" } diff --git a/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json b/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json index 6891653ef4d8..401678490fe1 100644 --- a/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json +++ b/src/locales/fr/mystery-encounters/the-pokemon-salesman-dialogue.json @@ -1,18 +1,18 @@ { "intro": "Un homme âgé a l’air malicieux vous aborde.", "speaker": "Gentleman", - "intro_dialogue": "Salutations ! J’ai une offre à proposer juste pour VOUS !", + "intro_dialogue": "Salutations !\nJ’ai une offre à proposer rien qu’à VOUS !", "title": "Le vendeur de Pokémon", - "description": "« Ce {{purchasePokemon}} incroyable et unique possède un talent unique parmi toute son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites vous ? »", - "description_shiny": "« Ce {{purchasePokemon}} incroyable et unique possède une pigmentation unique parmi toute son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites vous ? »", + "description": "« Cet incroyable et unique {{purchasePokemon}} possède un talent jamais vu dans son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites-vous ? »", + "description_shiny": "« Cet incroyable et unique {{purchasePokemon}} possède une pigmentation unique jamais vue dans son espèce ! Ce superbe {{purchasePokemon}} est à vous pour le prix imbattable de {{price, money}} ! »\n\n« Qu’en dites-vous ? »", "query": "Que voulez-vous faire ?", "option": { "1": { "label": "Accepter", "tooltip": "(-) Payer {{price, money}}\n(+) {{purchasePokemon}} obtenu avec son talent caché", "tooltip_shiny": "(-) Payer {{price, money}}\n(+) {{purchasePokemon}} chromatique obtenu", - "selected_message": "Vous payez une somme absolument honteuse pour acheter ce {{purchasePokemon}}.", - "selected_dialogue": "Excellent choix !$Vous avez pour sûr un sens aiguisé des affaires.$Oh, ah euh…@d{64} Ni repris, ni échangé par contre hein, compris ?" + "selected_message": "Vous déboursez une somme absolument honteuse\npour acheter ce {{purchasePokemon}}.", + "selected_dialogue": "Excellent choix !\nVous avez pour sûr un sens aiguisé des affaires.$Oh, ah euh…@d{64}\nNi repris, ni échangé par contre hein, compris ?" }, "2": { "label": "Refuser", diff --git a/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json b/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json index c80443ef8fd0..c9d22039a2e7 100644 --- a/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json +++ b/src/locales/fr/mystery-encounters/the-strong-stuff-dialogue.json @@ -1,21 +1,21 @@ { - "intro": "C’est un {{shuckleName}} absolument énorme et ce qui semble être une grosse réserve de… jus ?", + "intro": "C’est un {{shuckleName}} absolument énorme et\nce qui semble être un énorme bol de… jus ?", "title": "Un breuvage qui arrache", - "description": "Le {{shuckleName}} qui bloque la route semble incroyablement puissant. Le jus qui l’accompagne semble lui émaner une forme étrange de puissance.\n\nLe {{shuckleName}} tend un patte dans votre direction. Il semble vouloir quelque chose…", + "description": "Ce {{shuckleName}} qui bloque la route semble incroyablement puissant.\nLe jus qui l’accompagne semble émaner une étrange forme de puissance.\n\nIl tend une patte dans votre direction, l’air de vouloir quelque chose…", "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "Approcher le {{shuckleName}}", + "label": "Approcher {{shuckleName}}", "tooltip": "(?) Quelque chose de terrible ou d’incroyable peut se produire", - "selected": "Vous perdez connaissance.", - "selected_2": "@f{150}À votre réveil, le {{shuckleName}} est parti\net la réserve de jus complètement vide.${{highBstPokemon1}} et {{highBstPokemon2}}\nsont vicitmes d’une forte léthargie !$Leurs stats de base sont baissées de {{reductionValue}} !$Mais par contre, vos autres Pokémon sont envahis d’une vigueur jamais vue !$Leurs stats de base sont augumentées de {{increaseValue}} !" + "selected": "Vous perdez subitement connaissance.", + "selected_2": "@f{150}À votre réveil, le {{shuckleName}} est parti et\nle bol de jus est complètement vide.${{highBstPokemon1}} et {{highBstPokemon2}}\nsont vicitmes d’une forte léthargie !$Leurs stats de base sont baissées de {{reductionValue}} !$Mais par contre, vos autres Pokémon sont envahis\nd’une vigueur encore jamais vue !$Leurs stats de base sont augmentées de {{increaseValue}} !" }, "2": { - "label": "Affronter le {{shuckleName}}", + "label": "Affronter {{shuckleName}}", "tooltip": "(-) Combat difficile\n(+) Récompense spéciale", "selected": "Le {{shuckleName}} s’énerve, boit un peu de jus et attaque !", - "stat_boost": "Le jus de {{shuckleName}} augumente ses stats !" + "stat_boost": "Le jus de {{shuckleName}} augmente ses stats !" } }, - "outro": "Quelles étrange tournure d’évènements." + "outro": "Quelle étrange tournure des évènements." } diff --git a/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json b/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json index c4a7880c12df..1ab771617c64 100644 --- a/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json +++ b/src/locales/fr/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -1,7 +1,7 @@ { "intro": "C’est une famille dans la cour de leur maison !", "speaker": "La Famille Stratège", - "intro_dialogue": "Nous sommes les Stratège !$Ça te dirait d’affronter la famille dans une série de combats ?", + "intro_dialogue": "Nous sommes les Stratège !$Ça te dirait d’affronter la famille dans une série\nde combats ?", "title": "Le défi de la Famille Stratège", "description": "Les Stratège sont une famille de 5 Dresseurs, et ne demandent qu’à se battre ! Si vous parvenez à tous les battre à la suite, ils vous remettront une grosse récompense. Vous sentez-vous d’encaisser ce défi ?", "query": "Que voulez-vous faire ?", @@ -17,6 +17,6 @@ "selected": "C’est bien dommage. Hé, ton équipe a quand même l’air bien au bout, ça te dirait de rester te reposer un peu ?" } }, - "victory": "Félicitations pour avoir relevé notre défi !$Tout d’abourd, nous aimerions t’offir ce Coupon.", + "victory": "Félicitations pour avoir relevé notre défi !$Tout d’abord, nous aimerions t’offir ce Coupon.", "victory_2": "Mais aussi, notre famille utilise ce Bracelet Macho\npour entrainer plus efficacement nos Pokémon.$Il ne te sera peut-être d’aucune utilité vu que tu nous as tous battus, mais on serait ravis que tu l’accepte !" } diff --git a/src/locales/fr/mystery-encounters/training-session-dialogue.json b/src/locales/fr/mystery-encounters/training-session-dialogue.json index f018018fe4e2..8c10ac13839c 100644 --- a/src/locales/fr/mystery-encounters/training-session-dialogue.json +++ b/src/locales/fr/mystery-encounters/training-session-dialogue.json @@ -1,33 +1,33 @@ { - "intro": "You've come across some\ntraining tools and supplies.", - "title": "Training Session", - "description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", - "query": "How should you train?", - "invalid_selection": "Pokémon must be healthy enough.", + "intro": "Vous tombez sur du matériel d’entrainement.", + "title": "Session d’entrainement", + "description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme en le faisant combattre le reste de votre équipe.", + "query": "Quel entrainement choisir ?", + "invalid_selection": "Le Pokémon doit être en bonne santé.", "option": { "1": { - "label": "Light Training", - "tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its {{stat1}} and {{stat2}} IVs were improved!" + "label": "Léger", + "tooltip": "(-) Combat léger\n(+) Augmente au hasard 2 IV du Pokémon", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Ses IV en {{stat1}} et\nen {{stat2}} augmentent !" }, "2": { - "label": "Moderate Training", - "tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature", - "select_prompt": "Select a new nature\nto train your Pokémon in.", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its nature was changed to {{nature}}!" + "label": "Modéré", + "tooltip": "(-) Combat modéré\n(+) Modifie la nature du Pokémon", + "select_prompt": "Sélectionnez la nature pour laquelle\nvotre Pokémon doit s’entrainer.", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Il a beaucoup changé et\nest devenu {{nature}} !" }, "3": { - "label": "Heavy Training", - "tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability", - "select_prompt": "Select a new ability\nto train your Pokémon in.", - "finished": "{{selectedPokemon}} returns, feeling\nworn out but accomplished!$Its ability was changed to {{ability}}!" + "label": "Intense", + "tooltip": "(-) Combat intense\n(+) Modifie le talent du Pokémon", + "select_prompt": "Sélectionnez le talent pour lequel\nvotre Pokémon doit s’entrainer.", + "finished": "{{selectedPokemon}} revient vers vous,\nl’air fatigué mais fier de lui !$Il a beaucoup changé possède\ndesormais le talent {{ability}} !" }, "4": { - "label": "Leave", - "tooltip": "(-) No Rewards", - "selected": "You've no time for training.\nTime to move on." + "label": "Partir", + "tooltip": "(-) Aucune récompense", + "selected": "Oh la flemme, pas le temps de faire du sport.\nAllons ailleurs." }, - "selected": "{{selectedPokemon}} moves across\nthe clearing to face you..." + "selected": "{{selectedPokemon}} va de l’autre côté\ndu terrain pour vous faire face…" }, - "outro": "That was a successful training session!" -} \ No newline at end of file + "outro": "Cet entrainement a vraiement été très stimulant !" +} diff --git a/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json index ae6e63ed8007..fb39a1d12a1b 100644 --- a/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json +++ b/src/locales/fr/mystery-encounters/trash-to-treasure-dialogue.json @@ -1,19 +1,19 @@ { - "intro": "It's a massive pile of garbage!\nWhere did this come from?", - "title": "Trash to Treasure", - "description": "The garbage heap looms over you, and you can spot some items of value buried amidst the refuse. Are you sure you want to get covered in filth to get them, though?", - "query": "What will you do?", + "intro": "C’est un énorme tas d’ordures !\nComment il est arrivé là ?", + "title": "20 000 lieues sous la mer-", + "description": "Un énorme tas d’ordures se dresse devant vos yeux, dans lequel vous arrivez à entrevoir quelques objets de valeur.\nPar contre, il faut que vous acceptiez l’idée de vous couvrir de saletés pour allez les récupérer…", + "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "Dig for Valuables", - "tooltip": "(-) Lose Healing Items in Shops\n(+) Gain Amazing Items", - "selected": "You wade through the garbage pile, becoming mired in filth.$There's no way any respectable shopkeepers\nwill sell you anything in your grimy state!$You'll just have to make do without shop healing items.$However, you found some incredible items in the garbage!" + "label": "Le fouiller", + "tooltip": "(-) Prix de la boutique triplés\n(+) Gain d’objets exceptionnels", + "selected": "Vous barbotez dans le tas d’ordures et\nvous vous couvrez de crasse.$Vu votre état, la prochaine boutique va pour sûr\nfortement gonfler ses prix pour vous forcer à fuir !$Mais ça valait le coup, car ce que vous avez trouvé\ndans les ordures est incroyable !" }, "2": { - "label": "Investigate Further", - "tooltip": "(?) Find the Source of the Garbage", - "selected": "You wander around the heap, searching for any indication as to how this might have appeared here...", - "selected_2": "Suddenly, the garbage shifts! It wasn't just garbage, it was a Pokémon!" + "label": "Enquêter sur le tas", + "tooltip": "(?) Trouver la source des ordures", + "selected": "Vous vous baladez autour du tas, à la recherche du moindre indice sur la raison de sa présence ici…", + "selected_2": "Oh !\nLes ordures se mettent à bouger !$Le tas est en réalité un Pokémon !\nIl vous attaque !" } } -} \ No newline at end of file +} diff --git a/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json index e6f5b3d3fcd5..f3438bfcee64 100644 --- a/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json +++ b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json @@ -1,26 +1,26 @@ { - "intro": "That isn't just an ordinary Pokémon!", - "title": "Uncommon Breed", - "description": "That {{enemyPokemon}} looks special compared to others of its kind. @[TOOLTIP_TITLE]{Perhaps it knows a special move?} You could battle and catch it outright, but there might also be a way to befriend it.", - "query": "What will you do?", + "intro": "C’est un Pokémon tout ce qu’il y a de plus banal !", + "title": "Une forme peu commune", + "description": "Ce {{enemyPokemon}} a l’air speical par rapport au reste de son espèce. @[TOOLTIP_TITLE]{Peut-être connait-il une capacité particulière ?} Vous pourriez décider de l’affronter sur-le-champ, mais il y a peut-être moyen d’en faire un nouvel ami.", + "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "Battle the Pokémon", - "tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe", - "selected": "You approach the\n{{enemyPokemon}} without fear.", - "stat_boost": "The {{enemyPokemon}}'s heightened abilities boost its stats!" + "label": "L’affronter", + "tooltip": "(-) Combat compliqué\n(+) Capturer un adversaire puissant", + "selected": "Vous approchez\nle Pokémon sans frémir.", + "stat_boost": "Les caractéristiques particulières\nde ce {{enemyPokemon}} augmentent ses stats !" }, "2": { - "label": "Give It Food", - "disabled_tooltip": "You need 4 berry items to choose this", - "tooltip": "(-) Give 4 Berries\n(+) The {{enemyPokemon}} Likes You", - "selected": "You toss the berries at the {{enemyPokemon}}!$It eats them happily!$The {{enemyPokemon}} wants to join your party!" + "label": "Le nourrir", + "disabled_tooltip": "Vous avez besoin de 4 Baies pour chosir cette option", + "tooltip": "(-) Donner 4 Baies\n(+) Le {{enemyPokemon}} vous apprécie", + "selected": "Vous lancer quelques Baies\nau {{enemyPokemon}} !$Il les englouit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !" }, "3": { - "label": "Befriend It", - "disabled_tooltip": "Your Pokémon need to know certain moves to choose this", - "tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) The {{enemyPokemon}} Likes You", - "selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}} to charm the {{enemyPokemon}}!$The {{enemyPokemon}} wants to join your party!" + "label": "Devenir amis", + "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option", + "tooltip": "(+) {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Le {{enemyPokemon}} vous apprécie", + "selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$The {{enemyPokemon}} veut se joindre\nà votre équipe !" } } -} \ No newline at end of file +} diff --git a/src/locales/fr/mystery-encounters/weird-dream-dialogue.json b/src/locales/fr/mystery-encounters/weird-dream-dialogue.json index 44acde84002c..87f6006d8b14 100644 --- a/src/locales/fr/mystery-encounters/weird-dream-dialogue.json +++ b/src/locales/fr/mystery-encounters/weird-dream-dialogue.json @@ -1,22 +1,22 @@ { - "intro": "A shadowy woman blocks your path.\nSomething about her is unsettling...", - "speaker": "Woman", - "intro_dialogue": "I have seen your futures, your pasts...$Child, do you see them too?", + "intro": "Une femme aux airs sombres vous coupe la route.\nElle dégage comme une aura troublante…", + "speaker": "Femme mystérieuse", + "intro_dialogue": "J’ai tout vu…\nTes futurs, tes passés…$Les perçois-tu aussi, mon enfant ?", "title": "???", - "description": "The woman's words echo in your head. It wasn't just a singular voice, but a vast multitude, from all timelines and realities. You begin to feel dizzy, the question lingering on your mind...\n\n@[TOOLTIP_TITLE]{\"I have seen your futures, your pasts... Child, do you see them too?\"}", - "query": "What will you do?", + "description": "Les paroles de cette mystérieuse femme raisonnent dans votre tête. Ce n’est pas qu’une simple voix isolée, mais une infinité, d’une infinité d’espace-temps et de réalités.\nVous commencez ressentir des vertiges à mesure que cette question s’empare de votre esprit…\n\n@[TOOLTIP_TITLE]{« J’ai tout vu… Tes futurs, tes passés… Les perçois-tu aussi, mon enfant ? »}", + "query": "Que voulez-vous faire ?", "option": { "1": { - "label": "\"I See Them\"", - "tooltip": "@[SUMMARY_GREEN]{(?) Affects your Pokémon}", - "selected": "Her hand reaches out to touch you,\nand everything goes black.$Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.$Everything that has made you,\neverything you will become...@d{64}", - "cutscene": "You see your Pokémon,@d{32} converging from\nevery reality to become something new...@d{64}", - "dream_complete": "When you awaken, the woman - was it a woman or a ghost? - is gone...$.@d{32}.@d{32}.@d{32}$Your Pokémon team has changed...\nOr is it the same team you've always had?" + "label": "« Je les vois. »", + "tooltip": "@[SUMMARY_GREEN]{(?) Affecte vos Pokémon}", + "selected": "Sa main s’approche de vous et vous touche.\nTout devient subitement sombre.$Puis enfin…@d{64} La lumière.\nChaque espace-temps. Chacune de vos incarnations.$Tout ce qui a composé, compose\net composersa votre être…@d{64}", + "cutscene": "Vous percevez vos Pokémon,@d{32} convergeant de chaque\nréalité pour former quelque chose de nouveau…@d{64}", + "dream_complete": "Vous vous réveillez, mais la femme a disparu…\nOu bien n’etait-elle qu’une hallucination ?$.@d{32}.@d{32}.@d{32}$Les Pokémon de votre équipe ont changé…$Mais alors, comment peuvent-ils quand même\ntoujours vous sembler si familiers ?" }, "2": { - "label": "Quickly Leave", - "tooltip": "(-) Affects your Pokémon", - "selected": "You tear your mind from a numbing grip, and hastily depart.$When you finally stop to collect yourself, you check the Pokémon in your team.$For some reason, all of their levels have decreased!" + "label": "Partir en courant", + "tooltip": "(-) Affecte vos Pokémon", + "selected": "Vous parvenez à vous extraire de son emprise\net prenez vos jambes à votre cou.$Vous vous remettez de vos émotions et\nvérifiez si votre équipe va bien.$Mais pour une raison mystérieuse,\nils ont tous perdu des niveaux !" } } -} \ No newline at end of file +} diff --git a/src/locales/fr/party-ui-handler.json b/src/locales/fr/party-ui-handler.json index 3b263157695b..5491551e4a1a 100644 --- a/src/locales/fr/party-ui-handler.json +++ b/src/locales/fr/party-ui-handler.json @@ -13,6 +13,7 @@ "ALL": "Tout", "PASS_BATON": "Relais", "UNPAUSE_EVOLUTION": "Réactiver Évolution", + "PAUSE_EVOLUTION": "Interrompre Évolution", "REVIVE": "Ranimer", "RENAME": "Renommer", "SELECT": "Sélectionner", @@ -24,6 +25,7 @@ "tooManyItems": "{{pokemonName}} porte trop\nd’exemplaires de cet objet !", "anyEffect": "Cela n’aura aucun effet.", "unpausedEvolutions": "{{pokemonName}} peut de nouveau évoluer.", + "pausedEvolutions": "{{pokemonName}} ne peut plus évoluer.", "unspliceConfirmation": "Voulez-vous vraiment séparer {{fusionName}}\nde {{pokemonName}} ? {{fusionName}} sera perdu.", "wasReverted": "{{fusionName}} est redevenu {{pokemonName}}.", "releaseConfirmation": "Voulez-vous relâcher {{pokemonName}} ?", diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index c752b336b6ee..e310f5d57330 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "Vit. barre d’Exp", "expPartyDisplay": "Afficher Exp équipe", "skipSeenDialogues": "Passer dialogues connus", + "eggSkip": "Animation d’éclosion", + "never": "Jamais", + "always": "Toujours", + "ask": "Demander", "battleStyle": "Style de combat", "enableRetries": "Activer les réessais", "hideIvs": "Masquer Scanner d’IV", diff --git a/src/locales/fr/trainer-names.json b/src/locales/fr/trainer-names.json index 4175ef8c63fc..08f5485901b5 100644 --- a/src/locales/fr/trainer-names.json +++ b/src/locales/fr/trainer-names.json @@ -94,7 +94,7 @@ "caitlin": "Percila", "malva": "Malva", "siebold": "Narcisse", - "wikstrom": "Tileo", + "wikstrom": "Thyméo", "drasna": "Dracéna", "hala": "Pectorius", "molayne": "Molène", diff --git a/src/locales/fr/tutorial.json b/src/locales/fr/tutorial.json index 7936987457f5..3236bdafea2b 100644 --- a/src/locales/fr/tutorial.json +++ b/src/locales/fr/tutorial.json @@ -6,5 +6,5 @@ "pokerus": "Chaque jour, 3 starters tirés aléatoirement ont un contour violet.\n$Si un starter que vous possédez l’a, essayez de l’ajouter à votre équipe. Vérifiez bien son résumé !", "statChange": "Les changements de stats persistent à travers\nles combats tant que le Pokémon n’est pas rappelé.\n$Vos Pokémon sont rappelés avant un combat de\nDresseur et avant d’entrer dans un nouveau biome.\n$Vous pouvez voir en combat les changements de stats\nd’un Pokémon en maintenant C ou Maj.\n$Vous pouvez également voir les capacités de l’adversaire\nen maintenant V.\n$Seules les capacités que le Pokémon a utilisées dans\nce combat sont consultables.", "selectItem": "Après chaque combat, vous avez le choix entre 3 objets\ntirés au sort. Vous ne pouvez en prendre qu’un.\n$Cela peut être des objets consommables, des objets à\nfaire tenir, ou des objets passifs aux effets permanents.\n$La plupart des effets des objets non-consommables se cumuleront de diverses manières.\n$Certains objets n’apparaitront que s’ils ont une utilité immédiate, comme les objets d’évolution.\n$Vous pouvez aussi transférer des objets tenus entre\nPokémon en utilisant l’option de transfert.\n$L’option de transfert apparait en bas à droite dès\nqu’un Pokémon de l’équipe porte un objet.\n$Vous pouvez acheter des consommables avec de\nl’argent. Plus vous progressez, plus le choix sera large.\n$Choisir un des objets gratuits déclenchera le prochain\ncombat, donc faites bien tous vos achats avant.", - "eggGacha": "Depuis cet écran, vous pouvez utiliser vos coupons\npour recevoir Œufs de Pokémon au hasard.\n$Les Œufs éclosent après avoir remporté un certain nombre de combats. Plus ils sont rares, plus ils mettent de temps.\n$Les Pokémon éclos ne rejoindront pas votre équipe, mais seront ajoutés à vos starters.\n$Les Pokémon issus d’Œufs ont généralement de meilleurs IV que les Pokémon sauvages.\n$Certains Pokémon ne peuvent être obtenus que dans des Œufs.\n$Il y a 3 différentes machines à actionner avec différents\nbonus, prenez celle qui vous convient le mieux !" + "eggGacha": "Depuis cet écran, vous pouvez utiliser vos coupons\npour recevoir Œufs de Pokémon au hasard.\n$Les Œufs éclosent après avoir remporté un certain nombre de combats.\n$Plus ils sont rares, plus ils mettent de temps.\n$Les Pokémon éclos ne rejoindront pas votre équipe, mais seront ajoutés à vos starters.\n$Les Pokémon issus d’Œufs ont généralement de meilleurs IV que les Pokémon sauvages.\n$Certains Pokémon ne peuvent être obtenus que dans des Œufs.\n$Il y a 3 différentes machines à actionner avec différents\nbonus, prenez celle qui vous convient le mieux !" } diff --git a/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..6f2a39306e15 100644 --- a/src/locales/it/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/it/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?", @@ -23,4 +23,4 @@ "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." } } -} \ No newline at end of file +} diff --git a/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..6f2a39306e15 100644 --- a/src/locales/ja/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/ja/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?", @@ -23,4 +23,4 @@ "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." } } -} \ No newline at end of file +} diff --git a/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..6f2a39306e15 100644 --- a/src/locales/ko/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/ko/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?", @@ -23,4 +23,4 @@ "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." } } -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..6f2a39306e15 100644 --- a/src/locales/pt_BR/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/pt_BR/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?", @@ -23,4 +23,4 @@ "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." } } -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json b/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json index 6dd54d302abe..6f2a39306e15 100644 --- a/src/locales/zh_TW/mystery-encounters/an-offer-you-cant-refuse-dialogue.json +++ b/src/locales/zh_TW/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?", @@ -23,4 +23,4 @@ "selected": "What a rotten day...$Ah, well. Let's return to the yacht club then, {{liepardName}}." } } -} \ No newline at end of file +} diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 48c0d66fc45f..a23a9c5ece2f 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1,6 +1,7 @@ import * as Modifiers from "./modifier"; -import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move"; -import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball"; +import { MoneyMultiplierModifier } from "./modifier"; +import { allMoves, AttackMove, selfStatLowerMoves } from "../data/move"; +import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon"; import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions"; import { tmPoolTiers, tmSpecies } from "../data/tms"; @@ -9,17 +10,16 @@ import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from ".. import * as Utils from "../utils"; import { getBerryEffectDescription, getBerryName } from "../data/berry"; import { Unlockables } from "../system/unlockables"; -import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect"; +import { getStatusEffectDescriptor, StatusEffect } from "../data/status-effect"; import { SpeciesFormKey } from "../data/pokemon-species"; import BattleScene from "../battle-scene"; -import { VoucherType, getVoucherTypeIcon, getVoucherTypeName } from "../system/voucher"; -import { FormChangeItem, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger, pokemonFormChanges } from "../data/pokemon-forms"; +import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "../system/voucher"; +import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { ModifierTier } from "./modifier-tier"; -import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature"; +import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature"; import i18next from "i18next"; import { getModifierTierTextTint } from "#app/ui/text"; import Overrides from "#app/overrides"; -import { MoneyMultiplierModifier } from "./modifier"; import { Abilities } from "#enums/abilities"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -109,28 +109,31 @@ export class ModifierType { return null; } + /** + * Populates item id for ModifierType instance + * @param func + */ withIdFromFunc(func: ModifierTypeFunc): ModifierType { this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func)!; // TODO: is this bang correct? return this; } /** - * Populates the tier field by performing a reverse lookup on the modifier pool specified by {@linkcode poolType} using the - * {@linkcode ModifierType}'s id. - * @param poolType the {@linkcode ModifierPoolType} to look into to derive the item's tier; defaults to {@linkcode ModifierPoolType.PLAYER} + * Populates item tier for ModifierType instance + * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) + * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * @param poolType Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from */ withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { for (const tier of Object.values(getModifierPoolForType(poolType))) { for (const modifier of tier) { if (this.id === modifier.modifierType.id) { this.tier = modifier.modifierType.tier; - break; + return this; } } - if (this.tier) { - break; - } } + return this; } @@ -644,6 +647,55 @@ export class BaseStatBoosterModifierType extends PokemonHeldItemModifierType imp } } +/** + * Shuckle Juice item + */ +export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { + private readonly statModifier: integer; + + constructor(statModifier: integer) { + super("modifierType:ModifierType.MYSTERY_ENCOUNTER_SHUCKLE_JUICE", "berry_juice", (_type, args) => new Modifiers.PokemonBaseStatTotalModifier(this, (args[0] as Pokemon).id, this.statModifier)); + this.statModifier = statModifier; + } + + override getDescription(scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatTotalModifierType.description", { + increaseDecrease: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.increase" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.decrease"), + blessCurse: i18next.t(this.statModifier >= 0 ? "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.blessed" : "modifierType:ModifierType.PokemonBaseStatTotalModifierType.extra.cursed"), + statValue: this.statModifier, + }); + } + + public getPregenArgs(): any[] { + return [ this.statModifier ]; + } +} + +/** + * Old Gateau item + */ +export class PokemonBaseStatFlatModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { + private readonly statModifier: integer; + private readonly stats: Stat[]; + + constructor(statModifier: integer, stats: Stat[]) { + super("modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", "old_gateau", (_type, args) => new Modifiers.PokemonBaseStatFlatModifier(this, (args[0] as Pokemon).id, this.statModifier, this.stats)); + this.statModifier = statModifier; + this.stats = stats; + } + + override getDescription(scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { + stats: this.stats.map(stat => i18next.t(getStatKey(stat))).join("/"), + statValue: this.statModifier, + }); + } + + public getPregenArgs(): any[] { + return [ this.statModifier, this.stats ]; + } +} + class AllPokemonFullHpRestoreModifierType extends ModifierType { private descriptionKey: string; @@ -1057,11 +1109,11 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { } const evolutionItemPool = [ - party.filter(p => pokemonEvolutions.hasOwnProperty(p.species.speciesId)).map(p => { + party.filter(p => pokemonEvolutions.hasOwnProperty(p.species.speciesId) && (!p.pauseEvolutions || p.species.speciesId === Species.SLOWPOKE || p.species.speciesId === Species.EEVEE)).map(p => { const evolutions = pokemonEvolutions[p.species.speciesId]; return evolutions.filter(e => e.item !== EvolutionItem.NONE && (e.evoFormKey === null || (e.preFormKey || "") === p.getFormKey()) && (!e.condition || e.condition.predicate(p))); }).flat(), - party.filter(p => p.isFusion() && p.fusionSpecies && pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId)).map(p => { + party.filter(p => p.isFusion() && p.fusionSpecies && pokemonEvolutions.hasOwnProperty(p.fusionSpecies.speciesId) && (!p.pauseEvolutions || p.fusionSpecies.speciesId === Species.SLOWPOKE || p.fusionSpecies.speciesId === Species.EEVEE)).map(p => { const evolutions = pokemonEvolutions[p.fusionSpecies!.speciesId]; return evolutions.filter(e => e.item !== EvolutionItem.NONE && (e.evoFormKey === null || (e.preFormKey || "") === p.getFusionFormKey()) && (!e.condition || e.condition.predicate(p))); }).flat() @@ -1320,6 +1372,8 @@ export const modifierTypes = { FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(false), RARE_FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(true), + EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold", (type, _args) => new Modifiers.EvoTrackerModifier(type, (_args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)), + MEGA_BRACELET: () => new ModifierType("modifierType:ModifierType.MEGA_BRACELET", "mega_bracelet", (type, _args) => new Modifiers.MegaEvolutionAccessModifier(type)), DYNAMAX_BAND: () => new ModifierType("modifierType:ModifierType.DYNAMAX_BAND", "dynamax_band", (type, _args) => new Modifiers.GigantamaxAccessModifier(type)), TERA_ORB: () => new ModifierType("modifierType:ModifierType.TERA_ORB", "tera_orb", (type, _args) => new Modifiers.TerastallizeAccessModifier(type)), @@ -1505,6 +1559,22 @@ export const modifierTypes = { ENEMY_STATUS_EFFECT_HEAL_CHANCE: () => new ModifierType("modifierType:ModifierType.ENEMY_STATUS_EFFECT_HEAL_CHANCE", "wl_full_heal", (type, _args) => new Modifiers.EnemyStatusEffectHealChanceModifier(type, 2.5, 10)), ENEMY_ENDURE_CHANCE: () => new EnemyEndureChanceModifierType("modifierType:ModifierType.ENEMY_ENDURE_CHANCE", "wl_reset_urge", 2), ENEMY_FUSED_CHANCE: () => new ModifierType("modifierType:ModifierType.ENEMY_FUSED_CHANCE", "wl_custom_spliced", (type, _args) => new Modifiers.EnemyFusionChanceModifier(type, 1)), + + MYSTERY_ENCOUNTER_SHUCKLE_JUICE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new PokemonBaseStatTotalModifierType(pregenArgs[0] as integer); + } + return new PokemonBaseStatTotalModifierType(Utils.randSeedInt(20)); + }), + MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { + if (pregenArgs) { + return new PokemonBaseStatFlatModifierType(pregenArgs[0] as integer, pregenArgs[1] as Stat[]); + } + return new PokemonBaseStatFlatModifierType(Utils.randSeedInt(20), [Stat.HP, Stat.ATK, Stat.DEF]); + }), + MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", "black_sludge", (type, _args) => new Modifiers.HealShopCostModifier(type)), + MYSTERY_ENCOUNTER_MACHO_BRACE: () => new PokemonHeldItemModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_MACHO_BRACE", "macho_brace", (type, args) => new Modifiers.PokemonIncrementingStatModifier(type, (args[0] as Pokemon).id)), + MYSTERY_ENCOUNTER_GOLDEN_BUG_NET: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET", "golden_net", (type, _args) => new Modifiers.BoostBugSpawnModifier(type)), }; interface ModifierPool { @@ -1545,7 +1615,6 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.TEMP_STAT_STAGE_BOOSTER, 4), new WeightedModifierType(modifierTypes.BERRY, 2), new WeightedModifierType(modifierTypes.TM_COMMON, 2), - new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.COMMON); return m; }), @@ -1616,7 +1685,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BASE_STAT_BOOSTER, 3), new WeightedModifierType(modifierTypes.TERA_SHARD, 1), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 4 : 0), - new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 3, 0) : 0, 3), + new WeightedModifierType(modifierTypes.VOUCHER, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(1 - rerollCount, 0) : 0, 1), ].map(m => { m.setTier(ModifierTier.GREAT); return m; }), @@ -1697,7 +1766,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.RARE_FORM_CHANGE_ITEM, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 6, 24), new WeightedModifierType(modifierTypes.MEGA_BRACELET, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), new WeightedModifierType(modifierTypes.DYNAMAX_BAND, (party: Pokemon[]) => Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 50), 4) * 9, 36), - new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(9 - rerollCount * 3, 0) : 0, 9), + new WeightedModifierType(modifierTypes.VOUCHER_PLUS, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily ? Math.max(3 - rerollCount * 1, 0) : 0, 3), ].map(m => { m.setTier(ModifierTier.ROGUE); return m; }), @@ -1706,7 +1775,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.SHINY_CHARM, 14), new WeightedModifierType(modifierTypes.HEALING_CHARM, 18), new WeightedModifierType(modifierTypes.MULTI_LENS, 18), - new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(15 - rerollCount * 5, 0) : 0, 15), + new WeightedModifierType(modifierTypes.VOUCHER_PREMIUM, (party: Pokemon[], rerollCount: integer) => !party[0].scene.gameMode.isDaily && !party[0].scene.gameMode.isEndless && !party[0].scene.gameMode.isSplicedOnly ? Math.max(5 - rerollCount * 2, 0) : 0, 5), new WeightedModifierType(modifierTypes.DNA_SPLICERS, (party: Pokemon[]) => !party[0].scene.gameMode.isSplicedOnly && party.filter(p => !p.fusionSpecies).length > 1 ? 24 : 0, 24), new WeightedModifierType(modifierTypes.MINI_BLACK_HOLE, (party: Pokemon[]) => (!party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) ? 1 : 0, 1), ].map(m => { @@ -1975,29 +2044,107 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod } } +export interface CustomModifierSettings { + guaranteedModifierTiers?: ModifierTier[]; + guaranteedModifierTypeOptions?: ModifierTypeOption[]; + guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; + fillRemaining?: boolean; + /** Set to negative value to disable rerolls completely in shop */ + rerollMultiplier?: number; + allowLuckUpgrades?: boolean; +} + export function getModifierTypeFuncById(id: string): ModifierTypeFunc { return modifierTypes[id]; } -export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[]): ModifierTypeOption[] { +/** + * Generates modifier options for a {@linkcode SelectModifierPhase} + * @param count Determines the number of items to generate + * @param party Party is required for generating proper modifier pools + * @param modifierTiers (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customModifierSettings (Optional) If specified, can customize the item shop rewards further. + * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` If specified, will override the first X items to be specific modifier options (these should be pre-genned). + * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). + * - `guaranteedModifierTiers?: ModifierTier[]` If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `fillRemaining?: boolean` Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. + * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). + * - `rerollMultiplier?: number` If specified, can adjust the amount of money required for a shop reroll. If set to a negative value, the shop will not allow rerolls at all. + * - `allowLuckUpgrades?: boolean` Default `true`, if `false` will prevent set item tiers from upgrading via luck + */ +export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; const retryCount = Math.min(count * 5, 50); - new Array(count).fill(0).map((_, i) => { - let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined); - let r = 0; - while (options.length && ++r < retryCount && options.filter(o => o.type?.name === candidate?.type?.name || o.type?.group === candidate?.type?.group).length) { - candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type?.tier, candidate?.upgradeCount); + if (!customModifierSettings) { + new Array(count).fill(0).map((_, i) => { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers && modifierTiers.length > i ? modifierTiers[i] : undefined)); + }); + } else { + // Guaranteed mod options first + if (customModifierSettings?.guaranteedModifierTypeOptions && customModifierSettings.guaranteedModifierTypeOptions.length > 0) { + options.push(...customModifierSettings.guaranteedModifierTypeOptions!); + } + + // Guaranteed mod functions second + if (customModifierSettings.guaranteedModifierTypeFuncs && customModifierSettings.guaranteedModifierTypeFuncs.length > 0) { + customModifierSettings.guaranteedModifierTypeFuncs!.forEach((mod, i) => { + const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod) as string; + let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); + + // Populates item id and tier + guaranteedMod = guaranteedMod + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); + + const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; + if (modType) { + const option = new ModifierTypeOption(modType, 0); + options.push(option); + } + }); + } + + // Guaranteed tiers third + if (customModifierSettings.guaranteedModifierTiers && customModifierSettings.guaranteedModifierTiers.length > 0) { + const allowLuckUpgrades = customModifierSettings.allowLuckUpgrades ?? true; + customModifierSettings.guaranteedModifierTiers.forEach((tier) => { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier, allowLuckUpgrades)); + }); } - if (candidate) { - options.push(candidate); + + // Fill remaining + if (options.length < count && customModifierSettings.fillRemaining) { + while (options.length < count) { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, undefined)); + } } - }); + } overridePlayerModifierTypeOptions(options, party); return options; } +/** + * Will generate a {@linkcode ModifierType} from the {@linkcode ModifierPoolType.PLAYER} pool, attempting to retry duplicated items up to retryCount + * @param existingOptions Currently generated options + * @param retryCount How many times to retry before allowing a dupe item + * @param party Current player party, used to calculate items in the pool + * @param tier If specified will generate item of tier + * @param allowLuckUpgrades `true` to allow items to upgrade tiers (the little animation that plays and is affected by luck) + */ +function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption { + allowLuckUpgrades = allowLuckUpgrades ?? true; + let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades); + let r = 0; + while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate?.type.name || o.type.group === candidate?.type.group).length) { + candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate?.type.tier ?? tier, candidate?.upgradeCount, 0, allowLuckUpgrades); + } + return candidate!; +} + /** * Replaces the {@linkcode ModifierType} of the entries within {@linkcode options} with any * {@linkcode ModifierOverride} entries listed in {@linkcode Overrides.ITEM_REWARD_OVERRIDE} @@ -2123,7 +2270,16 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): Modifiers.P return ret; } -function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0): ModifierTypeOption | null { +/** + * Generates a ModifierType from the specified pool + * @param party party of the trainer using the item + * @param poolType PLAYER/WILD/TRAINER + * @param tier If specified, will override the initial tier of an item (can still upgrade with luck) + * @param upgradeCount If defined, means that this is a new ModifierType being generated to override another via luck upgrade. Used for recursive logic + * @param retryCount Max allowed tries before the next tier down is checked for a valid ModifierType + * @param allowLuckUpgrades Default true. If false, will not allow ModifierType to randomly upgrade to next tier + */ +function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption | null { const player = !poolType; const pool = getModifierPoolForType(poolType); let thresholds: object; @@ -2149,7 +2305,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, if (!upgradeCount) { upgradeCount = 0; } - if (player && tierValue) { + if (player && tierValue && allowLuckUpgrades) { const partyLuckValue = getPartyLuckValue(party); const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4)); let upgraded = false; @@ -2182,7 +2338,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, } } else if (upgradeCount === undefined && player) { upgradeCount = 0; - if (tier < ModifierTier.MASTER) { + if (tier < ModifierTier.MASTER && allowLuckUpgrades) { const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length; const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2)); while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 4b3d0a852800..0c4d2a63802f 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -29,7 +29,6 @@ import { Abilities } from "#app/enums/abilities"; import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LevelUpPhase } from "#app/phases/level-up-phase"; import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase"; -import { SpeciesFormKey } from "#app/data/pokemon-species"; export type ModifierPredicate = (modifier: Modifier) => boolean; @@ -841,6 +840,192 @@ export class BaseStatModifier extends PokemonHeldItemModifier { } } +export class EvoTrackerModifier extends PokemonHeldItemModifier { + protected species: Species; + protected required: integer; + readonly isTransferrable: boolean = false; + + constructor(type: ModifierType, pokemonId: integer, species: Species, required: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + this.species = species; + this.required = required; + } + + matchType(modifier: Modifier): boolean { + if (modifier instanceof EvoTrackerModifier) { + return (modifier as EvoTrackerModifier).species === this.species; + } + return false; + } + + clone(): PersistentModifier { + return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.species); + } + + apply(args: any[]): boolean { + return true; + } + + getMaxHeldItemCount(_pokemon: Pokemon): integer { + return this.required; + } +} + +/** + * Currently used by Shuckle Juice item + */ +export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier { + private statModifier: integer; + readonly isTransferrable: boolean = false; + + constructor(type: ModifierTypes.PokemonBaseStatTotalModifierType, pokemonId: integer, statModifier: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + this.statModifier = statModifier; + } + + override matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatTotalModifier; + } + + override clone(): PersistentModifier { + return new PokemonBaseStatTotalModifier(this.type as ModifierTypes.PokemonBaseStatTotalModifierType, this.pokemonId, this.statModifier, this.stackCount); + } + + override getArgs(): any[] { + return super.getArgs().concat(this.statModifier); + } + + override shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + override apply(args: any[]): boolean { + // Modifies the passed in baseStats[] array + args[1].forEach((v, i) => { + // HP is affected by half as much as other stats + const newVal = i === 0 ? Math.floor(v + this.statModifier / 2) : Math.floor(v + this.statModifier); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.2; + } + + override getMaxHeldItemCount(pokemon: Pokemon): integer { + return 2; + } +} + +/** + * Currently used by Old Gateau item + */ +export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier { + private statModifier: integer; + private stats: Stat[]; + readonly isTransferrable: boolean = false; + + constructor (type: ModifierType, pokemonId: integer, statModifier: integer, stats: Stat[], stackCount?: integer) { + super(type, pokemonId, stackCount); + + this.statModifier = statModifier; + this.stats = stats; + } + + override matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatFlatModifier; + } + + override clone(): PersistentModifier { + return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); + } + + override getArgs(): any[] { + return super.getArgs().concat(this.statModifier, this.stats); + } + + override shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + override apply(args: any[]): boolean { + // Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats + args[1].forEach((v, i) => { + if (this.stats.includes(i)) { + const newVal = Math.floor(v + this.statModifier); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + } + }); + + return true; + } + + override getScoreMultiplier(): number { + return 1.1; + } + + override getMaxHeldItemCount(pokemon: Pokemon): integer { + return 1; + } +} + +/** + * Currently used by Macho Brace item + */ +export class PokemonIncrementingStatModifier extends PokemonHeldItemModifier { + readonly isTransferrable: boolean = false; + + constructor (type: ModifierType, pokemonId: integer, stackCount?: integer) { + super(type, pokemonId, stackCount); + } + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonIncrementingStatModifier; + } + + clone(): PersistentModifier { + return new PokemonIncrementingStatModifier(this.type, this.pokemonId); + } + + getArgs(): any[] { + return super.getArgs(); + } + + shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + apply(args: any[]): boolean { + // Modifies the passed in stats[] array by +1 per stack for HP, +2 per stack for other stats + // If the Macho Brace is at max stacks (50), adds additional 5% to total HP and 10% to other stats + args[1].forEach((v, i) => { + const isHp = i === 0; + let mult = 1; + if (this.stackCount === this.getMaxHeldItemCount()) { + mult = isHp ? 1.05 : 1.1; + } + const newVal = Math.floor((v + this.stackCount * (isHp ? 1 : 2)) * mult); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + getScoreMultiplier(): number { + return 1.2; + } + + getMaxHeldItemCount(pokemon?: Pokemon): integer { + return 50; + } +} + /** * Modifier used for held items that apply {@linkcode Stat} boost(s) * using a multiplier. @@ -935,7 +1120,7 @@ export class EvolutionStatBoosterModifier extends StatBoosterModifier { * @returns true if the stat boosts can be applied, false otherwise */ shouldApply(args: any[]): boolean { - return super.shouldApply(args) && ((args[0] as Pokemon).getFormKey() !== SpeciesFormKey.GIGANTAMAX); + return super.shouldApply(args) && !(args[0] as Pokemon).isMax(); } /** @@ -1122,7 +1307,7 @@ export class SpeciesCritBoosterModifier extends CritBoosterModifier { * Applies Specific Type item boosts (e.g., Magnet) */ export class AttackTypeBoosterModifier extends PokemonHeldItemModifier { - private moveType: Type; + public moveType: Type; private boostMultiplier: number; constructor(type: ModifierType, pokemonId: integer, moveType: Type, boostPercent: number, stackCount?: integer) { @@ -2222,6 +2407,15 @@ export class MoneyRewardModifier extends ConsumableModifier { scene.addMoney(moneyAmount.value); + scene.getParty().map(p => { + if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { + p.evoCounter++; + const modifierType: ModifierType = modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL(); + const modifier = modifierType!.newModifier(p); + scene.addModifier(modifier); + } + }); + return true; } } @@ -2378,6 +2572,55 @@ export class LockModifierTiersModifier extends PersistentModifier { } } +/** + * Black Sludge item + */ +export class HealShopCostModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: integer) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof HealShopCostModifier; + } + + clone(): HealShopCostModifier { + return new HealShopCostModifier(this.type, this.stackCount); + } + + apply(args: any[]): boolean { + (args[0] as Utils.IntegerHolder).value *= Math.pow(3, this.getStackCount()); + + return true; + } + + getMaxStackCount(scene: BattleScene): integer { + return 1; + } +} + +export class BoostBugSpawnModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: integer) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof BoostBugSpawnModifier; + } + + clone(): HealShopCostModifier { + return new BoostBugSpawnModifier(this.type, this.stackCount); + } + + apply(args: any[]): boolean { + return true; + } + + getMaxStackCount(scene: BattleScene): integer { + return 1; + } +} + export class SwitchEffectTransferModifier extends PokemonHeldItemModifier { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { super(type, pokemonId, stackCount); diff --git a/src/overrides.ts b/src/overrides.ts index d1597dfdee86..6b550d152c21 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -10,9 +10,10 @@ import { VariantTier } from "#enums/variant-tiers"; import { WeatherType } from "#enums/weather-type"; import { type PokeballCounts } from "./battle-scene"; import { Gender } from "./data/gender"; -import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars import { Variant } from "./data/variant"; import { type ModifierOverride } from "./modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Overrides that are using when testing different in game situations @@ -135,6 +136,15 @@ class DefaultOverrides { readonly EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false; readonly EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; + // ------------------------- + // MYSTERY ENCOUNTER OVERRIDES + // ------------------------- + + /** 1 to 256, set to null to ignore */ + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = null; + readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier | null = null; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = null; + // ------------------------- // MODIFIER / ITEM OVERRIDES // ------------------------- diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 47d212aa5985..86e42acb26b0 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -15,6 +15,7 @@ import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { FieldPhase } from "./field-phase"; import { SelectTargetPhase } from "./select-target-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export class CommandPhase extends FieldPhase { protected fieldIndex: integer; @@ -68,7 +69,12 @@ export class CommandPhase extends FieldPhase { } } } else { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter?.skipToFightInput) { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex); + } else { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } } } @@ -134,6 +140,13 @@ export class CommandPhase extends FieldPhase { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter!.catchAllowed) { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(i18next.t("battle:noPokeballMysteryEncounter"), null, () => { + this.scene.ui.showText("", 0); + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + }, null, true); } else { const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex()); if (targets.length > 1) { @@ -173,7 +186,7 @@ export class CommandPhase extends FieldPhase { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); - } else if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) { + } else if (!isSwitch && (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.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, () => { diff --git a/src/phases/common-anim-phase.ts b/src/phases/common-anim-phase.ts index 66299064bb29..c4071488eefb 100644 --- a/src/phases/common-anim-phase.ts +++ b/src/phases/common-anim-phase.ts @@ -6,12 +6,14 @@ import { PokemonPhase } from "./pokemon-phase"; export class CommonAnimPhase extends PokemonPhase { private anim: CommonAnim | null; private targetIndex: integer | undefined; + private playOnEmptyField: boolean; - constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim) { + constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim, playOnEmptyField: boolean = false) { super(scene, battlerIndex); this.anim = anim!; // TODO: is this bang correct? this.targetIndex = targetIndex; + this.playOnEmptyField = playOnEmptyField; } setAnimation(anim: CommonAnim) { @@ -19,7 +21,8 @@ export class CommonAnimPhase extends PokemonPhase { } start() { - new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, false, () => { + const target = this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon(); + new CommonBattleAnim(this.anim, this.getPokemon(), target).play(this.scene, false, () => { this.end(); }); } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 3f37095569a7..1d9567ee9b3a 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -1,5 +1,5 @@ import BattleScene from "#app/battle-scene"; -import { BattleType, BattlerIndex } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { TrainerSlot } from "#app/data/trainer-config"; @@ -10,14 +10,15 @@ import { Species } from "#app/enums/species"; import { EncounterPhaseEvent } from "#app/events/battle-scene"; import Pokemon, { FieldPosition } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; -import { regenerateModifierPoolThresholds, ModifierPoolType } from "#app/modifier/modifier-type"; -import { IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; +import { ModifierPoolType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { BoostBugSpawnModifier, IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { handleTutorial, Tutorial } from "#app/tutorial"; import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import { BattlePhase } from "./battle-phase"; import * as Utils from "#app/utils"; +import { randSeedInt } from "#app/utils"; import { CheckSwitchPhase } from "./check-switch-phase"; import { GameOverPhase } from "./game-over-phase"; import { PostSummonPhase } from "./post-summon-phase"; @@ -27,6 +28,12 @@ import { ShinySparklePhase } from "./shiny-sparkle-phase"; import { SummonPhase } from "./summon-phase"; import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; import Overrides from "#app/overrides"; +import { initEncounterAnims, loadEncounterAnimAssets } from "#app/data/battle-anims"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { doTrainerExclamation } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { getGoldenBugNetSpecies } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -55,14 +62,45 @@ export class EncounterPhase extends BattlePhase { const battle = this.scene.currentBattle; + // Init Mystery Encounter if there is one + const mysteryEncounter = battle.mysteryEncounter; + if (mysteryEncounter) { + // If ME has an onInit() function, call it + // Usually used for calculating rand data before initializing anything visual + // Also prepopulates any dialogue tokens from encounter/option requirements + this.scene.executeWithSeedOffset(() => { + if (mysteryEncounter.onInit) { + mysteryEncounter.onInit(this.scene); + } + mysteryEncounter.populateDialogueTokensFromRequirements(this.scene); + }, this.scene.currentBattle.waveIndex); + + // Add any special encounter animations to load + if (mysteryEncounter.encounterAnimations && mysteryEncounter.encounterAnimations.length > 0) { + loadEnemyAssets.push(initEncounterAnims(this.scene, mysteryEncounter.encounterAnimations).then(() => loadEncounterAnimAssets(this.scene, true))); + } + + // Add intro visuals for mystery encounter + mysteryEncounter.initIntroVisuals(this.scene); + this.scene.field.add(mysteryEncounter.introVisuals!); + } + let totalBst = 0; - battle.enemyLevels?.forEach((level, e) => { + battle.enemyLevels?.every((level, e) => { + if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + // Skip enemy loading for MEs, those are loaded elsewhere + return false; + } if (!this.loaded) { if (battle.battleType === BattleType.TRAINER) { battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here? } else { - const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); + let enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true); + // If player has golden bug net, rolls 10% chance to replace with species from the golden bug net bug pool + if (this.scene.findModifier(m => m instanceof BoostBugSpawnModifier) && randSeedInt(10) === 0) { + enemySpecies = getGoldenBugNetSpecies(); + } battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies)); if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { battle.enemyParty[e].ivs = new Array(6).fill(31); @@ -79,7 +117,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER); + this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER || battle?.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE); } if (enemyPokemon.species.speciesId === Species.ETERNATUS) { @@ -104,6 +142,7 @@ export class EncounterPhase extends BattlePhase { loadEnemyAssets.push(enemyPokemon.loadAssets()); console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats); + return true; }); if (this.scene.getParty().filter(p => p.isShiny()).length === 6) { @@ -112,6 +151,25 @@ export class EncounterPhase extends BattlePhase { if (battle.battleType === BattleType.TRAINER) { loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct? + } else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + if (!battle.mysteryEncounter) { + battle.mysteryEncounter = this.scene.getMysteryEncounter(mysteryEncounter?.encounterType); + } + if (battle.mysteryEncounter.introVisuals) { + loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite())); + } + if (battle.mysteryEncounter.loadAssets.length > 0) { + loadEnemyAssets.push(...battle.mysteryEncounter.loadAssets); + } + // Load Mystery Encounter Exclamation bubble and sfx + loadEnemyAssets.push(new Promise(resolve => { + this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); + this.scene.loadImage("exclaim", "mystery-encounters"); + this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); + if (!this.scene.load.isLoading()) { + this.scene.load.start(); + } + })); } else { const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1; // for double battles, reduce the health segments for boss Pokemon unless there is an override @@ -127,7 +185,10 @@ export class EncounterPhase extends BattlePhase { } Promise.all(loadEnemyAssets).then(() => { - battle.enemyParty.forEach((enemyPokemon, e) => { + battle.enemyParty.every((enemyPokemon, e) => { + if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + return false; + } if (e < (battle.double ? 2 : 1)) { if (battle.battleType === BattleType.WILD) { this.scene.field.add(enemyPokemon); @@ -145,9 +206,10 @@ export class EncounterPhase extends BattlePhase { enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT); } } + return true; }); - if (!this.loaded) { + if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) { regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD); this.scene.generateEnemyModifiers(); } @@ -203,6 +265,19 @@ export class EncounterPhase extends BattlePhase { } } }); + + const encounterIntroVisuals = this.scene.currentBattle?.mysteryEncounter?.introVisuals; + if (encounterIntroVisuals) { + const enterFromRight = encounterIntroVisuals.enterFromRight; + if (enterFromRight) { + encounterIntroVisuals.x += 500; + } + this.scene.tweens.add({ + targets: encounterIntroVisuals, + x: enterFromRight ? "-=200" : "+=300", + duration: 2000 + }); + } } getEncounterMessage(): string { @@ -289,6 +364,63 @@ export class EncounterPhase extends BattlePhase { showDialogueAndSummon(); } } + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter) { + const encounter = this.scene.currentBattle.mysteryEncounter; + const introVisuals = encounter.introVisuals; + introVisuals?.playAnim(); + + if (encounter.onVisualsStart) { + encounter.onVisualsStart(this.scene); + } + + const doEncounter = () => { + const doShowEncounterOptions = () => { + this.scene.ui.clearText(); + this.scene.ui.getMessageHandler().hideNameText(); + + this.scene.unshiftPhase(new MysteryEncounterPhase(this.scene)); + this.end(); + }; + + if (showEncounterMessage) { + const introDialogue = encounter.dialogue.intro; + if (!introDialogue) { + doShowEncounterOptions(); + } else { + const FIRST_DIALOGUE_PROMPT_DELAY = 750; + let i = 0; + const showNextDialogue = () => { + const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; + const dialogue = introDialogue[i]; + const title = getEncounterText(this.scene, dialogue?.speaker); + const text = getEncounterText(this.scene, dialogue.text)!; + i++; + if (title) { + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + if (introDialogue.length > 0) { + showNextDialogue(); + } + } + } else { + doShowEncounterOptions(); + } + }; + + const encounterMessage = i18next.t("battle:mysteryEncounterAppeared"); + + if (!encounterMessage) { + doEncounter(); + } else { + doTrainerExclamation(this.scene); + this.scene.ui.showDialogue(encounterMessage, "???", null, () => { + this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doEncounter())); + }); + } } } @@ -301,7 +433,7 @@ export class EncounterPhase extends BattlePhase { } }); - if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { + if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => { // if there is not a player party, we can't continue if (!this.scene.getParty()?.length) { diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 91ee0456cd42..b4503a7b059e 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -14,11 +14,15 @@ import { FieldPhase } from "./field-phase"; */ export class EnemyCommandPhase extends FieldPhase { protected fieldIndex: integer; + protected skipTurn: boolean = false; constructor(scene: BattleScene, fieldIndex: integer) { super(scene); this.fieldIndex = fieldIndex; + if (this.scene.currentBattle.mysteryEncounter?.skipEnemyBattleTurns) { + this.skipTurn = true; + } } start() { @@ -57,7 +61,7 @@ export class EnemyCommandPhase extends FieldPhase { const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [false] }; + { command: Command.POKEMON, cursor: index, args: [false], skip: this.skipTurn }; battle.enemySwitchCounter++; @@ -71,7 +75,7 @@ export class EnemyCommandPhase extends FieldPhase { const nextMove = enemyPokemon.getNextMove(); this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.FIGHT, move: nextMove }; + { command: Command.FIGHT, move: nextMove, skip: this.skipTurn }; this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0); diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 2b63dcdd14bc..41384e5e491b 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -110,7 +110,7 @@ export class FaintPhase extends PokemonPhase { } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { + if ([BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType)) { const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length; if (hasReservePartyMember) { this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false)); @@ -124,9 +124,6 @@ export class FaintPhase extends PokemonPhase { this.scene.redirectPokemonMoves(pokemon, allyPokemon); } - pokemon.lapseTags(BattlerTagLapseType.FAINT); - this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); - pokemon.faintCry(() => { if (pokemon instanceof PlayerPokemon) { pokemon.addFriendship(-10); @@ -140,6 +137,9 @@ export class FaintPhase extends PokemonPhase { ease: "Sine.easeIn", onComplete: () => { pokemon.resetSprite(); + pokemon.lapseTags(BattlerTagLapseType.FAINT); + this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id)); + pokemon.y -= 150; pokemon.trySetStatus(StatusEffect.FAINT); if (pokemon.isPlayer()) { diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 17805e90f0f3..8ab191324c67 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -237,7 +237,9 @@ export class GameOverPhase extends BattlePhase { trainer: this.scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(this.scene.currentBattle.trainer) : null, gameVersion: this.scene.game.config.gameVersion, timestamp: new Date().getTime(), - challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)) + challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType, + mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData } as SessionSaveData; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index e3c8216b65ac..c3199166e840 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -102,7 +102,7 @@ export class MoveEffectPhase extends PokemonPhase { * (and not random target) and failed the hit check against its target (MISS), log the move * as FAILed or MISSed (depending on the conditions above) and end this phase. */ - if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) { + if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag))) { this.stopMultiHit(); if (hasActiveTargets) { this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); @@ -119,25 +119,12 @@ export class MoveEffectPhase extends PokemonPhase { /** All move effect attributes are chained together in this array to be applied asynchronously. */ const applyAttrs: Promise[] = []; + const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { - /** - * If the move missed a target, stop all future hits against that target - * and move on to the next target (if there is one). - */ - if (!targetHitChecks[target.getBattlerIndex()]) { - this.stopMultiHit(target); - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); - if (moveHistoryEntry.result === MoveResult.PENDING) { - moveHistoryEntry.result = MoveResult.MISS; - } - user.pushMoveHistory(moveHistoryEntry); - applyMoveAttrs(MissEffectAttr, user, null, move); - continue; - } /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -155,6 +142,21 @@ export class MoveEffectPhase extends PokemonPhase { && (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) || (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); + /** + * If the move missed a target, stop all future hits against that target + * and move on to the next target (if there is one). + */ + if (!isProtected && !targetHitChecks[target.getBattlerIndex()]) { + this.stopMultiHit(target); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); + if (moveHistoryEntry.result === MoveResult.PENDING) { + moveHistoryEntry.result = MoveResult.MISS; + } + user.pushMoveHistory(moveHistoryEntry); + applyMoveAttrs(MissEffectAttr, user, null, move); + continue; + } + /** Does this phase represent the invoked move's first strike? */ const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts new file mode 100644 index 000000000000..6c9d3fd8c1df --- /dev/null +++ b/src/phases/mystery-encounter-phases.ts @@ -0,0 +1,605 @@ +import i18next from "i18next"; +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 { getCharVariantFromDialogue } from "../data/dialogue"; +import { TrainerSlot } from "../data/trainer-config"; +import { BattleSpec } from "#enums/battle-spec"; +import { IvScannerModifier } from "../modifier/modifier"; +import * as Utils from "../utils"; +import { isNullOrUndefined } from "../utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { PostTurnStatusEffectPhase } from "#app/phases/post-turn-status-effect-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { ScanIvsPhase } from "#app/phases/scan-ivs-phase"; +import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase"; +import { ReturnPhase } from "#app/phases/return-phase"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { GameOverPhase } from "#app/phases/game-over-phase"; +import { SwitchPhase } from "#app/phases/switch-phase"; +import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; + +/** + * Will handle (in order): + * - Clearing of phase queues to enter the Mystery Encounter game state + * - Management of session data related to MEs + * - Initialization of ME option select menu and UI + * - Execute {@linkcode MysteryEncounter.onPreOptionPhase} logic if it exists for the selected option + * - Display any `OptionTextDisplay.selected` type dialogue that is set in the {@linkcode MysteryEncounterDialogue} dialogue tree for selected option + * - Queuing of the {@linkcode MysteryEncounterOptionSelectedPhase} + */ +export class MysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300; + optionSelectSettings?: OptionSelectSettings; + + /** + * Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time + * @param scene + * @param optionSelectSettings allows overriding the typical options of an encounter with new ones + */ + constructor(scene: BattleScene, optionSelectSettings?: OptionSelectSettings) { + super(scene); + this.optionSelectSettings = optionSelectSettings; + } + + /** + * Updates seed offset, sets seen encounter session data, sets UI mode + */ + start() { + super.start(); + + // Clears out queued phases that are part of standard battle + this.scene.clearPhaseQueue(); + this.scene.clearPhaseQueueSplice(); + + const encounter = this.scene.currentBattle.mysteryEncounter!; + encounter.updateSeedOffset(this.scene); + + if (!this.optionSelectSettings) { + // Sets flag that ME was encountered, only if this is not a followup option select phase + // Can be used in later MEs to check for requirements to spawn, run history, etc. + this.scene.mysteryEncounterSaveData.encounteredEvents.push(new SeenEncounterData(encounter.encounterType, encounter.encounterTier, this.scene.currentBattle.waveIndex)); + } + + // Initiates encounter dialogue window and option select + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings); + } + + /** + * Triggers after a player selects an option for the encounter + * @param option + * @param index + */ + handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { + // Set option selected flag + this.scene.currentBattle.mysteryEncounter!.selectedOption = option; + + if (!this.optionSelectSettings) { + // Saves the selected option in the ME save data, only if this is not a followup option select phase + // Can be used for analytics purposes to track what options are popular on certain encounters + const encounterSaveData = this.scene.mysteryEncounterSaveData.encounteredEvents[this.scene.mysteryEncounterSaveData.encounteredEvents.length - 1]; + if (encounterSaveData.type === this.scene.currentBattle.mysteryEncounter?.encounterType) { + encounterSaveData.selectedOption = index; + } + } + + if (!option.onOptionPhase) { + return false; + } + + // Populate dialogue tokens for option requirements + this.scene.currentBattle.mysteryEncounter!.populateDialogueTokensFromRequirements(this.scene); + + if (option.onPreOptionPhase) { + this.scene.executeWithSeedOffset(async () => { + return await option.onPreOptionPhase!(this.scene) + .then((result) => { + if (isNullOrUndefined(result) || result) { + this.continueEncounter(); + } + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); + } else { + this.continueEncounter(); + } + + return true; + } + + /** + * Queues {@linkcode MysteryEncounterOptionSelectedPhase}, displays option.selected dialogue and ends phase + */ + continueEncounter() { + const endDialogueAndContinueEncounter = () => { + this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene)); + this.end(); + }; + + const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue; + if (optionSelectDialogue?.selected && optionSelectDialogue.selected.length > 0) { + // Handle intermediate dialogue (between player selection event and the onOptionSelect logic) + this.scene.ui.setMode(Mode.MESSAGE); + const selectedDialogue = optionSelectDialogue.selected; + let i = 0; + const showNextDialogue = () => { + const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue; + const dialogue = selectedDialogue[i]; + let title: string | null = null; + const text: string | null = getEncounterText(this.scene, dialogue.text); + if (dialogue.speaker) { + title = getEncounterText(this.scene, dialogue.speaker); + } + + i++; + if (title) { + this.scene.ui.showDialogue(text ?? "", title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + showNextDialogue(); + } else { + endDialogueAndContinueEncounter(); + } + } + + /** + * Ends phase + */ + end() { + this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); + } +} + +/** + * Will handle (in order): + * - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option + * + * It is important to point out that no phases are directly queued by any logic within this phase + * Any phase that is meant to follow this one MUST be queued via the onOptionSelect() logic of the selected option + */ +export class MysteryEncounterOptionSelectedPhase extends Phase { + onOptionSelect: OptionPhaseCallback; + + constructor(scene: BattleScene) { + super(scene); + this.onOptionSelect = this.scene.currentBattle.mysteryEncounter!.selectedOption!.onOptionPhase; + } + + /** + * Will handle (in order): + * - Execute {@linkcode MysteryEncounter.onOptionSelect} logic if it exists for the selected option + * + * It is important to point out that no phases are directly queued by any logic within this phase. + * Any phase that is meant to follow this one MUST be queued via the {@linkcode MysteryEncounter.onOptionSelect} logic of the selected option. + */ + start() { + super.start(); + if (this.scene.currentBattle.mysteryEncounter?.autoHideIntroVisuals) { + transitionMysteryEncounterIntroVisuals(this.scene).then(() => { + this.scene.executeWithSeedOffset(() => { + this.onOptionSelect(this.scene).finally(() => { + this.end(); + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500); + }); + } else { + this.scene.executeWithSeedOffset(() => { + this.onOptionSelect(this.scene).finally(() => { + this.end(); + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500); + } + } +} + +/** + * Runs at the beginning of an Encounter's battle + * Will clean up any residual flinches, Endure, etc. that are left over from {@linkcode MysteryEncounter.startOfBattleEffects} + * Will also handle Game Overs, switches, etc. that could happen from {@linkcode handleMysteryEncounterBattleStartEffects} + * See {@linkcode TurnEndPhase} for more details + */ +export class MysteryEncounterBattleStartCleanupPhase extends Phase { + constructor(scene: BattleScene) { + super(scene); + } + + /** + * Cleans up `TURN_END` tags, any {@linkcode PostTurnStatusEffectPhase}s, checks for Pokemon switches, then continues + */ + start() { + super.start(); + + const field = this.scene.getField(true).filter(p => p.summonData); + field.forEach(pokemon => { + pokemon.lapseTags(BattlerTagLapseType.TURN_END); + }); + + // Remove any status tick phases + while (!!this.scene.findPhase(p => p instanceof PostTurnStatusEffectPhase)) { + this.scene.tryRemovePhase(p => p instanceof PostTurnStatusEffectPhase); + } + + // The total number of Pokemon in the player's party that can legally fight + const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle()); + // The total number of legal player Pokemon that aren't currently on the field + const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true)); + if (!legalPlayerPokemon.length) { + this.scene.unshiftPhase(new GameOverPhase(this.scene)); + return this.end(); + } + + // Check for any KOd player mons and switch + // For each fainted mon on the field, if there is a legal replacement, summon it + const playerField = this.scene.getPlayerField(); + playerField.forEach((pokemon, i) => { + if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) { + this.scene.unshiftPhase(new SwitchPhase(this.scene, i, true, false)); + } + }); + + // THEN, if is a double battle, and player only has 1 summoned pokemon, center pokemon on field + if (this.scene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) { + this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); + } + + this.end(); + } +} + +/** + * Will handle (in order): + * - Setting BGM + * - Showing intro dialogue for an enemy trainer or wild Pokemon + * - Sliding in the visuals for enemy trainer or wild Pokemon, as well as handling summoning animations + * - Queue the {@linkcode SummonPhase}s, {@linkcode PostSummonPhase}s, etc., required to initialize the phase queue for a battle + */ +export class MysteryEncounterBattlePhase extends Phase { + disableSwitch: boolean; + + constructor(scene: BattleScene, disableSwitch = false) { + super(scene); + this.disableSwitch = disableSwitch; + } + + /** + * Sets up a ME battle + */ + start() { + super.start(); + + this.doMysteryEncounterBattle(this.scene); + } + + /** + * Gets intro battle message for new battle + * @param scene + * @private + */ + private getBattleMessage(scene: BattleScene): string { + const enemyField = scene.getEnemyField(); + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + + if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { + return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name }); + } + + if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + if (scene.currentBattle.double) { + return i18next.t("battle:trainerAppearedDouble", { trainerName: scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); + + } else { + return i18next.t("battle:trainerAppeared", { trainerName: scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) }); + } + } + + return enemyField.length === 1 + ? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].name }) + : i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name }); + } + + /** + * Queues {@linkcode SummonPhase}s for the new battle, and handles trainer animations/dialogue if it's a Trainer battle + * @param scene + * @private + */ + private doMysteryEncounterBattle(scene: BattleScene) { + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) { + // Summons the wild/boss Pokemon + if (encounterMode === MysteryEncounterMode.BOSS_BATTLE) { + scene.playBgm(undefined); + } + const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length; + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + if (scene.currentBattle.double && availablePartyMembers > 1) { + scene.unshiftPhase(new SummonPhase(scene, 1, false)); + } + + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { + scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 0); + } else { + this.endBattleSetup(scene); + } + } else if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + this.showEnemyTrainer(); + const doSummon = () => { + scene.currentBattle.started = true; + scene.playBgm(undefined); + scene.pbTray.showPbTray(scene.getParty()); + scene.pbTrayEnemy.showPbTray(scene.getEnemyParty()); + const doTrainerSummon = () => { + this.hideEnemyTrainer(); + const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length; + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + if (scene.currentBattle.double && availablePartyMembers > 1) { + scene.unshiftPhase(new SummonPhase(scene, 1, false)); + } + this.endBattleSetup(scene); + }; + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { + scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1000, true); + } else { + doTrainerSummon(); + } + }; + + const encounterMessages = scene.currentBattle.trainer?.getEncounterMessages(); + + if (!encounterMessages || !encounterMessages.length) { + doSummon(); + } else { + const trainer = this.scene.currentBattle.trainer; + let message: string; + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); + message = message!; // tell TS compiler it's defined now + const showDialogueAndSummon = () => { + scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { + scene.charSprite.hide().then(() => scene.hideFieldOverlay(250).then(() => doSummon())); + }); + }; + if (this.scene.currentBattle.trainer?.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) { + this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer?.getKey()!, getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); // TODO: is this bang correct? + } else { + showDialogueAndSummon(); + } + } + } + } + + /** + * Initiate {@linkcode SummonPhase}s, {@linkcode ScanIvsPhase}, {@linkcode PostSummonPhase}s, etc. + * @param scene + * @private + */ + private endBattleSetup(scene: BattleScene) { + const enemyField = scene.getEnemyField(); + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; + + // PostSummon and ShinySparkle phases are handled by SummonPhase + + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) { + const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); + } + } + + const availablePartyMembers = scene.getParty().filter(p => !p.isFainted()); + + if (!availablePartyMembers[0].isOnField()) { + scene.pushPhase(new SummonPhase(scene, 0)); + } + + if (scene.currentBattle.double) { + if (availablePartyMembers.length > 1) { + scene.pushPhase(new ToggleDoublePositionPhase(scene, true)); + if (!availablePartyMembers[1].isOnField()) { + scene.pushPhase(new SummonPhase(scene, 1)); + } + } + } else { + if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { + scene.pushPhase(new ReturnPhase(scene, 1)); + } + scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); + } + + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { + const minPartySize = scene.currentBattle.double ? 2 : 1; + if (availablePartyMembers.length > minPartySize) { + scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double)); + if (scene.currentBattle.double) { + scene.pushPhase(new CheckSwitchPhase(scene, 1, scene.currentBattle.double)); + } + } + } + + this.end(); + } + + /** + * Ease in enemy trainer + * @private + */ + private showEnemyTrainer(): void { + // Show enemy trainer + const trainer = this.scene.currentBattle.trainer; + if (!trainer) { + return; + } + trainer.alpha = 0; + trainer.x += 16; + trainer.y -= 16; + trainer.setVisible(true); + this.scene.tweens.add({ + targets: trainer, + x: "-=16", + y: "+=16", + alpha: 1, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + trainer.untint(100, "Sine.easeOut"); + trainer.playAnim(); + } + }); + } + + private hideEnemyTrainer(): void { + this.scene.tweens.add({ + targets: this.scene.currentBattle.trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750 + }); + } +} + +/** + * Will handle (in order): + * - doContinueEncounter() callback for continuous encounters with back-to-back battles (this should push/shift its own phases as needed) + * + * OR + * + * - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterExp} + * - Any encounter reward logic that is set within {@linkcode MysteryEncounter.doEncounterRewards} + * - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true + * - Queuing of the {@linkcode PostMysteryEncounterPhase} + */ +export class MysteryEncounterRewardsPhase extends Phase { + addHealPhase: boolean; + + constructor(scene: BattleScene, addHealPhase: boolean = false) { + super(scene); + this.addHealPhase = addHealPhase; + } + + /** + * Runs {@linkcode MysteryEncounter.doContinueEncounter} and ends phase, OR {@linkcode MysteryEncounter.onRewards} then continues encounter + */ + start() { + super.start(); + const encounter = this.scene.currentBattle.mysteryEncounter!; + + if (encounter.doContinueEncounter) { + encounter.doContinueEncounter(this.scene).then(() => { + this.end(); + }); + } else { + this.scene.executeWithSeedOffset(() => { + if (encounter.onRewards) { + encounter.onRewards(this.scene).then(() => { + this.doEncounterRewardsAndContinue(); + }); + } else { + this.doEncounterRewardsAndContinue(); + } + // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) + }, this.scene.currentBattle.waveIndex * 1000); + } + } + + /** + * Queues encounter EXP and rewards phases, {@linkcode PostMysteryEncounterPhase}, and ends phase + */ + doEncounterRewardsAndContinue() { + const encounter = this.scene.currentBattle.mysteryEncounter!; + + if (encounter.doEncounterExp) { + encounter.doEncounterExp(this.scene); + } + + if (encounter.doEncounterRewards) { + encounter.doEncounterRewards(this.scene); + } else if (this.addHealPhase) { + this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase); + this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, undefined, { fillRemaining: false, rerollMultiplier: -1 })); + } + + this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene)); + this.end(); + } +} + +/** + * Will handle (in order): + * - {@linkcode MysteryEncounter.onPostOptionSelect} logic (based on an option that was selected) + * - Showing any outro dialogue messages + * - Cleanup of any leftover intro visuals + * - Queuing of the next wave + */ +export class PostMysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 750; + onPostOptionSelect?: OptionPhaseCallback; + + constructor(scene: BattleScene) { + super(scene); + this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter?.selectedOption?.onPostOptionPhase; + } + + /** + * Runs {@linkcode MysteryEncounter.onPostOptionSelect} then continues encounter + */ + start() { + super.start(); + + if (this.onPostOptionSelect) { + this.scene.executeWithSeedOffset(async () => { + return await this.onPostOptionSelect!(this.scene) + .then((result) => { + if (isNullOrUndefined(result) || result) { + this.continueEncounter(); + } + }); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 2000); + } else { + this.continueEncounter(); + } + } + + /** + * Queues {@linkcode NewBattlePhase}, plays outro dialogue and ends phase + */ + continueEncounter() { + const endPhase = () => { + this.scene.pushPhase(new NewBattlePhase(this.scene)); + this.end(); + }; + + const outroDialogue = this.scene.currentBattle?.mysteryEncounter?.dialogue?.outro; + if (outroDialogue && outroDialogue.length > 0) { + let i = 0; + const showNextDialogue = () => { + const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue; + const dialogue = outroDialogue[i]; + let title: string | null = null; + const text: string | null = getEncounterText(this.scene, dialogue.text); + if (dialogue.speaker) { + title = getEncounterText(this.scene, dialogue.speaker); + } + + i++; + this.scene.ui.setMode(Mode.MESSAGE); + if (title) { + this.scene.ui.showDialogue(text ?? "", title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); + } else { + this.scene.ui.showText(text ?? "", null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); + } + }; + + showNextDialogue(); + } else { + endPhase(); + } + } +} diff --git a/src/phases/new-biome-encounter-phase.ts b/src/phases/new-biome-encounter-phase.ts index 9f1209fb7ee5..48d928402dea 100644 --- a/src/phases/new-biome-encounter-phase.ts +++ b/src/phases/new-biome-encounter-phase.ts @@ -24,8 +24,14 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase { } const enemyField = this.scene.getEnemyField(); + const moveTargets: any[] = [this.scene.arenaEnemy, enemyField]; + const mysteryEncounter = this.scene.currentBattle?.mysteryEncounter?.introVisuals; + if (mysteryEncounter) { + moveTargets.push(mysteryEncounter); + } + this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, enemyField].flat(), + targets: moveTargets.flat(), x: "+=300", duration: 2000, onComplete: () => { diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index d51aa374b6e0..7de2472dd359 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -23,8 +23,28 @@ export class NextEncounterPhase extends EncounterPhase { this.scene.arenaNextEnemy.setVisible(true); const enemyField = this.scene.getEnemyField(); + const moveTargets: any[] = [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer]; + const lastEncounterVisuals = this.scene.lastMysteryEncounter?.introVisuals; + if (lastEncounterVisuals) { + moveTargets.push(lastEncounterVisuals); + } + const nextEncounterVisuals = this.scene.currentBattle.mysteryEncounter?.introVisuals; + if (nextEncounterVisuals) { + const enterFromRight = nextEncounterVisuals.enterFromRight; + if (enterFromRight) { + nextEncounterVisuals.x += 500; + this.scene.tweens.add({ + targets: nextEncounterVisuals, + x: "-=200", + duration: 2000 + }); + } else { + moveTargets.push(nextEncounterVisuals); + } + } + this.scene.tweens.add({ - targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(), + targets: moveTargets.flat(), x: "+=300", duration: 2000, onComplete: () => { @@ -36,6 +56,10 @@ export class NextEncounterPhase extends EncounterPhase { if (this.scene.lastEnemyTrainer) { this.scene.lastEnemyTrainer.destroy(); } + if (lastEncounterVisuals) { + this.scene.field.remove(lastEncounterVisuals, true); + this.scene.lastMysteryEncounter!.introVisuals = undefined; + } if (!this.tryOverrideForBattleSpec()) { this.doEncounterCommon(); diff --git a/src/phases/party-exp-phase.ts b/src/phases/party-exp-phase.ts new file mode 100644 index 000000000000..c5a254871cae --- /dev/null +++ b/src/phases/party-exp-phase.ts @@ -0,0 +1,31 @@ +import BattleScene from "#app/battle-scene"; +import { Phase } from "#app/phase"; + +/** + * Provides EXP to the player's party *without* doing any Pokemon defeated checks or queueing extraneous post-battle phases + * Intended to be used as a more 1-off phase to provide exp to the party (such as during MEs), rather than cleanup a battle entirely + */ +export class PartyExpPhase extends Phase { + expValue: number; + useWaveIndexMultiplier?: boolean; + pokemonParticipantIds?: Set; + + constructor(scene: BattleScene, expValue: number, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set) { + super(scene); + + this.expValue = expValue; + this.useWaveIndexMultiplier = useWaveIndexMultiplier; + this.pokemonParticipantIds = pokemonParticipantIds; + } + + /** + * Gives EXP to the party + */ + start() { + super.start(); + + this.scene.applyPartyExp(this.expValue, false, this.useWaveIndexMultiplier, this.pokemonParticipantIds); + + this.end(); + } +} diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 47a5513f0eb9..e7f6c6ea3db8 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -4,6 +4,9 @@ import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; import { ArenaTrapTag } from "#app/data/arena-tag"; import { StatusEffect } from "#app/enums/status-effect"; import { PokemonPhase } from "./pokemon-phase"; +import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BattleType } from "#app/battle"; export class PostSummonPhase extends PokemonPhase { constructor(scene: BattleScene, battlerIndex: BattlerIndex) { @@ -19,6 +22,12 @@ export class PostSummonPhase extends PokemonPhase { pokemon.status.turnCount = 0; } this.scene.arena.applyTags(ArenaTrapTag, pokemon); + + // If this is mystery encounter and has post summon phase tag, apply post summon effects + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag).length > 0) { + pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); + } + applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end()); } } diff --git a/src/phases/select-modifier-phase.ts b/src/phases/select-modifier-phase.ts index e14638c5dd2c..39a0da1167f9 100644 --- a/src/phases/select-modifier-phase.ts +++ b/src/phases/select-modifier-phase.ts @@ -9,16 +9,20 @@ import i18next from "i18next"; 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"; export class SelectModifierPhase extends BattlePhase { private rerollCount: integer; - private modifierTiers: ModifierTier[]; + private modifierTiers?: ModifierTier[]; + private customModifierSettings?: CustomModifierSettings; - constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[]) { + constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) { super(scene); this.rerollCount = rerollCount; - this.modifierTiers = modifierTiers!; // TODO: is this bang correct? + this.modifierTiers = modifierTiers; + this.customModifierSettings = customModifierSettings; } start() { @@ -36,6 +40,20 @@ export class SelectModifierPhase extends BattlePhase { if (this.isPlayer()) { this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount); } + + // If custom modifiers are specified, overrides default item count + if (!!this.customModifierSettings) { + const newItemCount = (this.customModifierSettings.guaranteedModifierTiers?.length || 0) + + (this.customModifierSettings.guaranteedModifierTypeOptions?.length || 0) + + (this.customModifierSettings.guaranteedModifierTypeFuncs?.length || 0); + if (this.customModifierSettings.fillRemaining) { + const originalCount = modifierCount.value; + modifierCount.value = originalCount > newItemCount ? originalCount : newItemCount; + } else { + modifierCount.value = newItemCount; + } + } + const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value); const modifierSelectCallback = (rowCursor: integer, cursor: integer) => { @@ -56,7 +74,7 @@ export class SelectModifierPhase extends BattlePhase { switch (cursor) { case 0: const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers); - if (this.scene.money < rerollCost) { + if (rerollCost < 0 || this.scene.money < rerollCost) { this.scene.ui.playError(); return false; } else { @@ -99,6 +117,12 @@ export class SelectModifierPhase extends BattlePhase { } return true; case 1: + if (typeOptions.length === 0) { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.MESSAGE); + super.end(); + return true; + } if (typeOptions[cursor].type) { modifierType = typeOptions[cursor].type; } @@ -217,7 +241,18 @@ export class SelectModifierPhase extends BattlePhase { } else { baseValue = 250; } - return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount), Number.MAX_SAFE_INTEGER); + + let multiplier = 1; + if (!isNullOrUndefined(this.customModifierSettings?.rerollMultiplier)) { + if (this.customModifierSettings!.rerollMultiplier! < 0) { + // Completely overrides reroll cost to -1 and early exits + return -1; + } + + // Otherwise, continue with custom multiplier + multiplier = this.customModifierSettings!.rerollMultiplier!; + } + return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER); } getPoolType(): ModifierPoolType { @@ -225,7 +260,7 @@ export class SelectModifierPhase extends BattlePhase { } getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] { - return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined); + return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings); } addModifier(modifier: Modifier): Promise { diff --git a/src/phases/show-party-exp-bar-phase.ts b/src/phases/show-party-exp-bar-phase.ts index 9e019b202a5e..f1783e7715f6 100644 --- a/src/phases/show-party-exp-bar-phase.ts +++ b/src/phases/show-party-exp-bar-phase.ts @@ -1,4 +1,5 @@ import BattleScene from "#app/battle-scene"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; import { ExpNotification } from "#app/enums/exp-notification"; import { ExpBoosterModifier } from "#app/modifier/modifier"; import * as Utils from "#app/utils"; @@ -44,7 +45,7 @@ export class ShowPartyExpBarPhase extends PlayerPartyMemberPokemonPhase { } else { this.end(); } - } else if (this.scene.expGainsSpeed < 3) { + } else if (this.scene.expGainsSpeed < ExpGainsSpeed.SKIP) { this.scene.partyExpBar.showPokemonExp(pokemon, exp.value, false, newLevel).then(() => { setTimeout(() => this.end(), 500 / Math.pow(2, this.scene.expGainsSpeed)); }); diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 2645060c5479..d909c5c35016 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -12,6 +12,7 @@ import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase"; import { PostSummonPhase } from "./post-summon-phase"; import { GameOverPhase } from "./game-over-phase"; import { ShinySparklePhase } from "./shiny-sparkle-phase"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export class SummonPhase extends PartyMemberPokemonPhase { private loaded: boolean; @@ -33,8 +34,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { */ preSummon(): void { const partyMember = this.getPokemon(); - // If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon - if (!partyMember.isAllowedInBattle()) { + // If the Pokemon about to be sent out is fainted, illegal under a challenge, or no longer in the party for some reason, switch to the first non-fainted legal Pokemon + if (!partyMember.isAllowedInBattle() || (this.player && !this.getParty().some(p => p.id === partyMember.id))) { console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve..."); // First check if they're somehow still in play, if so remove them. @@ -79,16 +80,22 @@ export class SummonPhase extends PartyMemberPokemonPhase { onComplete: () => this.scene.trainer.setVisible(false) }); this.scene.time.delayedCall(750, () => this.summon()); - } else { + } else if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { const trainerName = this.scene.currentBattle.trainer?.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); const pokemonName = this.getPokemon().getNameToRender(); const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); this.scene.pbTrayEnemy.hide(); this.scene.ui.showText(message, null, () => this.summon()); + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + this.scene.pbTrayEnemy.hide(); + this.summonWild(); } } + /** + * Enemy trainer or player trainer will do animations to throw Pokeball and summon a Pokemon to the field. + */ summon(): void { const pokemon = this.getPokemon(); @@ -167,6 +174,63 @@ export class SummonPhase extends PartyMemberPokemonPhase { }); } + /** + * Handles tweening and battle setup for a wild Pokemon that appears outside of the normal screen transition. + * Wild Pokemon will ease and fade in onto the field, then perform standard summon behavior. + * Currently only used by Mystery Encounters, as all other battle types pre-summon wild pokemon before screen transitions. + */ + summonWild(): void { + const pokemon = this.getPokemon(); + + if (this.fieldIndex === 1) { + pokemon.setFieldPosition(FieldPosition.RIGHT, 0); + } else { + const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length; + pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT); + } + + this.scene.add.existing(pokemon); + this.scene.field.add(pokemon); + if (!this.player) { + const playerPokemon = this.scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + this.scene.field.moveBelow(pokemon, playerPokemon); + } + this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id); + } + this.scene.updateModifiers(this.player); + this.scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.75); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + this.scene.updateFieldScale(); + pokemon.x += 16; + pokemon.y -= 20; + pokemon.alpha = 0; + + // Ease pokemon in + this.scene.tweens.add({ + targets: pokemon, + x: "-=16", + y: "+=16", + alpha: 1, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + this.scene.updateFieldScale(); + this.scene.time.delayedCall(1000, () => this.end()); + } + }); + } + onEnd(): void { const pokemon = this.getPokemon(); @@ -176,7 +240,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { + if (!this.loaded || [BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.scene.currentBattle.battleType) || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index 55b2a1608c0f..e925f0c47d48 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -30,7 +30,7 @@ export class TrainerVictoryPhase extends BattlePhase { const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM][vouchers[TrainerType[trainerType]].voucherType])); } } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index 568cfdc57140..92547878f126 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -9,6 +9,7 @@ import { CommandPhase } from "./command-phase"; import { EnemyCommandPhase } from "./enemy-command-phase"; import { GameOverPhase } from "./game-over-phase"; import { TurnStartPhase } from "./turn-start-phase"; +import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -46,6 +47,14 @@ export class TurnInitPhase extends FieldPhase { //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); + handleMysteryEncounterBattleStartEffects(this.scene); + + // If true, will skip remainder of current phase (and not queue CommandPhases etc.) + if (handleMysteryEncounterTurnStartEffects(this.scene)) { + this.end(); + return; + } + this.scene.getField().forEach((pokemon, i) => { if (pokemon?.isActive()) { if (pokemon.isPlayer()) { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 9679a79a37d3..c11dd80b3aab 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -1,24 +1,25 @@ import BattleScene from "#app/battle-scene"; import { BattlerIndex, BattleType } from "#app/battle"; import { modifierTypes } from "#app/modifier/modifier-type"; -import { ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; -import * as Utils from "#app/utils"; -import Overrides from "#app/overrides"; import { BattleEndPhase } from "./battle-end-phase"; import { NewBattlePhase } from "./new-battle-phase"; import { PokemonPhase } from "./pokemon-phase"; import { AddEnemyBuffModifierPhase } from "./add-enemy-buff-modifier-phase"; import { EggLapsePhase } from "./egg-lapse-phase"; -import { ExpPhase } from "./exp-phase"; import { GameOverPhase } from "./game-over-phase"; import { ModifierRewardPhase } from "./modifier-reward-phase"; import { SelectModifierPhase } from "./select-modifier-phase"; -import { ShowPartyExpBarPhase } from "./show-party-exp-bar-phase"; import { TrainerVictoryPhase } from "./trainer-victory-phase"; +import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class VictoryPhase extends PokemonPhase { - constructor(scene: BattleScene, battlerIndex: BattlerIndex) { + /** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */ + isExpOnly: boolean; + + constructor(scene: BattleScene, battlerIndex: BattlerIndex | integer, isExpOnly: boolean = false) { super(scene, battlerIndex); + + this.isExpOnly = isExpOnly; } start() { @@ -26,88 +27,15 @@ export class VictoryPhase extends PokemonPhase { this.scene.gameData.gameStats.pokemonDefeated++; - const participantIds = this.scene.currentBattle.playerParticipantIds; - const party = this.scene.getParty(); - const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; - const expBalanceModifier = this.scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const multipleParticipantExpBonusModifier = this.scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; - const nonFaintedPartyMembers = party.filter(p => p.hp); - const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.scene.getMaxExpLevel()); - const partyMemberExp: number[] = []; + const expValue = this.getPokemon().getExpValue(); + this.scene.applyPartyExp(expValue, true); - if (participantIds.size) { - let expValue = this.getPokemon().getExpValue(); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - expValue = Math.floor(expValue * 1.5); - } - for (const partyMember of nonFaintedPartyMembers) { - const pId = partyMember.id; - const participated = participantIds.has(pId); - if (participated) { - partyMember.addFriendship(2); - } - if (!expPartyMembers.includes(partyMember)) { - continue; - } - if (!participated && !expShareModifier) { - partyMemberExp.push(0); - continue; - } - let expMultiplier = 0; - if (participated) { - expMultiplier += (1 / participantIds.size); - if (participantIds.size > 1 && multipleParticipantExpBonusModifier) { - expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; - } - } else if (expShareModifier) { - expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.size; - } - if (partyMember.pokerus) { - expMultiplier *= 1.5; - } - if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { - expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; - } - const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); - this.scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); - partyMemberExp.push(Math.floor(pokemonExp.value)); - } - - if (expBalanceModifier) { - let totalLevel = 0; - let totalExp = 0; - expPartyMembers.forEach((expPartyMember, epm) => { - totalExp += partyMemberExp[epm]; - totalLevel += expPartyMember.level; - }); - - const medianLevel = Math.floor(totalLevel / expPartyMembers.length); - - const recipientExpPartyMemberIndexes: number[] = []; - expPartyMembers.forEach((expPartyMember, epm) => { - if (expPartyMember.level <= medianLevel) { - recipientExpPartyMemberIndexes.push(epm); - } - }); - - const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); - - expPartyMembers.forEach((_partyMember, pm) => { - partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); - }); - } - - for (let pm = 0; pm < expPartyMembers.length; pm++) { - const exp = partyMemberExp[pm]; - - if (exp) { - const partyMemberIndex = party.indexOf(expPartyMembers[pm]); - this.scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(this.scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(this.scene, partyMemberIndex, exp)); - } - } + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); + return this.end(); } - if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) { + if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) { this.scene.pushPhase(new BattleEndPhase(this.scene)); if (this.scene.currentBattle.battleType === BattleType.TRAINER) { this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); diff --git a/src/pipelines/sprite.ts b/src/pipelines/sprite.ts index c9a76dc50a40..88d6ce2d3871 100644 --- a/src/pipelines/sprite.ts +++ b/src/pipelines/sprite.ts @@ -4,6 +4,7 @@ import Pokemon from "../field/pokemon"; import Trainer from "../field/trainer"; import FieldSpritePipeline from "./field-sprite"; import * as Utils from "../utils"; +import MysteryEncounterIntroVisuals from "../field/mystery-encounter-intro"; const spriteFragShader = ` #ifdef GL_FRAGMENT_PRECISION_HIGH @@ -37,6 +38,7 @@ uniform vec2 texFrameUv; uniform vec2 size; uniform vec2 texSize; uniform float yOffset; +uniform float yShadowOffset; uniform vec4 tone; uniform ivec4 baseVariantColors[32]; uniform vec4 variantColors[32]; @@ -251,7 +253,7 @@ void main() { float width = size.x - (yOffset / 2.0); float spriteX = ((floor(outPosition.x / fieldScale) - relPosition.x) / width) + 0.5; - float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y) / size.y); + float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y - yShadowOffset) / size.y); if (yCenter == 1) { spriteY += 0.5; @@ -338,6 +340,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", 0, 0); this.set2f("texSize", 0, 0); this.set1f("yOffset", 0); + this.set1f("yShadowOffset", 0); this.set4fv("tone", this._tone); } @@ -350,10 +353,11 @@ export default class SpritePipeline extends FieldSpritePipeline { const tone = data["tone"] as number[]; const teraColor = data["teraColor"] as integer[] ?? [ 0, 0, 0 ]; const hasShadow = data["hasShadow"] as boolean; + const yShadowOffset = data["yShadowOffset"] as number; const ignoreFieldPos = data["ignoreFieldPos"] as boolean; const ignoreOverride = data["ignoreOverride"] as boolean; - const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer; + const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; const position = isEntityObj ? [ sprite.parentContainer.x, sprite.parentContainer.y ] @@ -376,6 +380,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", sprite.frame.width, sprite.height); this.set2f("texSize", sprite.texture.source[0].width, sprite.texture.source[0].height); this.set1f("yOffset", sprite.height - sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); + this.set1f("yShadowOffset", yShadowOffset ?? 0); this.set4fv("tone", tone); this.bindTexture(this.game.textures.get("tera").source[0].glTexture!, 1); // TODO: is this bang correct? @@ -447,14 +452,15 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set1f("vCutoff", v1); const hasShadow = sprite.pipelineData["hasShadow"] as boolean; + const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number ?? 0; if (hasShadow) { - const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer; + const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; const fieldScaleRatio = field.scale / 6; const baseY = (isEntityObj ? sprite.parentContainer.y : sprite.y + sprite.height) * 6 / fieldScaleRatio; - const bottomPadding = Math.ceil(sprite.height * 0.05) * 6 / fieldScaleRatio; + const bottomPadding = Math.ceil(sprite.height * 0.05 + Math.max(yShadowOffset, 0)) * 6 / fieldScaleRatio; const yDelta = (baseY - y1) / field.scale; y2 = y1 = baseY + bottomPadding; const pixelHeight = (v1 - v0) / (sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 705fd5143a41..ec3fe93c7651 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -167,6 +167,25 @@ export async function initI18n(): Promise { postProcess: ["korean-postposition"], }); + // Input: {{myMoneyValue, money}} + // Output: @[MONEY]{₽100,000,000} (useful for BBCode coloring of text) + // If you don't want the BBCode tag applied, just use 'number' formatter + i18next.services.formatter?.add("money", (value, lng, options) => { + const numberFormattedString = Intl.NumberFormat(lng, options).format(value); + switch (lng) { + case "ja": + return `@[MONEY]{${numberFormattedString}}円`; + case "de": + case "es": + case "fr": + case "it": + return `@[MONEY]{${numberFormattedString} ₽}`; + default: + // English and other languages that use same format + return `@[MONEY]{₽${numberFormattedString}}`; + } + }); + await initFonts(localStorage.getItem("prLang") ?? undefined); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 677bbe4add6a..04fef4a81da4 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -46,6 +46,8 @@ import { OutdatedPhase } from "#app/phases/outdated-phase"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; import { applySessionDataPatches, applySettingsDataPatches, applySystemDataPatches } from "./version-converter"; +import { MysteryEncounterSaveData } from "../data/mystery-encounters/mystery-encounter-save-data"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -130,6 +132,8 @@ export interface SessionSaveData { gameVersion: string; timestamp: integer; challenges: ChallengeData[]; + mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME, + mysteryEncounterSaveData: MysteryEncounterSaveData; } interface Unlocks { @@ -947,7 +951,9 @@ export class GameData { trainer: scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null, gameVersion: scene.game.config.gameVersion, timestamp: new Date().getTime(), - challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)) + challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: scene.currentBattle.mysteryEncounter?.encounterType ?? -1, + mysteryEncounterSaveData: scene.mysteryEncounterSaveData } as SessionSaveData; } @@ -1038,11 +1044,14 @@ export class GameData { scene.score = sessionData.score; scene.updateScoreText(); + scene.mysteryEncounterSaveData = new MysteryEncounterSaveData(sessionData.mysteryEncounterSaveData); + scene.newArena(sessionData.arena.biome); const battleType = sessionData.battleType || 0; const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null; - const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1)!; // TODO: is this bang correct? + const mysteryEncounterType = sessionData.mysteryEncounterType !== -1 ? sessionData.mysteryEncounterType : undefined; + const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterType)!; // TODO: is this bang correct? battle.enemyLevels = sessionData.enemyParty.map(p => p.level); scene.arena.init(); @@ -1254,6 +1263,14 @@ export class GameData { return ret; } + if (k === "mysteryEncounterType") { + return v as MysteryEncounterType; + } + + if (k === "mysteryEncounterSaveData") { + return new MysteryEncounterSaveData(v); + } + return v; }) as SessionSaveData; @@ -1538,12 +1555,28 @@ export class GameData { } } - setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { + /** + * + * @param pokemon + * @param incrementCount + * @param fromEgg + * @param showMessage + * @returns `true` if Pokemon catch unlocked a new starter, `false` if Pokemon catch did not unlock a starter + */ + setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg, showMessage); } - setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { - return new Promise(resolve => { + /** + * + * @param pokemon + * @param incrementCount + * @param fromEgg + * @param showMessage + * @returns `true` if Pokemon catch unlocked a new starter, `false` if Pokemon catch did not unlock a starter + */ + setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { + return new Promise(resolve => { const dexEntry = this.dexData[species.speciesId]; const caughtAttr = dexEntry.caughtAttr; const formIndex = pokemon.formIndex; @@ -1598,24 +1631,24 @@ export class GameData { } } - const checkPrevolution = () => { + const checkPrevolution = (newStarter: boolean) => { if (hasPrevolution) { const prevolutionSpecies = pokemonPrevolutions[species.speciesId]; - this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showMessage).then(() => resolve()); + this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showMessage).then(result => resolve(result)); } else { - resolve(); + resolve(newStarter); } }; if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { if (!showMessage) { - resolve(); + resolve(true); return; } this.scene.playSound("level_up_fanfare"); - this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true); + this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(true), null, true); } else { - checkPrevolution(); + checkPrevolution(false); } }); } @@ -1657,7 +1690,14 @@ export class GameData { this.starterData[species.speciesId].candyCount += count; } - setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, showMessage: boolean = true): Promise { + /** + * + * @param species + * @param eggMoveIndex + * @param showMessage Default true. If true, will display message for unlocked egg move + * @param prependSpeciesToMessage Default false. If true, will change message from "X Egg Move Unlocked!" to "Bulbasaur X Egg Move Unlocked!" + */ + setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, showMessage: boolean = true, prependSpeciesToMessage: boolean = false): Promise { return new Promise(resolve => { const speciesId = species.speciesId; if (!speciesEggMoves.hasOwnProperty(speciesId) || !speciesEggMoves[speciesId][eggMoveIndex]) { @@ -1683,9 +1723,10 @@ export class GameData { } this.scene.playSound("level_up_fanfare"); const moveName = allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name; - this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, (() => { - resolve(true); - }), null, true); + let message = prependSpeciesToMessage ? species.getName() + " " : ""; + message += eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }); + + this.scene.ui.showText(message, null, () => resolve(true), null, true); }); } diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 1fafcbf8acc3..5e6c0d93c8c6 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -12,6 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export default class PokemonData { public id: integer; @@ -43,6 +44,7 @@ export default class PokemonData { public pauseEvolutions: boolean; public pokerus: boolean; public usedTMs: Moves[]; + public evoCounter: integer; public fusionSpecies: Species; public fusionFormIndex: integer; @@ -56,6 +58,8 @@ export default class PokemonData { public bossSegments?: integer; public summonData: PokemonSummonData; + /** Data that can customize a Pokemon in non-standard ways from its Species */ + public mysteryEncounterPokemonData: MysteryEncounterPokemonData; constructor(source: Pokemon | any, forHistory: boolean = false) { const sourcePokemon = source instanceof Pokemon ? source : null; @@ -92,6 +96,8 @@ export default class PokemonData { } 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; @@ -101,6 +107,8 @@ export default class PokemonData { this.fusionLuck = source.fusionLuck !== undefined ? source.fusionLuck : (source.fusionShiny ? source.fusionVariant + 1 : 0); this.usedTMs = source.usedTMs ?? []; + this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(source.mysteryEncounterPokemonData); + if (!forHistory) { this.boss = (source instanceof EnemyPokemon && !!source.bossSegments) || (!this.player && !!source.boss); this.bossSegments = source.bossSegments; diff --git a/src/test/arena/grassy_terrain.test.ts b/src/test/arena/grassy_terrain.test.ts new file mode 100644 index 000000000000..e80646767415 --- /dev/null +++ b/src/test/arena/grassy_terrain.test.ts @@ -0,0 +1,56 @@ +import { allMoves } 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, expect, it, vi } from "vitest"; + +describe("Arena - Grassy Terrain", () => { + 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 + .battleType("single") + .disableCrits() + .enemyLevel(30) + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .moveset([Moves.GRASSY_TERRAIN, Moves.EARTHQUAKE]) + .ability(Abilities.BALL_FETCH); + }); + + it("halves the damage of Earthquake", async () => { + await game.classicMode.startBattle([Species.FEEBAS]); + + const eq = allMoves[Moves.EARTHQUAKE]; + vi.spyOn(eq, "calculateBattlePower"); + + game.move.select(Moves.EARTHQUAKE); + await game.toNextTurn(); + + expect(eq.calculateBattlePower).toHaveReturnedWith(100); + + game.move.select(Moves.GRASSY_TERRAIN); + await game.toNextTurn(); + + game.move.select(Moves.EARTHQUAKE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(eq.calculateBattlePower).toHaveReturnedWith(50); + }, TIMEOUT); +}); diff --git a/src/test/battle/damage_calculation.test.ts b/src/test/battle/damage_calculation.test.ts index 89f2bb4c2695..a348df6c0855 100644 --- a/src/test/battle/damage_calculation.test.ts +++ b/src/test/battle/damage_calculation.test.ts @@ -1,14 +1,13 @@ -import { DamagePhase } from "#app/phases/damage-phase"; -import { toDmgValue } from "#app/utils"; +import { allMoves } from "#app/data/move"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Round Down and Minimun 1 test in Damage Calculation", () => { +describe("Battle Mechanics - Damage Calculation", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -24,24 +23,86 @@ describe("Round Down and Minimun 1 test in Damage Calculation", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override.battleType("single"); - game.override.startingLevel(10); + game.override + .battleType("single") + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingLevel(100) + .enemyLevel(100) + .disableCrits() + .moveset([Moves.TACKLE, Moves.DRAGON_RAGE, Moves.FISSURE, Moves.JUMP_KICK]); + }); + + it("Tackle deals expected base damage", async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(playerPokemon, "getEffectiveStat").mockReturnValue(80); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getEffectiveStat").mockReturnValue(90); + + // expected base damage = [(2*level/5 + 2) * power * playerATK / enemyDEF / 50] + 2 + // = 31.8666... + expect(enemyPokemon.getAttackDamage(playerPokemon, allMoves[Moves.TACKLE]).damage).toBeCloseTo(31); + }); + + it("Attacks deal 1 damage at minimum", async () => { + game.override + .startingLevel(1) + .enemySpecies(Species.AGGRON); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const aggron = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.TACKLE); + + await game.phaseInterceptor.to("BerryPhase", false); + + // Lvl 1 0 Atk Magikarp Tackle vs. 0 HP / 0 Def Aggron: 1-1 (0.3 - 0.3%) -- possibly the worst move ever + expect(aggron.hp).toBe(aggron.getMaxHp() - 1); + }); + + it("Fixed-damage moves ignore damage multipliers", async () => { + game.override + .enemySpecies(Species.DRAGONITE) + .enemyAbility(Abilities.MULTISCALE); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const magikarp = game.scene.getPlayerPokemon()!; + const dragonite = game.scene.getEnemyPokemon()!; + + expect(dragonite.getAttackDamage(magikarp, allMoves[Moves.DRAGON_RAGE]).damage).toBe(40); + }); + + it("One-hit KO moves ignore damage multipliers", async () => { + game.override + .enemySpecies(Species.AGGRON) + .enemyAbility(Abilities.MULTISCALE); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const magikarp = game.scene.getPlayerPokemon()!; + const aggron = game.scene.getEnemyPokemon()!; + + expect(aggron.getAttackDamage(magikarp, allMoves[Moves.FISSURE]).damage).toBe(aggron.hp); }); it("When the user fails to use Jump Kick with Wonder Guard ability, the damage should be 1.", async () => { - game.override.enemySpecies(Species.GASTLY); - game.override.enemyMoveset(Moves.SPLASH); - game.override.starterSpecies(Species.SHEDINJA); - game.override.moveset([Moves.JUMP_KICK]); - game.override.ability(Abilities.WONDER_GUARD); + game.override + .enemySpecies(Species.GASTLY) + .ability(Abilities.WONDER_GUARD); - await game.startBattle(); + await game.classicMode.startBattle([Species.SHEDINJA]); const shedinja = game.scene.getPlayerPokemon()!; game.move.select(Moves.JUMP_KICK); - await game.phaseInterceptor.to(DamagePhase); + await game.phaseInterceptor.to("DamagePhase"); expect(shedinja.hp).toBe(shedinja.getMaxHp() - 1); }); @@ -49,21 +110,19 @@ describe("Round Down and Minimun 1 test in Damage Calculation", () => { it("Charizard with odd HP survives Stealth Rock damage twice", async () => { game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0); - game.override.seed("Charizard Stealth Rock test"); - game.override.enemySpecies(Species.CHARIZARD); - game.override.enemyAbility(Abilities.BLAZE); - game.override.starterSpecies(Species.PIKACHU); - game.override.enemyLevel(100); + game.override + .seed("Charizard Stealth Rock test") + .enemySpecies(Species.CHARIZARD) + .enemyAbility(Abilities.BLAZE); - await game.startBattle(); + await game.classicMode.startBattle([Species.PIKACHU]); const charizard = game.scene.getEnemyPokemon()!; - const maxHp = charizard.getMaxHp(); - const damage_prediction = toDmgValue(charizard.getMaxHp() / 2); - const currentHp = charizard.hp; - const expectedHP = maxHp - damage_prediction; - - expect(currentHp).toBe(expectedHP); + if (charizard.getMaxHp() % 2 === 1) { + expect(charizard.hp).toBeGreaterThan(charizard.getMaxHp() / 2); + } else { + expect(charizard.hp).toBe(charizard.getMaxHp() / 2); + } }); }); diff --git a/src/test/battlerTags/substitute.test.ts b/src/test/battlerTags/substitute.test.ts index 1ce81850c133..12888daca50a 100644 --- a/src/test/battlerTags/substitute.test.ts +++ b/src/test/battlerTags/substitute.test.ts @@ -1,8 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import Pokemon, { MoveResult, PokemonTurnData, TurnMove, PokemonMove } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; -import { BattlerTagLapseType, SubstituteTag, TrappedTag } from "#app/data/battler-tags"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BattlerTagLapseType, BindTag, SubstituteTag } from "#app/data/battler-tags"; import { Moves } from "#app/enums/moves"; import { PokemonAnimType } from "#app/enums/pokemon-anim-type"; import * as messages from "#app/messages"; @@ -25,7 +24,7 @@ describe("BattlerTag - SubstituteTag", () => { getMaxHp: vi.fn().mockReturnValue(101) as Pokemon["getMaxHp"], findAndRemoveTags: vi.fn().mockImplementation((tagFilter) => { // simulate a Trapped tag set by another Pokemon, then expect the filter to catch it. - const trapTag = new TrappedTag(BattlerTagType.TRAPPED, BattlerTagLapseType.CUSTOM, 0, Moves.NONE, 1); + const trapTag = new BindTag(5, 0); expect(tagFilter(trapTag)).toBeTruthy(); return true; }) as Pokemon["findAndRemoveTags"] diff --git a/src/test/enemy_command.test.ts b/src/test/enemy_command.test.ts new file mode 100644 index 000000000000..9a2caa56dfc8 --- /dev/null +++ b/src/test/enemy_command.test.ts @@ -0,0 +1,106 @@ +import { allMoves, MoveCategory } from "#app/data/move"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { AiType, EnemyPokemon } from "#app/field/pokemon"; +import { randSeedInt } from "#app/utils"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 20 * 1000; +const NUM_TRIALS = 300; + +type MoveChoiceSet = { [key: number]: number }; + +function getEnemyMoveChoices(pokemon: EnemyPokemon, moveChoices: MoveChoiceSet): void { + // Use an unseeded random number generator in place of the mocked-out randBattleSeedInt + vi.spyOn(pokemon.scene, "randBattleSeedInt").mockImplementation((range, min?) => { + return randSeedInt(range, min); + }); + for (let i = 0; i < NUM_TRIALS; i++) { + const queuedMove = pokemon.getNextMove(); + moveChoices[queuedMove.move]++; + } + + for (const [moveId, count] of Object.entries(moveChoices)) { + console.log(`Move: ${allMoves[moveId].name} Count: ${count} (${count / NUM_TRIALS * 100}%)`); + } +} + +describe("Enemy Commands - Move Selection", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + game.override + .ability(Abilities.BALL_FETCH) + .enemyAbility(Abilities.BALL_FETCH); + }); + + it( + "should never use Status moves if an attack can KO", + async () => { + game.override + .enemySpecies(Species.ETERNATUS) + .enemyMoveset([Moves.ETERNABEAM, Moves.SLUDGE_BOMB, Moves.DRAGON_DANCE, Moves.COSMIC_POWER]) + .startingLevel(1) + .enemyLevel(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.aiType = AiType.SMART_RANDOM; + + const moveChoices: MoveChoiceSet = {}; + const enemyMoveset = enemyPokemon.getMoveset(); + enemyMoveset.forEach(mv => moveChoices[mv!.moveId] = 0); + getEnemyMoveChoices(enemyPokemon, moveChoices); + + enemyMoveset.forEach(mv => { + if (mv?.getMove().category === MoveCategory.STATUS) { + expect(moveChoices[mv.moveId]).toBe(0); + } + }); + }, TIMEOUT + ); + + it( + "should not select Last Resort if it would fail, even if the move KOs otherwise", + async () => { + game.override + .enemySpecies(Species.KANGASKHAN) + .enemyMoveset([Moves.LAST_RESORT, Moves.GIGA_IMPACT, Moves.SPLASH, Moves.SWORDS_DANCE]) + .startingLevel(1) + .enemyLevel(100); + + await game.classicMode.startBattle([Species.MAGIKARP]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.aiType = AiType.SMART_RANDOM; + + const moveChoices: MoveChoiceSet = {}; + const enemyMoveset = enemyPokemon.getMoveset(); + enemyMoveset.forEach(mv => moveChoices[mv!.moveId] = 0); + getEnemyMoveChoices(enemyPokemon, moveChoices); + + enemyMoveset.forEach(mv => { + if (mv?.getMove().category === MoveCategory.STATUS || mv?.moveId === Moves.LAST_RESORT) { + expect(moveChoices[mv.moveId]).toBe(0); + } + }); + }, TIMEOUT + ); +}); diff --git a/src/test/moves/baneful_bunker.test.ts b/src/test/moves/baneful_bunker.test.ts new file mode 100644 index 000000000000..c4a3036565c8 --- /dev/null +++ b/src/test/moves/baneful_bunker.test.ts @@ -0,0 +1,93 @@ +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, test } from "vitest"; +import GameManager from "../utils/gameManager"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { BattlerIndex } from "#app/battle"; +import { StatusEffect } from "#app/enums/status-effect"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Baneful Bunker", () => { + 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"); + + game.override.moveset(Moves.SLASH); + + game.override.enemySpecies(Species.SNORLAX); + game.override.enemyAbility(Abilities.INSOMNIA); + game.override.enemyMoveset(Moves.BANEFUL_BUNKER); + + game.override.startingLevel(100); + game.override.enemyLevel(100); + }); + test( + "should protect the user and poison attackers that make contact", + async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy(); + }, TIMEOUT + ); + test( + "should protect the user and poison attackers that make contact, regardless of accuracy checks", + async () => { + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeTruthy(); + }, TIMEOUT + ); + + test( + "should not poison attackers that don't make contact", + async () => { + game.override.moveset(Moves.FLASH_CANNON); + await game.classicMode.startBattle([Species.CHARIZARD]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FLASH_CANNON); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + await game.move.forceMiss(); + await game.phaseInterceptor.to("BerryPhase", false); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + expect(leadPokemon.status?.effect === StatusEffect.POISON).toBeFalsy(); + }, TIMEOUT + ); +}); diff --git a/src/test/moves/obstruct.test.ts b/src/test/moves/obstruct.test.ts index 539b11090dec..eb12daa785de 100644 --- a/src/test/moves/obstruct.test.ts +++ b/src/test/moves/obstruct.test.ts @@ -43,6 +43,23 @@ describe("Moves - Obstruct", () => { expect(enemy.getStatStage(Stat.DEF)).toBe(-2); }, TIMEOUT); + it("bypasses accuracy checks when applying protection and defense reduction", async () => { + game.override.enemyMoveset(Array(4).fill(Moves.ICE_PUNCH)); + await game.classicMode.startBattle(); + + game.move.select(Moves.OBSTRUCT); + await game.phaseInterceptor.to("MoveEffectPhase"); + await game.move.forceMiss(); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(player.isFullHp()).toBe(true); + expect(enemy.getStatStage(Stat.DEF)).toBe(-2); + }, TIMEOUT + ); + it("protects from non-contact damaging moves and doesn't lower the opponent's defense by 2 stages", async () => { game.override.enemyMoveset(Array(4).fill(Moves.WATER_GUN)); await game.classicMode.startBattle(); diff --git a/src/test/moves/roost.test.ts b/src/test/moves/roost.test.ts index df7fc7096b09..c1fc962e876c 100644 --- a/src/test/moves/roost.test.ts +++ b/src/test/moves/roost.test.ts @@ -1,3 +1,4 @@ +import { BattlerIndex } from "#app/battle"; import { Type } from "#app/data/type"; import { BattlerTagType } from "#app/enums/battler-tag-type"; import { Moves } from "#app/enums/moves"; @@ -29,10 +30,9 @@ describe("Moves - Roost", () => { game.override.battleType("single"); game.override.enemySpecies(Species.RELICANTH); game.override.startingLevel(100); - game.override.enemyLevel(60); + game.override.enemyLevel(100); game.override.enemyMoveset(Moves.EARTHQUAKE); game.override.moveset([Moves.ROOST, Moves.BURN_UP, Moves.DOUBLE_SHOCK]); - game.override.starterForms({ [Species.ROTOM]: 4 }); }); /** @@ -55,6 +55,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be normal type, and NOT flying type @@ -81,6 +82,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be normal type, and NOT flying type @@ -108,6 +110,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure fighting type and grounded @@ -131,13 +134,15 @@ describe("Moves - Roost", () => { test( "Pokemon with levitate after using roost should lose flying type but still be unaffected by ground moves", async () => { + game.override.starterForms({ [Species.ROTOM]: 4 }); await game.classicMode.startBattle([Species.ROTOM]); const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); - // Should only be pure fighting type and grounded + // Should only be pure eletric type and grounded let playerPokemonTypes = playerPokemon.getTypes(); expect(playerPokemonTypes[0] === Type.ELECTRIC).toBeTruthy(); expect(playerPokemonTypes.length === 1).toBeTruthy(); @@ -145,7 +150,7 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); - // Should have lost HP and is now back to being fighting/flying + // Should have lost HP and is now back to being electric/flying playerPokemonTypes = playerPokemon.getTypes(); expect(playerPokemon.hp).toBe(playerPokemonStartingHP); expect(playerPokemonTypes[0] === Type.ELECTRIC).toBeTruthy(); @@ -162,6 +167,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.BURN_UP); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure flying type after burn up @@ -171,6 +177,7 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded @@ -200,6 +207,7 @@ describe("Moves - Roost", () => { const playerPokemon = game.scene.getPlayerPokemon()!; const playerPokemonStartingHP = playerPokemon.hp; game.move.select(Moves.DOUBLE_SHOCK); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be pure flying type after burn up @@ -209,6 +217,7 @@ describe("Moves - Roost", () => { await game.phaseInterceptor.to(TurnEndPhase); game.move.select(Moves.ROOST); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to(MoveEffectPhase); // Should only be typeless type after roost and is grounded diff --git a/src/test/mystery-encounter/encounter-test-utils.ts b/src/test/mystery-encounter/encounter-test-utils.ts new file mode 100644 index 000000000000..a31ee150bfde --- /dev/null +++ b/src/test/mystery-encounter/encounter-test-utils.ts @@ -0,0 +1,176 @@ +import { Button } from "#app/enums/buttons"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; +import { Mode } from "#app/ui/ui"; +import GameManager from "../utils/gameManager"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { expect, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { isNullOrUndefined } from "#app/utils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; +import { MessagePhase } from "#app/phases/message-phase"; + +/** + * Runs a {@linkcode MysteryEncounter} to either the start of a battle, or to the {@linkcode MysteryEncounterRewardsPhase}, depending on the option selected + * @param game + * @param optionNo Human number, not index + * @param secondaryOptionSelect + * @param isBattle If selecting option should lead to battle, set to `true` + */ +export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect?: { pokemonNo: number, optionNo?: number }, isBattle: boolean = false) { + vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption"); + await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect); + + // run the selected options phase + game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase)); + + if (isBattle) { + game.onNextPrompt("DamagePhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + // If a battle is started, fast forward to end of the battle + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); + game.endPhase(); + }); + + // Handle end of battle trainer messages + game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle egg hatch dialogue + game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + await game.phaseInterceptor.to(CommandPhase); + } else { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } +} + +export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, secondaryOptionSelect?: { pokemonNo: number, optionNo?: number }) { + // Handle any eventual queued messages (e.g. weather phase, etc.) + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); + + if (game.isCurrentPhase(MessagePhase)) { + await game.phaseInterceptor.run(MessagePhase); + } + + // dispose of intro messages + game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); + + await game.phaseInterceptor.to(MysteryEncounterPhase, true); + + // select the desired option + const uiHandler = game.scene.ui.getHandler(); + uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that + + switch (optionNo) { + default: + case 1: + // no movement needed. Default cursor position + break; + case 2: + uiHandler.processInput(Button.RIGHT); + break; + case 3: + uiHandler.processInput(Button.DOWN); + break; + case 4: + uiHandler.processInput(Button.RIGHT); + uiHandler.processInput(Button.DOWN); + break; + } + + if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { + await handleSecondaryOptionSelect(game, secondaryOptionSelect!.pokemonNo, secondaryOptionSelect!.optionNo); + } else { + uiHandler.processInput(Button.ACTION); + } +} + +async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo?: number) { + // Handle secondary option selections + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + + const encounterUiHandler = game.scene.ui.getHandler(); + encounterUiHandler.processInput(Button.ACTION); + + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + + for (let i = 1; i < pokemonNo; i++) { + partyUiHandler.processInput(Button.DOWN); + } + + // Open options on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon options + partyUiHandler.processInput(Button.ACTION); + + // If there is a second choice to make after selecting a Pokemon + if (!isNullOrUndefined(optionNo)) { + // Wait for Summary menu to close and second options to spawn + const secondOptionUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(secondOptionUiHandler, "show"); + await vi.waitFor(() => expect(secondOptionUiHandler.show).toHaveBeenCalled()); + + // Navigate down to the correct option + for (let i = 1; i < optionNo!; i++) { + secondOptionUiHandler.processInput(Button.DOWN); + } + + // Select the option + secondOptionUiHandler.processInput(Button.ACTION); + } +} + +/** + * For any {@linkcode MysteryEncounter} that has a battle, can call this to skip battle and proceed to {@linkcode MysteryEncounterRewardsPhase} + * @param game + * @param runRewardsPhase + */ +export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase: boolean = true) { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.getEnemyParty().forEach(p => { + p.hp = 0; + p.status = new Status(StatusEffect.FAINT); + game.scene.field.remove(p); + }); + game.scene.pushPhase(new VictoryPhase(game.scene, 0)); + game.phaseInterceptor.superEndPhase(); + game.setMode(Mode.MESSAGE); + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase, runRewardsPhase); +} diff --git a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts new file mode 100644 index 000000000000..b4cc186864c7 --- /dev/null +++ b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -0,0 +1,205 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; +import { EggTier } from "#enums/egg-type"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; + +const namespace = "mysteryEncounter:aTrainersTest"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("A Trainer's Test - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.A_TRAINERS_TEST]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + expect(ATrainersTestEncounter.encounterType).toBe(MysteryEncounterType.A_TRAINERS_TEST); + expect(ATrainersTestEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(ATrainersTestEncounter.dialogue).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro?.[0].speaker).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro?.[0].text).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(ATrainersTestEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(ATrainersTestEncounter.options.length).toBe(2); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.A_TRAINERS_TEST); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ATrainersTestEncounter; + + const { onInit } = ATrainersTestEncounter; + + expect(ATrainersTestEncounter.onInit).toBeDefined(); + + ATrainersTestEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(ATrainersTestEncounter.dialogueTokens?.statTrainerName).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerType).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerNameKey).toBeDefined(); + expect(ATrainersTestEncounter.misc.trainerEggDescription).toBeDefined(); + expect(ATrainersTestEncounter.dialogue.intro).toBeDefined(); + expect(ATrainersTestEncounter.options[1].dialogue?.selected).toBeDefined(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Accept the Challenge", () => { + it("should have the correct properties", () => { + const option = ATrainersTestEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue!.buttonLabel).toStrictEqual(`${namespace}.option.1.label`); + expect(option.dialogue!.buttonTooltip).toStrictEqual(`${namespace}.option.1.tooltip`); + }); + + it("Should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(["buck", "cheryl", "marley", "mira", "riley"].includes(scene.currentBattle.trainer!.config.name.toLowerCase())).toBeTruthy(); + expect(enemyField[0]).toBeDefined(); + }); + + it("Should reward the player with an Epic or Legendary egg", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + 1).toBe(eggsAfter.length); + const eggTier = eggsAfter[eggsAfter.length - 1].tier; + expect(eggTier === EggTier.ULTRA || eggTier === EggTier.MASTER).toBeTruthy(); + }); + }); + + describe("Option 2 - Decline the Challenge", () => { + beforeEach(() => { + // Mock sound object + vi.spyOn(scene, "playSoundWithoutBgm").mockImplementation(() => { + return { + totalDuration: 1, + destroy: () => null + } as any; + }); + }); + + it("should have the correct properties", () => { + const option = ATrainersTestEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue?.buttonLabel).toStrictEqual(`${namespace}.option.2.label`); + expect(option.dialogue?.buttonTooltip).toStrictEqual(`${namespace}.option.2.tooltip`); + }); + + it("Should fully heal the party", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const partyHealPhases = phaseSpy.mock.calls.filter(p => p[0] instanceof PartyHealPhase).map(p => p[0]); + expect(partyHealPhases.length).toBe(1); + }); + + it("Should reward the player with a Rare egg", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + + const eggsBefore = scene.gameData.eggs; + expect(eggsBefore).toBeDefined(); + const eggsBeforeLength = eggsBefore.length; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const eggsAfter = scene.gameData.eggs; + expect(eggsAfter).toBeDefined(); + expect(eggsBeforeLength + 1).toBe(eggsAfter.length); + const eggTier = eggsAfter[eggsAfter.length - 1].tier; + expect(eggTier).toBe(EggTier.GREAT); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.A_TRAINERS_TEST, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts new file mode 100644 index 000000000000..7cca7abba27b --- /dev/null +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -0,0 +1,270 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; +import { Moves } from "#enums/moves"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:absoluteAvarice"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 45; + +describe("Absolute Avarice - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.PLAINS, [MysteryEncounterType.ABSOLUTE_AVARICE]], + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(AbsoluteAvariceEncounter.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(AbsoluteAvariceEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(AbsoluteAvariceEncounter.dialogue).toBeDefined(); + expect(AbsoluteAvariceEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(AbsoluteAvariceEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should not spawn if player does not have enough berries", async () => { + scene.modifiers = []; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should spawn if player has enough berries", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should remove all player's berries at the start of the encounter", async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + }); + + describe("Option 1 - Fight the Greedent", () => { + it("should have the correct properties", () => { + const option1 = AbsoluteAvariceEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against Greedent", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.GREEDENT); + const moveset = enemyField[0].moveset.map(m => m?.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STUFF_CHEEKS).length).toBe(1); // Stuff Cheeks used before battle + }); + + it("should give reviver seed to each pokemon after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + for (const partyPokemon of scene.getParty()) { + const pokemonId = partyPokemon.id; + const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; + const revSeed = pokemonItems.find(i => i.type.name === "Reviver Seed"); + expect(revSeed).toBeDefined; + expect(revSeed?.stackCount).toBe(1); + } + }); + }); + + describe("Option 2 - Reason with It", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should return 3 (2/5ths floored) berries if 8 were stolen", {retry: 5}, async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 3, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(3); + }); + + it("Should return 2 (2/5ths floored) berries if 7 were stolen", {retry: 5}, async () => { + game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 2, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(2); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Let it have the food", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should add Greedent to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + const partyCountBefore = scene.getParty().length; + + await runMysteryEncounterToEnd(game, 3); + const partyCountAfter = scene.getParty().length; + + expect(partyCountBefore + 1).toBe(partyCountAfter); + const greedent = scene.getParty()[scene.getParty().length - 1]; + expect(greedent.species.speciesId).toBe(Species.GREEDENT); + const moveset = greedent.moveset.map(m => m?.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts new file mode 100644 index 000000000000..1c68852a63dc --- /dev/null +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -0,0 +1,259 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { AnOfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; +import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:offerYouCantRefuse"; +/** Gyarados for Indimidate */ +const defaultParty = [Species.GYARADOS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("An Offer You Can't Refuse - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + + expect(AnOfferYouCantRefuseEncounter.encounterType).toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); + expect(AnOfferYouCantRefuseEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(AnOfferYouCantRefuseEncounter.dialogue).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { speaker: `${namespace}.speaker`, text: `${namespace}.intro_dialogue` } + ]); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(AnOfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(AnOfferYouCantRefuseEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = AnOfferYouCantRefuseEncounter; + + const { onInit } = AnOfferYouCantRefuseEncounter; + + expect(AnOfferYouCantRefuseEncounter.onInit).toBeDefined(); + + AnOfferYouCantRefuseEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.strongestPokemon).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.price).toBeDefined(); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.option2PrimaryAbility).toBe("Intimidate"); + expect(AnOfferYouCantRefuseEncounter.dialogueTokens?.moveOrAbility).toBe("Intimidate"); + expect(AnOfferYouCantRefuseEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(AnOfferYouCantRefuseEncounter.misc?.price?.toString()).toBe(AnOfferYouCantRefuseEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Sell your Pokemon for money and a Shiny Charm", () => { + it("should have the correct properties", () => { + const option = AnOfferYouCantRefuseEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("Should give the player a Shiny Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof ShinyRateBoosterModifier) as ShinyRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier?.stackCount).toBe(1); + }); + + it("Should remove the Pokemon from the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; + + await runMysteryEncounterToEnd(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize - 1); + expect(scene.getParty().find(p => p.name === pokemonName)).toBeUndefined(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Extort the Kid", () => { + it("should have the correct properties", () => { + const option = AnOfferYouCantRefuseEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should award EXP to a pokemon with an ability in EXTORTION_ABILITIES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + const party = scene.getParty(); + const gyarados = party.find((pkm) => pkm.species.speciesId === Species.GYARADOS)!; + const expBefore = gyarados.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + expect(gyarados.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, [Species.ABRA]); + const party = scene.getParty(); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + abra.moveset = [new PokemonMove(Moves.BEAT_UP)]; + const expBefore = abra.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + expect(abra.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts new file mode 100644 index 000000000000..73ffad36258e --- /dev/null +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -0,0 +1,245 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { BerryModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:berriesAbound"; +const defaultParty = [Species.PYUKUMUKU]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Berries Abound - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + game.override.startingModifier([]); + game.override.startingHeldItems([]); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.BERRIES_ABOUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + expect(BerriesAboundEncounter.encounterType).toBe(MysteryEncounterType.BERRIES_ABOUND); + expect(BerriesAboundEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(BerriesAboundEncounter.dialogue).toBeDefined(); + expect(BerriesAboundEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(BerriesAboundEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BERRIES_ABOUND); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; + + const { onInit } = BerriesAboundEncounter; + + expect(BerriesAboundEncounter.onInit).toBeDefined(); + + BerriesAboundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = BerriesAboundEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.levelAdditiveMultiplier).toBe(1); + expect(config.pokemonConfigs?.[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = BerriesAboundEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + }); + + // 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 () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; + + // Clear out any pesky mods that slipped through test spin-up + scene.modifiers.forEach(mod => { + scene.removeModifier(mod); + }); + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + const berriesAfterCount = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + + expect(numBerries).toBe(berriesAfterCount); + }); + + it("should spawn a shop with 5 berries", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + }); + }); + + describe("Option 2 - Race to the Bush", () => { + it("should have the correct properties", () => { + const option = BerriesAboundEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should start battle if fastest pokemon is slower than boss", async () => { + const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + // Setting enemy's level arbitrarily high to outspeed + config.pokemonConfigs![0].dataSource!.level = 1000; + + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + + // Should be enraged + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected_bad`); + }); + + it("Should skip battle when fastest pokemon is faster than boss", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + // Setting party pokemon's level arbitrarily high to outspeed + const fastestPokemon = scene.getParty()[0]; + fastestPokemon.level = 1000; + fastestPokemon.calculateStats(); + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected`); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts new file mode 100644 index 000000000000..70adf93d502f --- /dev/null +++ b/src/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -0,0 +1,585 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { TrainerType } from "#enums/trainer-type"; +import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; +import { CommandPhase } from "#app/phases/command-phase"; +import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; +import * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:bugTypeSuperfan"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.WEEDLE]; +const defaultBiome = Biome.CAVE; +const defaultWave = 24; + +const POOL_1_POKEMON = [ + Species.PARASECT, + Species.VENOMOTH, + Species.LEDIAN, + Species.ARIADOS, + Species.YANMA, + Species.BEAUTIFLY, + Species.DUSTOX, + Species.MASQUERAIN, + Species.NINJASK, + Species.VOLBEAT, + Species.ILLUMISE, + Species.ANORITH, + Species.KRICKETUNE, + Species.WORMADAM, + Species.MOTHIM, + Species.SKORUPI, + Species.JOLTIK, + Species.LARVESTA, + Species.VIVILLON, + Species.CHARJABUG, + Species.RIBOMBEE, + Species.SPIDOPS, + Species.LOKIX +]; + +const POOL_2_POKEMON = [ + Species.SCYTHER, + Species.PINSIR, + Species.HERACROSS, + Species.FORRETRESS, + Species.SCIZOR, + Species.SHUCKLE, + Species.SHEDINJA, + Species.ARMALDO, + Species.VESPIQUEN, + Species.DRAPION, + Species.YANMEGA, + Species.LEAVANNY, + Species.SCOLIPEDE, + Species.CRUSTLE, + Species.ESCAVALIER, + Species.ACCELGOR, + Species.GALVANTULA, + Species.VIKAVOLT, + Species.ARAQUANID, + Species.ORBEETLE, + Species.CENTISKORCH, + Species.FROSMOTH, + Species.KLEAVOR, +]; + +const POOL_3_POKEMON: { species: Species, formIndex?: number }[] = [ + { + species: Species.PINSIR, + formIndex: 1 + }, + { + species: Species.SCIZOR, + formIndex: 1 + }, + { + species: Species.HERACROSS, + formIndex: 1 + }, + { + species: Species.ORBEETLE, + formIndex: 1 + }, + { + species: Species.CENTISKORCH, + formIndex: 1 + }, + { + species: Species.DURANT, + }, + { + species: Species.VOLCARONA, + }, + { + species: Species.GOLISOPOD, + }, +]; + +const POOL_4_POKEMON = [ + Species.GENESECT, + Species.SLITHER_WING, + Species.BUZZWOLE, + Species.PHEROMOSA +]; + +const PHYSICAL_TUTOR_MOVES = [ + Moves.MEGAHORN, + Moves.X_SCISSOR, + Moves.ATTACK_ORDER, + Moves.PIN_MISSILE, + Moves.FIRST_IMPRESSION +]; + +const SPECIAL_TUTOR_MOVES = [ + Moves.SILVER_WIND, + Moves.BUG_BUZZ, + Moves.SIGNAL_BEAM, + Moves.POLLEN_PUFF +]; + +const STATUS_TUTOR_MOVES = [ + Moves.STRING_SHOT, + Moves.STICKY_WEB, + Moves.SILK_TRAP, + Moves.RAGE_POWDER, + Moves.HEAL_ORDER +]; + +const MISC_TUTOR_MOVES = [ + Moves.BUG_BITE, + Moves.LEECH_LIFE, + Moves.DEFEND_ORDER, + Moves.QUIVER_DANCE, + Moves.TAIL_GLOW, + Moves.INFESTATION, + Moves.U_TURN +]; + +describe("Bug-Type Superfan - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.BUG_TYPE_SUPERFAN]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + + expect(BugTypeSuperfanEncounter.encounterType).toBe(MysteryEncounterType.BUG_TYPE_SUPERFAN); + expect(BugTypeSuperfanEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(BugTypeSuperfanEncounter.dialogue).toBeDefined(); + expect(BugTypeSuperfanEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(BugTypeSuperfanEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(BugTypeSuperfanEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.BUG_TYPE_SUPERFAN); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = BugTypeSuperfanEncounter; + + const { onInit } = BugTypeSuperfanEncounter; + + expect(BugTypeSuperfanEncounter.onInit).toBeDefined(); + + BugTypeSuperfanEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + const config = BugTypeSuperfanEncounter.enemyPartyConfigs[0]; + + expect(config).toBeDefined(); + expect(config.trainerConfig?.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(config.trainerConfig?.partyTemplates).toBeDefined(); + expect(config.female).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle the Bug-Type Superfan", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against the Bug-Type Superfan with wave 30 party template", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(2); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + }); + + it("should start battle against the Bug-Type Superfan with wave 50 party template", async () => { + game.override.startingWave(43); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(3); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 70 party template", async () => { + game.override.startingWave(61); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(4); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 100 party template", async () => { + game.override.startingWave(81); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(POOL_1_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 120 party template", async () => { + game.override.startingWave(111); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_2_POKEMON.includes(enemyParty[3].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[4].species.speciesId === config.species)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 140 party template", async () => { + game.override.startingWave(131); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[4].species.speciesId === config.species)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 160 party template", async () => { + game.override.startingWave(151); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(POOL_2_POKEMON.includes(enemyParty[2].species.speciesId)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_4_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should start battle against the Bug-Type Superfan with wave 180 party template", async () => { + game.override.startingWave(171); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyParty = scene.getEnemyParty(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyParty.length).toBe(5); + expect(scene.currentBattle.trainer?.config.trainerType).toBe(TrainerType.BUG_TYPE_SUPERFAN); + expect(enemyParty[0].species.speciesId).toBe(Species.BEEDRILL); + expect(enemyParty[0].formIndex).toBe(1); + expect(enemyParty[0].isBoss()).toBe(true); + expect(enemyParty[0].bossSegments).toBe(2); + expect(enemyParty[1].species.speciesId).toBe(Species.BUTTERFREE); + expect(enemyParty[1].formIndex).toBe(1); + expect(enemyParty[1].isBoss()).toBe(true); + expect(enemyParty[1].bossSegments).toBe(2); + expect(POOL_3_POKEMON.some(config => enemyParty[2].species.speciesId === config.species)).toBe(true); + expect(POOL_3_POKEMON.some(config => enemyParty[3].species.speciesId === config.species)).toBe(true); + expect(POOL_4_POKEMON.includes(enemyParty[4].species.speciesId)).toBe(true); + }); + + it("should let the player learn a Bug move after battle ends", async () => { + const selectOptionSpy = vi.spyOn(encounterPhaseUtils, "selectOptionThenPokemon"); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game, false); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterRewardsPhase.name); + game.phaseInterceptor["prompts"] = []; // Clear out prompt handlers + game.onNextPrompt("MysteryEncounterRewardsPhase", Mode.OPTION_SELECT, () => { + game.phaseInterceptor.superEndPhase(); + }); + await game.phaseInterceptor.run(MysteryEncounterRewardsPhase); + + expect(selectOptionSpy).toHaveBeenCalledTimes(1); + const optionData = selectOptionSpy.mock.calls[0][1]; + expect(PHYSICAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[0].label)).toBe(true); + expect(SPECIAL_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[1].label)).toBe(true); + expect(STATUS_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[2].label)).toBe(true); + expect(MISC_TUTOR_MOVES.some(move => new PokemonMove(move).getName() === optionData[3].label)).toBe(true); + }); + }); + + describe("Option 2 - Show off Bug Types", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip` + }); + }); + + it("should NOT be selectable if the player doesn't have any Bug types", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.ABRA]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should proceed to rewards screen with 0-1 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(2); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("SUPER_LURE"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("GREAT_BALL"); + }); + + it("should proceed to rewards screen with 2-3 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("QUICK_CLAW"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ULTRA_BALL"); + }); + + it("should proceed to rewards screen with 4-5 Bug Types reward options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL, Species.GALVANTULA, Species.VOLCARONA]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("GRIP_CLAW"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("ROGUE_BALL"); + }); + + it("should proceed to rewards screen with 6 Bug Types reward options (including form change item)", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE, Species.BEEDRILL, Species.GALVANTULA, Species.VOLCARONA, Species.ANORITH, Species.GENESECT]); + await runMysteryEncounterToEnd(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MASTER_BALL"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("MAX_LURE"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toBe("FORM_CHANGE_ITEM"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give a Bug Item", () => { + it("should have the correct properties", () => { + const option = BugTypeSuperfanEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_dialogue`, + }, + ], + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + }); + }); + + it("should NOT be selectable if the player doesn't have any Bug items", async () => { + game.scene.modifiers = []; + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + game.scene.modifiers = []; + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should remove the gifted item and proceed to rewards screen", async () => { + game.override.startingHeldItems([{name: "GRIP_CLAW", count: 1}]); + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE]); + + const gripClawCountBefore = scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(2); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED"); + + const gripClawCountAfter = scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + expect(gripClawCountBefore - 1).toBe(gripClawCountAfter); + }); + + it("should leave encounter without battle", async () => { + game.override.startingHeldItems([{name: "GRIP_CLAW", count: 1}]); + const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [Species.BUTTERFREE]); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts new file mode 100644 index 000000000000..383e3bd3564e --- /dev/null +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -0,0 +1,383 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { Abilities } from "#enums/abilities"; +import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { Button } from "#enums/buttons"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { Type } from "#app/data/type"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { NewBattlePhase } from "#app/phases/new-battle-phase"; + +const namespace = "mysteryEncounter:clowningAround"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Clowning Around - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.CLOWNING_AROUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + expect(ClowningAroundEncounter.encounterType).toBe(MysteryEncounterType.CLOWNING_AROUND); + expect(ClowningAroundEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(ClowningAroundEncounter.dialogue).toBeDefined(); + expect(ClowningAroundEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(ClowningAroundEncounter.options.length).toBe(3); + }); + + it("should not run below wave 80", async () => { + game.override.startingWave(79); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = ClowningAroundEncounter; + + expect(ClowningAroundEncounter.onInit).toBeDefined(); + + ClowningAroundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + const config = ClowningAroundEncounter.enemyPartyConfigs[0]; + + expect(config.doubleBattle).toBe(true); + expect(config.trainerConfig?.trainerType).toBe(TrainerType.HARLEQUIN); + expect(config.pokemonConfigs?.[0]).toEqual({ + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }); + expect(config.pokemonConfigs?.[1]).toEqual({ + species: getPokemonSpecies(Species.BLACEPHALON), + mysteryEncounterPokemonData: expect.anything(), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }); + expect(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.types.length).toBe(2); + expect([ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER + ]).toContain(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); + expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].mysteryEncounterPokemonData?.ability); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle the Clown", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start double battle against the clown", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(2); + expect(enemyField[0].species.speciesId).toBe(Species.MR_MIME); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.TEETER_DANCE), new PokemonMove(Moves.ALLY_SWITCH), new PokemonMove(Moves.DAZZLING_GLEAM), new PokemonMove(Moves.PSYCHIC)]); + expect(enemyField[1].species.speciesId).toBe(Species.BLACEPHALON); + expect(enemyField[1].moveset).toEqual([new PokemonMove(Moves.TRICK), new PokemonMove(Moves.HYPNOSIS), new PokemonMove(Moves.SHADOW_BALL), new PokemonMove(Moves.MIND_BLOWN)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(3); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.ROLE_PLAY).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TAUNT).length).toBe(2); + }); + + it("should let the player gain the ability after battle completion", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + const abilityToTrain = scene.currentBattle.mysteryEncounter?.misc.ability; + + game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + }); + + // Run to ability train option selection + const optionSelectUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(optionSelectUiHandler, "show"); + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + game.endPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + expect(scene.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); + + // Wait for Yes/No confirmation to appear + await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled()); + // Select "Yes" on train ability + optionSelectUiHandler.processInput(Button.ACTION); + // Select first pokemon in party to train + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Stop next battle before it runs + await game.phaseInterceptor.to(NewBattlePhase, false); + + const leadPokemon = scene.getParty()[0]; + expect(leadPokemon.mysteryEncounterPokemonData?.ability).toBe(abilityToTrain); + }); + }); + + describe("Option 2 - Remain Unprovoked", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_3`, + }, + ], + }); + }); + + it("should randomize held items of the Pokemon with the most items, and not the held items of other pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // Set some moves on party for attack type booster generation + scene.getParty()[0].moveset = [new PokemonMove(Moves.TACKLE), new PokemonMove(Moves.THIEF)]; + + // 2 Sitrus Berries on lead + scene.modifiers = []; + let itemType = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 2 Ganlon Berries on lead + itemType = generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 5 Golden Punch on lead (ultra) + itemType = generateModifierType(scene, modifierTypes.GOLDEN_PUNCH) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Lucky Egg on lead (ultra) + itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Soul Dew on lead (rogue) + itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 2 Golden Egg on lead (rogue) + itemType = generateModifierType(scene, modifierTypes.GOLDEN_EGG) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + + // 5 Soul Dew on second party pokemon (these should not change) + itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[1], 5, itemType); + + await runMysteryEncounterToEnd(game, 2); + + const leadItemsAfter = scene.getParty()[0].getHeldItems(); + const ultraCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ULTRA) + .reduce((a, b) => a + b.stackCount, 0); + const rogueCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ROGUE) + .reduce((a, b) => a + b.stackCount, 0); + expect(ultraCountAfter).toBe(10); + expect(rogueCountAfter).toBe(7); + + const secondItemsAfter = scene.getParty()[1].getHeldItems(); + expect(secondItemsAfter.length).toBe(1); + expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); + expect(secondItemsAfter[0]?.stackCount).toBe(5); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Return the Insults", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected`, + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_3`, + }, + ], + }); + }); + + it("should randomize the pokemon types of the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // Same type moves on lead + scene.getParty()[0].moveset = [new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF)]; + // Different type moves on second + scene.getParty()[1].moveset = [new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL)]; + // No moves on third + scene.getParty()[2].moveset = []; + await runMysteryEncounterToEnd(game, 3); + + const leadTypesAfter = scene.getParty()[0].mysteryEncounterPokemonData?.types; + const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterPokemonData?.types; + const thirdTypesAfter = scene.getParty()[2].mysteryEncounterPokemonData?.types; + + expect(leadTypesAfter.length).toBe(2); + expect(leadTypesAfter[0]).toBe(Type.WATER); + expect([Type.WATER, Type.ICE].includes(leadTypesAfter[1])).toBeFalsy(); + expect(secondaryTypesAfter.length).toBe(2); + expect(secondaryTypesAfter[0]).toBe(Type.GHOST); + expect([Type.GHOST, Type.POISON].includes(secondaryTypesAfter[1])).toBeFalsy(); + expect([Type.GRASS, Type.ELECTRIC].includes(secondaryTypesAfter[1])).toBeTruthy(); + expect(thirdTypesAfter.length).toBe(2); + expect(thirdTypesAfter[0]).toBe(Type.PSYCHIC); + expect(secondaryTypesAfter[1]).not.toBe(Type.PSYCHIC); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); + +async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: integer, itemType: PokemonHeldItemModifierType) { + const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; + itemMod.stackCount = stackCount; + await scene.addModifier(itemMod, true, false, false, true); + await scene.updateModifiers(true); +} diff --git a/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts new file mode 100644 index 000000000000..5a2512ddaf60 --- /dev/null +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -0,0 +1,261 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Moves } from "#enums/moves"; +import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { PokemonMove } from "#app/field/pokemon"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; + +const namespace = "mysteryEncounter:dancingLessons"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 45; + +describe("Dancing Lessons - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.PLAINS, [MysteryEncounterType.DANCING_LESSONS]], + [Biome.SPACE, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + + expect(DancingLessonsEncounter.encounterType).toBe(MysteryEncounterType.DANCING_LESSONS); + expect(DancingLessonsEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(DancingLessonsEncounter.dialogue).toBeDefined(); + expect(DancingLessonsEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DancingLessonsEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DancingLessonsEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.SPACE); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DANCING_LESSONS); + }); + + describe("Option 1 - Fight the Oricorio", () => { + it("should have the correct properties", () => { + const option1 = DancingLessonsEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against Oricorio", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.ORICORIO); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 0, 0, 0]); + const moveset = enemyField[0].moveset.map(m => m?.moveId); + expect(moveset.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance used before battle + }); + + it("should have a Baton in the rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + // Make party lead's level arbitrarily high to not get KOed by move + const partyLead = scene.getParty()[0]; + partyLead.level = 1000; + partyLead.calculateStats(); + await runMysteryEncounterToEnd(game, 1, undefined, true); + // For some reason updateModifiers breaks in this test and does not resolve promise + vi.spyOn(game.scene, "updateModifiers").mockImplementation(() => new Promise(resolve => resolve())); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); // Should fill remaining + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("BATON"); + }); + }); + + describe("Option 2 - Learn its Dance", () => { + it("should have the correct properties", () => { + const option = DancingLessonsEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should select a pokemon to learn Revelation Dance", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = []; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof LearnMovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as LearnMovePhase)["moveId"] === Moves.REVELATION_DANCE).length).toBe(1); // Revelation Dance taught to pokemon + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = []; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Teach it a Dance", () => { + it("should have the correct properties", () => { + const option = DancingLessonsEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should add Oricorio to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + const partyCountBefore = scene.getParty().length; + scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)]; + await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1}); + const partyCountAfter = scene.getParty().length; + + expect(partyCountBefore + 1).toBe(partyCountAfter); + const oricorio = scene.getParty()[scene.getParty().length - 1]; + expect(oricorio.species.speciesId).toBe(Species.ORICORIO); + const moveset = oricorio.moveset.map(m => m?.moveId); + expect(moveset?.some(m => m === Moves.REVELATION_DANCE)).toBeTruthy(); + expect(moveset?.some(m => m === Moves.DRAGON_DANCE)).toBeTruthy(); + }); + + it("should NOT be selectable if the player doesn't have a Dance type move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + const partyCountBefore = scene.getParty().length; + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + const partyCountAfter = scene.getParty().length; + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + expect(partyCountBefore).toBe(partyCountAfter); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DANCING_LESSONS, defaultParty); + scene.getParty()[0].moveset = [new PokemonMove(Moves.DRAGON_DANCE)]; + await runMysteryEncounterToEnd(game, 3, {pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts new file mode 100644 index 000000000000..969188dca060 --- /dev/null +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -0,0 +1,482 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; + +const namespace = "mysteryEncounter:delibirdy"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Delibird-y - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.DELIBIRDY]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + expect(DelibirdyEncounter.encounterType).toBe(MysteryEncounterType.DELIBIRDY); + expect(DelibirdyEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(DelibirdyEncounter.dialogue).toBeDefined(); + expect(DelibirdyEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(DelibirdyEncounter.dialogue.outro).toStrictEqual([{ text: `${namespace}.outro` }]); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DelibirdyEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY); + }); + + describe("Option 1 - Give them money", () => { + it("should have the correct properties", () => { + const option1 = DelibirdyEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = (scene.currentBattle.mysteryEncounter?.options[0].requirements[0] as MoneyRequirement).requiredMoney; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should give the player a Hidden Ability Charm", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const abilityCharm = generateModifierType(scene, modifierTypes.ABILITY_CHARM)!.newModifier() as HiddenAbilityRateBoosterModifier; + abilityCharm.stackCount = 4; + await scene.addModifier(abilityCharm, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 1); + + const abilityCharmAfter = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(abilityCharmAfter).toBeDefined(); + expect(abilityCharmAfter?.stackCount).toBe(4); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + scene.money = 200000; + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Give Food", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.option.2.select_prompt`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should decrease Berry stacks and give the player a Candy Jar", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Sitrus berries on party lead + scene.modifiers = []; + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + + expect(sitrusAfter?.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter?.stackCount).toBe(1); + }); + + it("Should remove Reviver Seed and give the player a Healing Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 99 Candy Jars + scene.modifiers = []; + const candyJar = generateModifierType(scene, modifierTypes.CANDY_JAR)!.newModifier() as LevelIncrementBoosterModifier; + candyJar.stackCount = 99; + await scene.addModifier(candyJar, true, false, false, true); + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + + // Sitrus berries on party + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(sitrusAfter?.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter?.stackCount).toBe(99); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter?.stackCount).toBe(5); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give Item", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.select_prompt`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("Should decrease held item stacks and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 2; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter?.stackCount).toBe(1); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); + }); + + it("Should remove held item and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Soul Dew on party lead + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter?.stackCount).toBe(3); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter?.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED)!; + const modifier = revSeed.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts new file mode 100644 index 000000000000..f22bd8329649 --- /dev/null +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -0,0 +1,239 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale-encounter"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:departmentStoreSale"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 37; + +describe("Department Store Sale - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.DEPARTMENT_STORE_SALE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + + expect(DepartmentStoreSaleEncounter.encounterType).toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + expect(DepartmentStoreSaleEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(DepartmentStoreSaleEncounter.dialogue).toBeDefined(); + expect(DepartmentStoreSaleEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(DepartmentStoreSaleEncounter.options.length).toBe(4); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + describe("Option 1 - TM Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + }); + }); + + it("should have shop with only TMs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("TM_"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Vitamin Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should have shop with only Vitamins", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("PP_UP") || + option.modifierTypeOption.type.id.includes("BASE_STAT_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - X Item Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + }); + }); + + it("should have shop with only X Items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id.includes("DIRE_HIT") || + option.modifierTypeOption.type.id.includes("TEMP_STAT_STAGE_BOOSTER")).toBeTruthy(); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 4 - Pokeball Shop", () => { + it("should have the correct properties", () => { + const option = DepartmentStoreSaleEncounter.options[3]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + }); + }); + + it("should have shop with only Pokeballs", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 4); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BALL"); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); + await runMysteryEncounterToEnd(game, 4); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts new file mode 100644 index 000000000000..7a8d951c5da5 --- /dev/null +++ b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -0,0 +1,231 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { FieldTripEncounter } from "#app/data/mystery-encounters/encounters/field-trip-encounter"; +import { Moves } from "#enums/moves"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:fieldTrip"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Field Trip - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + game.override.moveset([Moves.TACKLE, Moves.UPROAR, Moves.SWORDS_DANCE]); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.FIELD_TRIP]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + + expect(FieldTripEncounter.encounterType).toBe(MysteryEncounterType.FIELD_TRIP); + expect(FieldTripEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FieldTripEncounter.dialogue).toBeDefined(); + expect(FieldTripEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro` + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue` + } + ]); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FieldTripEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIELD_TRIP); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + describe("Option 1 - Show off a physical move", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 2 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Physical move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Attack"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Defense"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Give Food", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Special move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Sp. Atk"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Sp. Def"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give Item", () => { + it("should have the correct properties", () => { + const option = FieldTripEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.second_option_prompt`, + }); + }); + + it("Should give no reward on incorrect option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("Should give proper rewards on correct Special move option", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 3 }); + await game.phaseInterceptor.to(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.name).toBe("X Accuracy"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Speed"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("5x Great Ball"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("IV Scanner"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.name).toBe("Rarer Candy"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 3 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts new file mode 100644 index 000000000000..445ab4491a4b --- /dev/null +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -0,0 +1,287 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; +import { Gender } from "#app/data/gender"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { Type } from "#app/data/type"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:fieryFallout"; +/** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ +const defaultParty = [Species.ARCANINE, Species.NINETALES, Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.VOLCANO; +const defaultWave = 56; + +describe("Fiery Fallout - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIERY_FALLOUT]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + + expect(FieryFalloutEncounter.encounterType).toBe(MysteryEncounterType.FIERY_FALLOUT); + expect(FieryFalloutEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FieryFalloutEncounter.dialogue).toBeDefined(); + expect(FieryFalloutEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FieryFalloutEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FieryFalloutEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of volcano biome", async () => { + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should not run below wave 41", async () => { + game.override.startingWave(38); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; + const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true); + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = FieryFalloutEncounter; + + expect(FieryFalloutEncounter.onInit).toBeDefined(); + + FieryFalloutEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(FieryFalloutEncounter.enemyPartyConfigs).toEqual([ + { + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.VOLCARONA), + isBoss: false, + gender: Gender.MALE + }, + { + species: getPokemonSpecies(Species.VOLCARONA), + isBoss: false, + gender: Gender.FEMALE + } + ], + doubleBattle: true, + disableSwitch: true + } + ]); + expect(weatherSpy).toHaveBeenCalledTimes(1); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight 2 Volcarona", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start battle against 2 Volcarona", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(2); + expect(enemyField[0].species.speciesId).toBe(Species.VOLCARONA); + expect(enemyField[1].species.speciesId).toBe(Species.VOLCARONA); + expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(4); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle + }); + + it("should give charcoal to lead pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; + const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); + expect(charcoal).toBeDefined; + }); + }); + + describe("Option 2 - Suffer the weather", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + + const party = scene.getParty(); + const lapras = party.find((pkm) => pkm.species.speciesId === Species.LAPRAS)!; + lapras.status = new Status(StatusEffect.POISON); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); + + await runMysteryEncounterToEnd(game, 2); + + const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE)); + const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE)); + expect(scene.currentBattle.mysteryEncounter?.dialogueTokens["burnedPokemon"]).toBe("Gengar"); + burnablePokemon.forEach((pkm) => { + expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); + }); + expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy(); + notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - use FIRE types", () => { + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[2]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should give charcoal to lead pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leadPokemonId = scene.getParty()?.[0].id; + const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[]; + const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal"); + expect(charcoal).toBeDefined; + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if not enough FIRE types are in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [Species.MAGIKARP, Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const continueEncounterSpy = vi.spyOn((encounterPhase as MysteryEncounterPhase), "continueEncounter"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(continueEncounterSpy).not.toHaveBeenCalled(); + }); + }); +}); 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 new file mode 100644 index 000000000000..735dcc709bf8 --- /dev/null +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -0,0 +1,222 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { FightOrFlightEncounter } from "#app/data/mystery-encounters/encounters/fight-or-flight-encounter"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:fightOrFlight"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fight or Flight - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + expect(FightOrFlightEncounter.encounterType).toBe(MysteryEncounterType.FIGHT_OR_FLIGHT); + expect(FightOrFlightEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FightOrFlightEncounter.dialogue).toBeDefined(); + expect(FightOrFlightEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FightOrFlightEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIGHT_OR_FLIGHT); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; + + const { onInit } = FightOrFlightEncounter; + + expect(FightOrFlightEncounter.onInit).toBeDefined(); + + FightOrFlightEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = FightOrFlightEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.levelAdditiveMultiplier).toBe(1); + expect(config.pokemonConfigs?.[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = FightOrFlightEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + }); + + it("should reward the player with the item based on wave", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const item = game.scene.currentBattle.mysteryEncounter?.misc; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + }); + }); + + describe("Option 2 - Attempt to Steal", () => { + it("should have the correct properties", () => { + const option = FightOrFlightEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't have a Stealing move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("Should skip fight when player meets requirements", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)]; + const item = game.scene.currentBattle.mysteryEncounter!.misc; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts new file mode 100644 index 000000000000..70250350af48 --- /dev/null +++ b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -0,0 +1,304 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { Nature } from "#enums/nature"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; +import { Moves } from "#enums/moves"; +import { Command } from "#app/ui/command-ui-handler"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; + +const namespace = "mysteryEncounter:funAndGames"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fun And Games! - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.FUN_AND_GAMES]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + + expect(FunAndGamesEncounter.encounterType).toBe(MysteryEncounterType.FUN_AND_GAMES); + expect(FunAndGamesEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(FunAndGamesEncounter.dialogue).toBeDefined(); + expect(FunAndGamesEncounter.dialogue.intro).toStrictEqual([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FunAndGamesEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CIVILIZATIONN biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + const onInitResult = onInit!(scene); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Play the Wobbuffet game", () => { + it("should have the correct properties", () => { + const option = FunAndGamesEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should NOT be selectable if the player doesn't have enough money", async () => { + game.scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should get 3 turns to attack the Wobbuffet for a reward", async () => { + scene.money = 20000; + game.override.moveset([Moves.TACKLE]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.getEnemyPokemon()?.species.speciesId).toBe(Species.WOBBUFFET); + expect(scene.getEnemyPokemon()?.ivs).toEqual([0, 0, 0, 0, 0, 0]); + expect(scene.getEnemyPokemon()?.nature).toBe(Nature.MILD); + + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Turn 1 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 2 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 3 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + }); + + it("should have no items in rewards if Wubboffet doesn't take enough damage", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("should have Wide Lens item in rewards if Wubboffet is at 15-33% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("WIDE_LENS"); + }); + + it("should have Scope Lens item in rewards if Wubboffet is at 3-15% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SCOPE_LENS"); + }); + + it("should have Multi Lens item in rewards if Wubboffet is at <3% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = 1; + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MULTI_LENS"); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts new file mode 100644 index 000000000000..e91b936cb9d8 --- /dev/null +++ b/src/test/mystery-encounter/encounters/global-trade-system-encounter.test.ts @@ -0,0 +1,270 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { PokemonNatureWeightModifier } from "#app/modifier/modifier"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { GlobalTradeSystemEncounter } from "#app/data/mystery-encounters/encounters/global-trade-system-encounter"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { ModifierTier } from "#app/modifier/modifier-tier"; + +const namespace = "mysteryEncounter:globalTradeSystem"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Global Trade System - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.GLOBAL_TRADE_SYSTEM]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + expect(GlobalTradeSystemEncounter.encounterType).toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + expect(GlobalTradeSystemEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(GlobalTradeSystemEncounter.dialogue).toBeDefined(); + expect(GlobalTradeSystemEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(GlobalTradeSystemEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(GlobalTradeSystemEncounter.options.length).toBe(4); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.GLOBAL_TRADE_SYSTEM); + }); + + describe("Option 1 - Check Trade Offers", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + secondOptionPrompt: `${namespace}.option.1.trade_options_prompt`, + }); + }); + + it("Should trade a Pokemon from the player's party for the first of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[0].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("Should trade a Pokemon from the player's party for the second of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[1].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2, optionNo: 2 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("Should trade a Pokemon from the player's party for the third of 3 Pokemon options", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[2].species.speciesId; + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 3, optionNo: 3 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Wonder Trade", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip` + }); + }); + + it("Should trade a Pokemon from the player's party for a random wonder trade Pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + const speciesBefore = scene.getParty()[2].species.speciesId; + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + const speciesAfter = scene.getParty().at(-1)?.species.speciesId; + + expect(speciesAfter).toBeDefined(); + expect(speciesBefore).not.toBe(speciesAfter); + expect(defaultParty.includes(speciesAfter!)).toBeFalsy(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 2 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Trade an Item", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + secondOptionPrompt: `${namespace}.option.3.trade_options_prompt`, + }); + }); + + it("should decrease item stacks of chosen item and have a tiered up item in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + // Set 2 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 2; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier).toBe(ModifierTier.MASTER); + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + expect(soulDewAfter?.stackCount).toBe(1); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierType(scene, modifierTypes.SOUL_DEW)!; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 4 - Leave", () => { + it("should have the correct properties", () => { + const option = GlobalTradeSystemEncounter.options[3]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.4.label`, + buttonTooltip: `${namespace}.option.4.tooltip`, + selected: [ + { + text: `${namespace}.option.4.selected`, + }, + ], + }); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.GLOBAL_TRADE_SYSTEM, defaultParty); + await runMysteryEncounterToEnd(game, 4); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts new file mode 100644 index 000000000000..82670e32daa0 --- /dev/null +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -0,0 +1,284 @@ +import { LostAtSeaEncounter } from "#app/data/mystery-encounters/encounters/lost-at-sea-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "../encounter-test-utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; + + +const namespace = "mysteryEncounter:lostAtSea"; +/** Blastoise for surf. Pidgeot for fly. Abra for none. */ +const defaultParty = [Species.BLASTOISE, Species.PIDGEOT, Species.ABRA]; +const defaultBiome = Biome.SEA; +const defaultWave = 33; + +describe("Lost at Sea - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.SEA, [MysteryEncounterType.LOST_AT_SEA]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + + expect(LostAtSeaEncounter.encounterType).toBe(MysteryEncounterType.LOST_AT_SEA); + expect(LostAtSeaEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(LostAtSeaEncounter.dialogue).toBeDefined(); + expect(LostAtSeaEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(LostAtSeaEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of sea biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); + }); + + it("should not run below wave 11", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; + + const { onInit } = LostAtSeaEncounter; + + expect(LostAtSeaEncounter.onInit).toBeDefined(); + + LostAtSeaEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(LostAtSeaEncounter.dialogueTokens?.damagePercentage).toBe("25"); + expect(LostAtSeaEncounter.dialogueTokens?.option1RequiredMove).toBe("Surf"); + expect(LostAtSeaEncounter.dialogueTokens?.option2RequiredMove).toBe("Fly"); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Surf", () => { + it("should have the correct properties", () => { + const option1 = LostAtSeaEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + disabledButtonLabel: `${namespace}.option.1.label_disabled`, + buttonTooltip: `${namespace}.option.1.tooltip`, + disabledButtonTooltip: `${namespace}.option.1.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should award exp to surfable PKM (Blastoise)", async () => { + const laprasSpecies = getPokemonSpecies(Species.LAPRAS); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + const party = game.scene.getParty(); + const blastoise = party.find((pkm) => pkm.species.speciesId === Species.BLASTOISE); + const expBefore = blastoise!.exp; + + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(PartyExpPhase); + + expect(blastoise?.exp).toBe(expBefore + Math.floor(laprasSpecies.baseExp * defaultWave / 5 + 1)); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if no surfable PKM is in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, [Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 2 - Fly", () => { + it("should have the correct properties", () => { + const option2 = LostAtSeaEncounter.options[1]; + + expect(option2.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option2.dialogue).toBeDefined(); + expect(option2.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + disabledButtonLabel: `${namespace}.option.2.label_disabled`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.tooltip_disabled`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should award exp to flyable PKM (Pidgeot)", async () => { + const laprasBaseExp = 187; + const wave = 33; + game.override.startingWave(wave); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + const party = game.scene.getParty(); + const pidgeot = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); + const expBefore = pidgeot!.exp; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(PartyExpPhase); + + expect(pidgeot!.exp).toBe(expBefore + Math.floor(laprasBaseExp * defaultWave / 5 + 1)); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should be disabled if no flyable PKM is in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, [Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + }); + + describe("Option 3 - Wander aimlessy", () => { + it("should have the correct properties", () => { + const option3 = LostAtSeaEncounter.options[2]; + + expect(option3.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option3.dialogue).toBeDefined(); + expect(option3.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should damage all (allowed in battle) party PKM by 25%", async () => { + game.override.startingWave(33); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + + const party = game.scene.getParty(); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA)!; + vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); + + await runMysteryEncounterToEnd(game, 3); + + const allowedPkm = party.filter((pkm) => pkm.isAllowedInBattle()); + const notAllowedPkm = party.filter((pkm) => !pkm.isAllowedInBattle()); + allowedPkm.forEach((pkm) => + expect(pkm.hp, `${pkm.name} should have receivd 25% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.25)) + ); + + notAllowedPkm.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp())); + }); + + it("should leave encounter without battle", async () => { + game.override.startingWave(33); + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts new file mode 100644 index 000000000000..de527538711b --- /dev/null +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -0,0 +1,270 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter"; +import { TrainerConfig, TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#app/data/trainer-config"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:mysteriousChallengers"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Mysterious Challengers - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + + expect(MysteriousChallengersEncounter.encounterType).toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(MysteriousChallengersEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(MysteriousChallengersEncounter.dialogue).toBeDefined(); + expect(MysteriousChallengersEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(MysteriousChallengersEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(3); + expect(encounter.enemyPartyConfigs).toEqual([ + { + trainerConfig: expect.any(TrainerConfig), + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveMultiplier: 1, + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveMultiplier: 1.5, + female: expect.any(Boolean), + } + ]); + expect(encounter.enemyPartyConfigs[1].trainerConfig?.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE, false, true) + )); + expect(encounter.enemyPartyConfigs[2].trainerConfig?.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE), + new TrainerPartyTemplate(3, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)) + ); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(3); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Normal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have normal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("TM_COMMON"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toContain("TM_GREAT"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toContain("MEMORY_MUSHROOM"); + }); + }); + + describe("Option 2 - Hard Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have hard trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); + + describe("Option 3 - Brutal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have brutal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts new file mode 100644 index 000000000000..f73c1f437d08 --- /dev/null +++ b/src/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -0,0 +1,295 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter"; +import { PokemonMove } from "#app/field/pokemon"; +import { Moves } from "#enums/moves"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; + +const namespace = "mysteryEncounter:partTimer"; +// Pyukumuku for lowest speed, Regieleki for highest speed, Feebas for lowest "bulk", Melmetal for highest "bulk" +const defaultParty = [Species.PYUKUMUKU, Species.REGIELEKI, Species.FEEBAS, Species.MELMETAL]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 37; + +describe("Part-Timer - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.PART_TIMER]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + + expect(PartTimerEncounter.encounterType).toBe(MysteryEncounterType.PART_TIMER); + expect(PartTimerEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(PartTimerEncounter.dialogue).toBeDefined(); + expect(PartTimerEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(PartTimerEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(PartTimerEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.PART_TIMER); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + describe("Option 1 - Make Deliveries", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected` + } + ] + }); + }); + + it("should give the player 1x money multiplier money with max slowest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[0].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should give the player 4x money multiplier money with max fastest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.ivs = [20, 20, 20, 20, 20, 20]; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 2 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[1].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Help in the Warehouse", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected` + } + ] + }); + }); + + it("should give the player 1x money multiplier money with least bulky Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(1), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[2].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should give the player 4x money multiplier money with bulkiest Pokemon", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Override party levels to 50 so stats can be fully reflective + scene.getParty().forEach(p => { + p.level = 50; + p.ivs = [20, 20, 20, 20, 20, 20]; + p.calculateStats(); + }); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 4 }); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(4), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[3].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Assist with Sales", () => { + it("should have the correct properties", () => { + const option = PartTimerEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected` + } + ] + }); + }); + + it("Should NOT be selectable when requirements are not met", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Mock movesets + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + expect(EncounterPhaseUtils.updatePlayerMoney).not.toHaveBeenCalled(); + }); + + it("should be selectable and give the player 2.5x money multiplier money with requirements met", async () => { + vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.ATTRACT)]; + await runMysteryEncounterToEnd(game, 3); + + expect(EncounterPhaseUtils.updatePlayerMoney).toHaveBeenCalledWith(scene, scene.getWaveMoneyAmount(2.5), true, false); + // Expect PP of mon's moves to have been reduced to 2 + const moves = scene.getParty()[0].moveset; + for (const move of moves) { + expect((move?.getMovePp() ?? 0) - (move?.ppUsed ?? 0)).toBe(2); + } + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.PART_TIMER, defaultParty); + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1 }); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts new file mode 100644 index 000000000000..13860e83baa3 --- /dev/null +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -0,0 +1,299 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; + +const namespace = "mysteryEncounter:teleportingHijinks"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Teleporting Hijinks - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + scene.money = 20000; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.TELEPORTING_HIJINKS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + + expect(TeleportingHijinksEncounter.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + expect(TeleportingHijinksEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(TeleportingHijinksEncounter.dialogue).toBeDefined(); + expect(TeleportingHijinksEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TeleportingHijinksEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TeleportingHijinksEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should run in waves that are X1", async () => { + game.override.startingWave(11); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should run in waves that are X2", async () => { + game.override.startingWave(32); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should run in waves that are X3", async () => { + game.override.startingWave(23); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should NOT run in waves that are not X1, X2, or X3", async () => { + game.override.startingWave(54); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).not.toBe(MysteryEncounterType.TELEPORTING_HIJINKS); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TeleportingHijinksEncounter; + + const { onInit } = TeleportingHijinksEncounter; + + expect(TeleportingHijinksEncounter.onInit).toBeDefined(); + + TeleportingHijinksEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TeleportingHijinksEncounter.misc.price).toBeDefined(); + expect(TeleportingHijinksEncounter.dialogueTokens.price).toBeDefined(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Pay Money", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should NOT be selectable if the player doesn't have enough money", async () => { + game.scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should be selectable if the player has enough money", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + }); + + it("should transport to a new area", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + + const previousBiome = scene.arena.biomeType; + + 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); + }); + + it("should start a battle against an enraged boss", { retry: 5 }, async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + }); + + describe("Option 2 - Use Electric/Steel Typing", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't the right type pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.BLASTOISE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should be selectable if the player has the right type pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.METAGROSS]); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + }); + + it("should transport to a new area", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]); + + const previousBiome = scene.arena.biomeType; + + 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); + }); + + it("should start a battle against an enraged boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]); + await runMysteryEncounterToEnd(game, 2, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + }); + + describe("Option 3 - Inspect the Machine", () => { + it("should have the correct properties", () => { + const option = TeleportingHijinksEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }); + }); + + it("should start a battle against a boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.statStages).toEqual([0, 0, 0, 0, 0, 0, 0]); + expect(enemyField[0].isBoss()).toBe(true); + }); + + it("should have Magnet and Metal Coat in rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 3, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "Metal Coat")).toBe(true); + expect(modifierSelectHandler.options.some(opt => opt.modifierTypeOption.type.name === "Magnet")).toBe(true); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts new file mode 100644 index 000000000000..c43577337dae --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -0,0 +1,208 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { ThePokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/the-pokemon-salesman-encounter"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; + +const namespace = "mysteryEncounter:pokemonSalesman"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Pokemon Salesman - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.THE_POKEMON_SALESMAN]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + + expect(ThePokemonSalesmanEncounter.encounterType).toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + expect(ThePokemonSalesmanEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(ThePokemonSalesmanEncounter.dialogue).toBeDefined(); + expect(ThePokemonSalesmanEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { speaker: `${namespace}.speaker`, text: `${namespace}.intro_dialogue` } + ]); + expect(ThePokemonSalesmanEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(ThePokemonSalesmanEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(ThePokemonSalesmanEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(ThePokemonSalesmanEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ThePokemonSalesmanEncounter; + + const { onInit } = ThePokemonSalesmanEncounter; + + expect(ThePokemonSalesmanEncounter.onInit).toBeDefined(); + + ThePokemonSalesmanEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(ThePokemonSalesmanEncounter.dialogueTokens?.purchasePokemon).toBeDefined(); + expect(ThePokemonSalesmanEncounter.dialogueTokens?.price).toBeDefined(); + expect(ThePokemonSalesmanEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(ThePokemonSalesmanEncounter.misc?.price?.toString()).toBe(ThePokemonSalesmanEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_POKEMON_SALESMAN); + }); + + describe("Option 1 - Purchase the pokemon", () => { + it("should have the correct properties", () => { + const option = ThePokemonSalesmanEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected_message`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = scene.currentBattle.mysteryEncounter!.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should add the Pokemon to the party", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; + + await runMysteryEncounterToEnd(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize + 1); + + const newlyPurchasedPokemon = scene.getParty().find(p => p.name === pokemonName); + expect(newlyPurchasedPokemon).toBeDefined(); + expect(newlyPurchasedPokemon!.moveset.length > 0).toBeTruthy(); + }); + + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + scene.money = 20000; + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); 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 new file mode 100644 index 000000000000..be35ec31784c --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -0,0 +1,240 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; +import { Nature } from "#app/data/nature"; +import { BerryType } from "#enums/berry-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { BerryModifier, PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:theStrongStuff"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Strong Stuff - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.THE_STRONG_STUFF]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + expect(TheStrongStuffEncounter.encounterType).toBe(MysteryEncounterType.THE_STRONG_STUFF); + expect(TheStrongStuffEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(TheStrongStuffEncounter.dialogue).toBeDefined(); + expect(TheStrongStuffEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TheStrongStuffEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CAVE biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingBiome(Biome.MOUNTAIN); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully ", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = TheStrongStuffEncounter; + + expect(TheStrongStuffEncounter.onInit).toBeDefined(); + + TheStrongStuffEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ + { + levelAdditiveMultiplier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SHUCKLE), + isBoss: true, + bossSegments: 5, + mysteryEncounterPokemonData: new MysteryEncounterPokemonData({ spriteScale: 1.25 }), + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierConfigs: expect.any(Array), + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: expect.any(Function) + } + ], + } + ]); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Power Swap BSTs", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should lower stats of 2 highest BST and raise stats for rest of party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + + const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); + await runMysteryEncounterToEnd(game, 1); + + const bstsAfter = scene.getParty().map(p => { + const baseStats = p.getSpeciesForm().baseStats.slice(0); + scene.applyModifiers(PokemonBaseStatTotalModifier, true, p, baseStats); + return baseStats.reduce((a, b) => a + b); + }); + + // HP stat changes are halved compared to other values + expect(bstsAfter[0]).toEqual(bstsPrior[0] - 15 * 5 - 8); + expect(bstsAfter[1]).toEqual(bstsPrior[1] - 15 * 5 - 8); + expect(bstsAfter[2]).toEqual(bstsPrior[2] + 10 * 5 + 5); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - battle the Shuckle", () => { + it("should have the correct properties", () => { + const option1 = TheStrongStuffEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should start battle against Shuckle", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE); + expect(enemyField[0].summonData.statStages).toEqual([0, 2, 0, 2, 0, 0, 0]); + const shuckleItems = enemyField[0].getHeldItems(); + expect(shuckleItems.length).toBe(5); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.ENIGMA)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.GANLON)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.APICOT)?.stackCount).toBe(1); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.LUM)?.stackCount).toBe(2); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.INFESTATION), new PokemonMove(Moves.SALT_CURE), new PokemonMove(Moves.GASTRO_ACID), new PokemonMove(Moves.HEAL_ORDER)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(2); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.GASTRO_ACID).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STEALTH_ROCK).length).toBe(1); + }); + + it("should have Soul Dew in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SOUL_DEW"); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts new file mode 100644 index 000000000000..0c6422250316 --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -0,0 +1,388 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { Nature } from "#enums/nature"; +import { Moves } from "#enums/moves"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { PartyHealPhase } from "#app/phases/party-heal-phase"; +import { VictoryPhase } from "#app/phases/victory-phase"; + +const namespace = "mysteryEncounter:theWinstrateChallenge"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("The Winstrate Challenge - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.THE_WINSTRATE_CHALLENGE]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + + expect(TheWinstrateChallengeEncounter.encounterType).toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); + expect(TheWinstrateChallengeEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(TheWinstrateChallengeEncounter.dialogue).toBeDefined(); + expect(TheWinstrateChallengeEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TheWinstrateChallengeEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TheWinstrateChallengeEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_WINSTRATE_CHALLENGE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(5); + expect(encounter.enemyPartyConfigs).toEqual([ + { + trainerType: TrainerType.VITO, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.HISUI_ELECTRODE), + isBoss: false, + abilityIndex: 0, // Soundproof + nature: Nature.MODEST, + moveSet: [Moves.THUNDERBOLT, Moves.GIGA_DRAIN, Moves.FOUL_PLAY, Moves.THUNDER_WAVE], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.SWALOT), + isBoss: false, + abilityIndex: 2, // Gluttony + nature: Nature.QUIET, + moveSet: [Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.ICE_BEAM, Moves.EARTHQUAKE], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.DODRIO), + isBoss: false, + abilityIndex: 2, // Tangled Feet + nature: Nature.JOLLY, + moveSet: [Moves.DRILL_PECK, Moves.QUICK_ATTACK, Moves.THRASH, Moves.KNOCK_OFF], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.ALAKAZAM), + isBoss: false, + formIndex: 1, + nature: Nature.BOLD, + moveSet: [Moves.PSYCHIC, Moves.SHADOW_BALL, Moves.FOCUS_BLAST, Moves.THUNDERBOLT], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.DARMANITAN), + isBoss: false, + abilityIndex: 0, // Sheer Force + nature: Nature.IMPISH, + moveSet: [Moves.EARTHQUAKE, Moves.U_TURN, Moves.FLARE_BLITZ, Moves.ROCK_SLIDE], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICKY, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.MEDICHAM), + isBoss: false, + formIndex: 1, + nature: Nature.IMPISH, + moveSet: [Moves.AXE_KICK, Moves.ICE_PUNCH, Moves.ZEN_HEADBUTT, Moves.BULLET_PUNCH], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VIVI, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SEAKING), + isBoss: false, + abilityIndex: 3, // Lightning Rod + nature: Nature.ADAMANT, + moveSet: [Moves.WATERFALL, Moves.MEGAHORN, Moves.KNOCK_OFF, Moves.REST], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.BRELOOM), + isBoss: false, + abilityIndex: 1, // Poison Heal + nature: Nature.JOLLY, + moveSet: [Moves.SPORE, Moves.SWORDS_DANCE, Moves.SEED_BOMB, Moves.DRAIN_PUNCH], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.CAMERUPT), + isBoss: false, + formIndex: 1, + nature: Nature.CALM, + moveSet: [Moves.EARTH_POWER, Moves.FIRE_BLAST, Moves.YAWN, Moves.PROTECT], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICTORIA, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.ROSERADE), + isBoss: false, + abilityIndex: 0, // Natural Cure + nature: Nature.CALM, + moveSet: [Moves.SYNTHESIS, Moves.SLUDGE_BOMB, Moves.GIGA_DRAIN, Moves.SLEEP_POWDER], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.GARDEVOIR), + isBoss: false, + formIndex: 1, + nature: Nature.TIMID, + moveSet: [Moves.PSYSHOCK, Moves.MOONBLAST, Moves.SHADOW_BALL, Moves.WILL_O_WISP], + modifierConfigs: expect.any(Array) + } + ] + }, + { + trainerType: TrainerType.VICTOR, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.SWELLOW), + isBoss: false, + abilityIndex: 0, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.BRAVE_BIRD, Moves.PROTECT, Moves.QUICK_ATTACK], + modifierConfigs: expect.any(Array) + }, + { + species: getPokemonSpecies(Species.OBSTAGOON), + isBoss: false, + abilityIndex: 1, // Guts + nature: Nature.ADAMANT, + moveSet: [Moves.FACADE, Moves.OBSTRUCT, Moves.NIGHT_SLASH, Moves.FIRE_PUNCH], + modifierConfigs: expect.any(Array) + } + ] + } + ]); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(5); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Normal Battle", () => { + it("should have the correct properties", () => { + const option = TheWinstrateChallengeEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should battle all 5 trainers for a Macho Brace reward", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTOR); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(4); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTORIA); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(3); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VIVI); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(2); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICKY); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(1); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + await skipBattleToNextBattle(game); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VITO); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(0); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + + // Should have Macho Brace in the rewards + await skipBattleToNextBattle(game, true); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_MACHO_BRACE"); + }, 15000); + }); + + describe("Option 2 - Refuse the Challenge", () => { + it("should have the correct properties", () => { + const option = TheWinstrateChallengeEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("Should fully heal the party", async () => { + const phaseSpy = vi.spyOn(scene, "unshiftPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const partyHealPhases = phaseSpy.mock.calls.filter(p => p[0] instanceof PartyHealPhase).map(p => p[0]); + expect(partyHealPhases.length).toBe(1); + }); + + it("should have a Rarer Candy in the rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.THE_WINSTRATE_CHALLENGE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("RARER_CANDY"); + }); + }); +}); + +/** + * For any {@linkcode MysteryEncounter} that has a battle, can call this to skip battle and proceed to MysteryEncounterRewardsPhase + * @param game + * @param isFinalBattle + */ +async function skipBattleToNextBattle(game: GameManager, isFinalBattle: boolean = false) { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + const commandUiHandler = game.scene.ui.handlers[Mode.COMMAND]; + commandUiHandler.clear(); + game.scene.getEnemyParty().forEach(p => { + p.hp = 0; + p.status = new Status(StatusEffect.FAINT); + game.scene.field.remove(p); + }); + game.phaseInterceptor["onHold"] = []; + game.scene.pushPhase(new VictoryPhase(game.scene, 0)); + game.phaseInterceptor.superEndPhase(); + if (isFinalBattle) { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } else { + await game.phaseInterceptor.to(CommandPhase); + } +} 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 new file mode 100644 index 000000000000..a4bfaea659a5 --- /dev/null +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -0,0 +1,220 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import * as BattleAnims from "#app/data/battle-anims"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { HitHealModifier, HealShopCostModifier, TurnHealModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { CommandPhase } from "#app/phases/command-phase"; +import { MovePhase } from "#app/phases/move-phase"; + +const namespace = "mysteryEncounter:trashToTreasure"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Trash to Treasure - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.TRASH_TO_TREASURE]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + + expect(TrashToTreasureEncounter.encounterType).toBe(MysteryEncounterType.TRASH_TO_TREASURE); + expect(TrashToTreasureEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(TrashToTreasureEncounter.dialogue).toBeDefined(); + expect(TrashToTreasureEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(TrashToTreasureEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(TrashToTreasureEncounter.options.length).toBe(2); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.TRASH_TO_TREASURE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TrashToTreasureEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = TrashToTreasureEncounter; + + expect(TrashToTreasureEncounter.onInit).toBeDefined(); + + TrashToTreasureEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([ + { + levelAdditiveMultiplier: 1, + disableSwitch: true, + pokemonConfigs: [ + { + species: getPokemonSpecies(Species.GARBODOR), + isBoss: true, + formIndex: 1, + bossSegmentModifier: 1, + moveSet: [Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH], + } + ], + } + ]); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Dig for Valuables", () => { + it("should have the correct properties", () => { + const option1 = TrashToTreasureEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should give 2 Leftovers, 2 Shell Bell, and Black Sludge", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const leftovers = scene.findModifier(m => m instanceof TurnHealModifier) as TurnHealModifier; + expect(leftovers).toBeDefined(); + expect(leftovers?.stackCount).toBe(2); + + const shellBell = scene.findModifier(m => m instanceof HitHealModifier) as HitHealModifier; + expect(shellBell).toBeDefined(); + expect(shellBell?.stackCount).toBe(2); + + const blackSludge = scene.findModifier(m => m instanceof HealShopCostModifier) as HealShopCostModifier; + expect(blackSludge).toBeDefined(); + expect(blackSludge?.stackCount).toBe(1); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Battle Garbodor", () => { + it("should have the correct properties", () => { + const option1 = TrashToTreasureEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should start battle against Garbodor", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.GARBODOR); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.PAYBACK), new PokemonMove(Moves.GUNK_SHOT), new PokemonMove(Moves.STOMPING_TANTRUM), new PokemonMove(Moves.DRAIN_PUNCH)]); + + // Should have used moves pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(2); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TOXIC).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.AMNESIA).length).toBe(1); + }); + + it("should have 2 Rogue, 1 Ultra, 1 Great in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TRASH_TO_TREASURE, defaultParty); + await runMysteryEncounterToEnd(game, 2, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts new file mode 100644 index 000000000000..f235609ee084 --- /dev/null +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -0,0 +1,265 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierType } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter"; +import { MovePhase } from "#app/phases/move-phase"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { BerryType } from "#enums/berry-type"; +import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; +import { Stat } from "#enums/stat"; +import { BerryModifier } from "#app/modifier/modifier"; +import { modifierTypes } from "#app/modifier/modifier-type"; + +const namespace = "mysteryEncounter:uncommonBreed"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Uncommon Breed - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.UNCOMMON_BREED]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + + expect(UncommonBreedEncounter.encounterType).toBe(MysteryEncounterType.UNCOMMON_BREED); + expect(UncommonBreedEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(UncommonBreedEncounter.dialogue).toBeDefined(); + expect(UncommonBreedEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(UncommonBreedEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.UNCOMMON_BREED); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = UncommonBreedEncounter; + + const { onInit } = UncommonBreedEncounter; + + expect(UncommonBreedEncounter.onInit).toBeDefined(); + + UncommonBreedEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + const config = UncommonBreedEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.pokemonConfigs?.[0].isBoss).toBe(false); + expect(onInitResult).toBe(true); + }); + + describe.skip("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should 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); + + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; + + await runMysteryEncounterToEnd(game, 1, undefined, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(speciesToSpawn); + + const statStagePhases = unshiftPhaseSpy.mock.calls.filter(p => p[0] instanceof StatStageChangePhase)[0][0] as any; + expect(statStagePhases.stats).toEqual([Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD]); + + // Should have used its egg move pre-battle + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + const eggMoves: Moves[] = speciesEggMoves[getPokemonSpecies(speciesToSpawn).getRootSpeciesId()]; + const usedMove = (movePhases[0] as MovePhase).move.moveId; + expect(eggMoves.includes(usedMove)).toBe(true); + }); + }); + + describe("Option 2 - Give it Food", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + } + ], + }); + }); + + // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue + it.skip("should NOT be selectable if the player doesn't have enough berries", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + // Clear out any pesky mods that slipped through test spin-up + scene.modifiers.forEach(mod => { + scene.removeModifier(mod); + }); + await scene.updateModifiers(true); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + // TODO: there is some severe test flakiness occurring for this file, needs to be looked at/addressed in separate issue + it.skip("Should skip fight when player meets requirements", { retry: 5 }, async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + + // Berries on party lead + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS])!; + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + const ganlon = generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON])!; + const ganlonMod = ganlon.newModifier(scene.getParty()[0]) as BerryModifier; + ganlonMod.stackCount = 3; + await scene.addModifier(ganlonMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Use an Attracting Move", () => { + it("should have the correct properties", () => { + const option = UncommonBreedEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + } + ], + }); + }); + + it("should NOT be selectable if the player doesn't have an Attracting move", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + scene.getParty().forEach(p => p.moveset = []); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("Should skip fight when player meets requirements", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty); + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.CHARM)]; + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts new file mode 100644 index 000000000000..ef014c6949b5 --- /dev/null +++ b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts @@ -0,0 +1,219 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter"; +import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +const namespace = "mysteryEncounter:weirdDream"; +const defaultParty = [Species.MAGBY, Species.HAUNTER, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Weird Dream - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + vi.spyOn(EncounterTransformationSequence, "doPokemonTransformationSequence").mockImplementation(() => new Promise(resolve => resolve())); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.WEIRD_DREAM]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + + expect(WeirdDreamEncounter.encounterType).toBe(MysteryEncounterType.WEIRD_DREAM); + expect(WeirdDreamEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE); + expect(WeirdDreamEncounter.dialogue).toBeDefined(); + expect(WeirdDreamEncounter.dialogue.intro).toStrictEqual([ + { + text: `${namespace}.intro` + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(WeirdDreamEncounter.options.length).toBe(2); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.WEIRD_DREAM); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = WeirdDreamEncounter; + const loadBgmSpy = vi.spyOn(scene, "loadBgm"); + + const { onInit } = WeirdDreamEncounter; + + expect(WeirdDreamEncounter.onInit).toBeDefined(); + + WeirdDreamEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit!(scene); + + expect(loadBgmSpy).toHaveBeenCalled(); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Accept Transformation", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + + const pokemonPrior = scene.getParty().map(pokemon => pokemon); + const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal()); + + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + + const pokemonAfter = scene.getParty(); + const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal()); + const bstDiff = bstsAfter.map((bst, index) => bst - bstsPrior[index]); + + for (let i = 0; i < pokemonAfter.length; i++) { + const newPokemon = pokemonAfter[i]; + expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId); + expect(newPokemon.mysteryEncounterPokemonData?.types.length).toBe(2); + } + + const plus90To110 = bstDiff.filter(bst => bst > 80); + const plus40To50 = bstDiff.filter(bst => bst < 80); + + expect(plus90To110.length).toBe(2); + expect(plus40To50.length).toBe(1); + }); + + it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 1); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT"); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Leave", () => { + it("should have the correct properties", () => { + const option = WeirdDreamEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }); + }); + + it("should reduce party levels by 20%", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + const levelsPrior = scene.getParty().map(p => p.level); + await runMysteryEncounterToEnd(game, 2); + + const levelsAfter = scene.getParty().map(p => p.level); + + for (let i = 0; i < levelsPrior.length; i++) { + expect(Math.max(Math.ceil(0.8 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); + expect(scene.getParty()[i].levelExp).toBe(0); + } + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts new file mode 100644 index 000000000000..61eb1eaffd10 --- /dev/null +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -0,0 +1,341 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import { StatusEffect } from "#app/data/status-effect"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { Type } from "#app/data/type"; +import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MessagePhase } from "#app/phases/message-phase"; + +describe("Mystery Encounter Utils", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + initSceneWithoutEncounterPhase(game.scene, [Species.ARCEUS, Species.MANAPHY]); + }); + + describe("getRandomPlayerPokemon", () => { + it("gets a random pokemon from player party", () => { + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets a fainted pokemon from player party if isAllowedInBattle is false", () => { + // Both pokemon fainted + scene.getParty().forEach(p => { + p.hp = 0; + p.trySetStatus(StatusEffect.FAINT); + p.updateInfo(); + }); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets an unfainted pokemon from player party if isAllowedInBattle is true", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("returns last unfainted pokemon if doNotReturnLastAbleMon is false", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true, false); + expect(result.species.speciesId).toBe(Species.MANAPHY); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true, false); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("never returns last unfainted pokemon if doNotReturnLastAbleMon is true", () => { + // Only faint 1st pokemon + const party = scene.getParty(); + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + + // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) + game.override.seed("random"); + + let result = getRandomPlayerPokemon(scene, true, true); + expect(result.species.speciesId).toBe(Species.ARCEUS); + + game.override.seed("random2"); + + result = getRandomPlayerPokemon(scene, true, true); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + }); + + describe("getHighestLevelPlayerPokemon", () => { + it("gets highest level pokemon", () => { + const party = scene.getParty(); + party[0].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("gets highest level pokemon at different index", () => { + const party = scene.getParty(); + party[1].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("breaks ties by getting returning lower index", () => { + const party = scene.getParty(); + party[0].level = 100; + party[1].level = 100; + + const result = getHighestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("returns highest level unfainted if unfainted is true", () => { + const party = scene.getParty(); + party[0].level = 100; + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + party[1].level = 10; + + const result = getHighestLevelPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + }); + + describe("getLowestLevelPokemon", () => { + it("gets lowest level pokemon", () => { + const party = scene.getParty(); + party[0].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + + it("gets lowest level pokemon at different index", () => { + const party = scene.getParty(); + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("breaks ties by getting returning lower index", () => { + const party = scene.getParty(); + party[0].level = 100; + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene); + expect(result.species.speciesId).toBe(Species.ARCEUS); + }); + + it("returns lowest level unfainted if unfainted is true", () => { + const party = scene.getParty(); + party[0].level = 10; + party[0].hp = 0; + party[0].trySetStatus(StatusEffect.FAINT); + party[0].updateInfo(); + party[1].level = 100; + + const result = getLowestLevelPlayerPokemon(scene, true); + expect(result.species.speciesId).toBe(Species.MANAPHY); + }); + }); + + describe("getRandomSpeciesByStarterTier", () => { + it("gets species for a starter tier", () => { + const result = getRandomSpeciesByStarterTier(5); + const pokeSpecies = getPokemonSpecies(result); + + expect(pokeSpecies.speciesId).toBe(result); + expect(speciesStarters[result]).toBe(5); + }); + + it("gets species for a starter tier range", () => { + const result = getRandomSpeciesByStarterTier([5, 8]); + const pokeSpecies = getPokemonSpecies(result); + + expect(pokeSpecies.speciesId).toBe(result); + expect(speciesStarters[result]).toBeGreaterThanOrEqual(5); + expect(speciesStarters[result]).toBeLessThanOrEqual(8); + }); + + it("excludes species from search", () => { + // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian + const result = getRandomSpeciesByStarterTier(9, [Species.KORAIDON, Species.MIRAIDON, Species.ARCEUS, Species.RAYQUAZA, Species.KYOGRE, Species.GROUDON]); + const pokeSpecies = getPokemonSpecies(result); + expect(pokeSpecies.speciesId).toBe(Species.ZACIAN); + }); + + it("gets species of specified types", () => { + // Only 9 tiers are: Koraidon, Miraidon, Arceus, Rayquaza, Kyogre, Groudon, Zacian + const result = getRandomSpeciesByStarterTier(9, undefined, [Type.GROUND]); + const pokeSpecies = getPokemonSpecies(result); + expect(pokeSpecies.speciesId).toBe(Species.GROUDON); + }); + }); + + describe("koPlayerPokemon", () => { + it("KOs a pokemon", () => { + const party = scene.getParty(); + const arceus = party[0]; + arceus.hp = 100; + expect(arceus.isAllowedInBattle()).toBe(true); + + koPlayerPokemon(scene, arceus); + expect(arceus.isAllowedInBattle()).toBe(false); + }); + }); + + describe("getTextWithEncounterDialogueTokens", () => { + it("injects dialogue tokens and color styling", () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}"); + }); + + it("can perform nested dialogue token injection", () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new"); + + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}"); + }); + }); + + describe("queueEncounterMessage", () => { + it("queues a message with encounter dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene, "queueMessage"); + const phaseSpy = vi.spyOn(game.scene, "unshiftPhase"); + + queueEncounterMessage(scene, "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, true); + expect(phaseSpy).toHaveBeenCalledWith(expect.any(MessagePhase)); + }); + }); + + describe("showEncounterText", () => { + it("showText with dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene.ui, "showText"); + + await showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0, true, null); + }); + }); + + describe("showEncounterDialogue", () => { + it("showText with dialogue tokens", async () => { + scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); + const spy = vi.spyOn(game.scene.ui, "showDialogue"); + + await showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0); + }); + }); + + describe("initBattleWithEnemyConfig", () => { + it("", () => { + + }); + }); + + describe("setCustomEncounterRewards", () => { + it("", () => { + + }); + }); + + describe("selectPokemonForOption", () => { + it("", () => { + + }); + }); + + describe("setEncounterExp", () => { + it("", () => { + + }); + }); + + describe("leaveEncounterWithoutBattle", () => { + it("", () => { + + }); + }); + + describe("handleMysteryEncounterVictory", () => { + it("", () => { + + }); + }); +}); + diff --git a/src/test/mystery-encounter/mystery-encounter.test.ts b/src/test/mystery-encounter/mystery-encounter.test.ts new file mode 100644 index 000000000000..d2a2e7f9d92f --- /dev/null +++ b/src/test/mystery-encounter/mystery-encounter.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeAll, beforeEach, expect, describe, it } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import { Species } from "#enums/species"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; + +describe("Mystery Encounters", () => { + 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.startingWave(11); + game.override.mysteryEncounterChance(100); + }); + + it("Spawns a mystery encounter", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); + }); + + it("", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + expect(game.scene.getCurrentPhase()!.constructor.name).toBe(MysteryEncounterPhase.name); + }); + + it("spawns mysterious challengers encounter", async () => { + }); + + it("spawns mysterious chest encounter", async () => { + }); + + it("spawns dark deal encounter", async () => { + }); + + it("spawns fight or flight encounter", async () => { + }); +}); + diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts new file mode 100644 index 000000000000..0a99cd00db30 --- /dev/null +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -0,0 +1,154 @@ +import {afterEach, beforeAll, beforeEach, expect, describe, it, vi } from "vitest"; +import GameManager from "#app/test/utils/gameManager"; +import Phaser from "phaser"; +import {Species} from "#enums/species"; +import { MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import {Mode} from "#app/ui/ui"; +import {Button} from "#enums/buttons"; +import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; +import {MysteryEncounterType} from "#enums/mystery-encounter-type"; +import MessageUiHandler from "#app/ui/message-ui-handler"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; + +describe("Mystery Encounter Phases", () => { + 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.startingWave(11); + game.override.mysteryEncounterChance(100); + // Seed guarantees wild encounter to be replaced by ME + game.override.seed("test"); + }); + + describe("MysteryEncounterPhase", () => { + it("Runs to MysteryEncounterPhase", async() => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + expect(game.scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + }); + + it("Runs MysteryEncounterPhase", async() => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + game.onNextPrompt("MysteryEncounterPhase", Mode.MYSTERY_ENCOUNTER, () => { + // End phase early for test + game.phaseInterceptor.superEndPhase(); + }); + await game.phaseInterceptor.run(MysteryEncounterPhase); + + expect(game.scene.mysteryEncounterSaveData.encounteredEvents.length).toBeGreaterThan(0); + expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].type).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(game.scene.mysteryEncounterSaveData.encounteredEvents[0].tier).toEqual(MysteryEncounterTier.GREAT); + expect(game.scene.ui.getMode()).toBe(Mode.MYSTERY_ENCOUNTER); + }); + + it("Selects an option for MysteryEncounterPhase", async() => { + const dialogueSpy = vi.spyOn(game.scene.ui, "showDialogue"); + const messageSpy = vi.spyOn(game.scene.ui, "showText"); + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); + + game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { + const handler = game.scene.ui.getHandler() as MessageUiHandler; + handler.processInput(Button.ACTION); + }); + + await game.phaseInterceptor.run(MysteryEncounterPhase); + + // Select option 1 for encounter + const handler = game.scene.ui.getHandler() as MysteryEncounterUiHandler; + handler.unblockInput(); + handler.processInput(Button.ACTION); + + // Waitfor required so that option select messages and preOptionPhase logic are handled + await vi.waitFor(() => expect(game.scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterOptionSelectedPhase.name)); + expect(game.scene.ui.getMode()).toBe(Mode.MESSAGE); + expect(dialogueSpy).toHaveBeenCalledTimes(1); + expect(messageSpy).toHaveBeenCalledTimes(2); + expect(dialogueSpy).toHaveBeenCalledWith("What's this?", "???", null, expect.any(Function)); + expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 750, true); + expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 300, true); + }); + }); + + describe("MysteryEncounterOptionSelectedPhase", () => { + it("runs phase", () => { + + }); + + it("handles onOptionSelect execution", () => { + + }); + + it("hides intro visuals", () => { + + }); + + it("does not hide intro visuals if option disabled", () => { + + }); + }); + + describe("MysteryEncounterBattlePhase", () => { + it("runs phase", () => { + + }); + + it("handles TRAINER_BATTLE variant", () => { + + }); + + it("handles BOSS_BATTLE variant", () => { + + }); + + it("handles WILD_BATTLE variant", () => { + + }); + + it("handles double battle", () => { + + }); + }); + + describe("MysteryEncounterRewardsPhase", () => { + it("runs phase", () => { + + }); + + it("handles doEncounterRewards", () => { + + }); + + it("handles heal phase if enabled", () => { + + }); + }); + + describe("PostMysteryEncounterPhase", () => { + it("runs phase", () => { + + }); + + it("handles onPostOptionSelect execution", () => { + + }); + + it("runs to next EncounterPhase", () => { + + }); + }); +}); + diff --git a/src/test/phases/select-modifier-phase.test.ts b/src/test/phases/select-modifier-phase.test.ts new file mode 100644 index 000000000000..d946c850ae38 --- /dev/null +++ b/src/test/phases/select-modifier-phase.test.ts @@ -0,0 +1,209 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { initSceneWithoutEncounterPhase } from "#app/test/utils/gameManagerUtils"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import * as Utils from "#app/utils"; +import { CustomModifierSettings, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; +import BattleScene from "#app/battle-scene"; +import { Species } from "#enums/species"; +import { Mode } from "#app/ui/ui"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; + +describe("SelectModifierPhase", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + scene = game.scene; + + initSceneWithoutEncounterPhase(scene, [Species.ABRA, Species.VOLCARONA]); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + + vi.clearAllMocks(); + }); + + it("should start a select modifier phase", async () => { + const selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + }); + + it("should generate random modifiers", async () => { + const selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + }); + + it("should modify reroll cost", async () => { + const options = [ + new ModifierTypeOption(modifierTypes.POTION(), 0, 100), + new ModifierTypeOption(modifierTypes.ETHER(), 0, 400), + new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000) + ]; + + const selectModifierPhase1 = new SelectModifierPhase(scene); + const selectModifierPhase2 = new SelectModifierPhase(scene, 0, undefined, { rerollMultiplier: 2 }); + + const cost1 = selectModifierPhase1.getRerollCost(options, false); + const cost2 = selectModifierPhase2.getRerollCost(options, false); + expect(cost2).toEqual(cost1 * 2); + }); + + it("should generate random modifiers from reroll", async () => { + let selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + + // Simulate selecting reroll + selectModifierPhase = new SelectModifierPhase(scene, 1, [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON]); + scene.unshiftPhase(selectModifierPhase); + scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase()); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + expect(modifierSelectHandler.options.length).toEqual(3); + }); + + it("should generate random modifiers of same tier for reroll with reroll lock", async () => { + // Just use fully random seed for this test + vi.spyOn(scene, "resetSeed").mockImplementation(() => { + scene.waveSeed = Utils.shiftCharCodes(scene.seed, 5); + Phaser.Math.RND.sow([scene.waveSeed]); + console.log("Wave Seed:", scene.waveSeed, 5); + scene.rngCounter = 0; + }); + + let selectModifierPhase = new SelectModifierPhase(scene); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + const firstRollTiers: ModifierTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier); + + // Simulate selecting reroll with lock + scene.lockModifierTiers = true; + scene.reroll = true; + selectModifierPhase = new SelectModifierPhase(scene, 1, firstRollTiers); + scene.unshiftPhase(selectModifierPhase); + scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase()); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + expect(modifierSelectHandler.options.length).toEqual(3); + // Reroll with lock can still upgrade + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[0]); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[1]); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[2]); + }); + + it("should generate custom modifiers", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_ULTRA, modifierTypes.LEFTOVERS, modifierTypes.AMULET_COIN, modifierTypes.GOLDEN_PUNCH] + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_ULTRA"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("LEFTOVERS"); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("AMULET_COIN"); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.id).toEqual("GOLDEN_PUNCH"); + }); + + it("should generate custom modifier tiers that can upgrade from luck", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.ULTRA, ModifierTier.ROGUE, ModifierTier.MASTER] + }; + const pokemon = new PlayerPokemon(scene, getPokemonSpecies(Species.BULBASAUR), 10, undefined, 0, undefined, true, 2, undefined, undefined, undefined); + + // Fill party with max shinies + while (scene.getParty().length > 0) { + scene.getParty().pop(); + } + scene.getParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon); + + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.COMMON); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.MASTER); + }); + + it("should generate custom modifiers and modifier tiers together", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_COMMON], + guaranteedModifierTiers: [ModifierTier.MASTER, ModifierTier.MASTER] + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_COMMON"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + }); + + it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => { + const customModifiers: CustomModifierSettings = { + guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM], + guaranteedModifierTiers: [ModifierTier.MASTER], + fillRemaining: true + }; + const selectModifierPhase = new SelectModifierPhase(scene, 0, undefined, customModifiers); + scene.pushPhase(selectModifierPhase); + await game.phaseInterceptor.run(SelectModifierPhase); + + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(3); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER); + }); +}); diff --git a/src/test/ui/battle_info.test.ts b/src/test/ui/battle_info.test.ts new file mode 100644 index 000000000000..4d511b75e6f2 --- /dev/null +++ b/src/test/ui/battle_info.test.ts @@ -0,0 +1,55 @@ +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; +import { Species } from "#app/enums/species"; +import { ExpPhase } from "#app/phases/exp-phase"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../data/exp", ({}) => { + return { + getLevelRelExp: vi.fn(() => 1), //consistent levelRelExp + }; +}); + +describe("UI - Battle Info", () => { + 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.GUILLOTINE, Moves.SPLASH]) + .battleType("single") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.CATERPIE); + }); + + it.each([ExpGainsSpeed.FAST, ExpGainsSpeed.FASTER, ExpGainsSpeed.SKIP])( + "should increase exp gains animation by 2^%i", + async (expGainsSpeed) => { + game.settings.expGainsSpeed(expGainsSpeed); + vi.spyOn(Math, "pow"); + + await game.classicMode.startBattle([Species.CHARIZARD]); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.phaseInterceptor.to(ExpPhase, true); + + expect(Math.pow).not.toHaveBeenCalledWith(2, expGainsSpeed); + } + ); +}); diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index 507161eb6d00..466bcbf80520 100644 --- a/src/test/utils/TextInterceptor.ts +++ b/src/test/utils/TextInterceptor.ts @@ -1,3 +1,6 @@ +/** + * Class will intercept any text or dialogue message calls and log them for test purposes + */ export default class TextInterceptor { private scene; public logs: string[] = []; diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cc364a74b831..452956ab4662 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -49,6 +49,12 @@ import { OverridesHelper } from "./helpers/overridesHelper"; import { SettingsHelper } from "./helpers/settingsHelper"; import { ReloadHelper } from "./helpers/reloadHelper"; import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { expect } from "vitest"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { isNullOrUndefined } from "#app/utils"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; /** * Class to manage the game state and transitions between phases. @@ -88,6 +94,9 @@ export default class GameManager { this.challengeMode = new ChallengeModeHelper(this); this.settings = new SettingsHelper(this); this.reload = new ReloadHelper(this); + + // Disables Mystery Encounters on all tests (can be overridden at test level) + this.override.mysteryEncounterChance(0); } /** @@ -140,7 +149,7 @@ export default class GameManager { this.scene.gameSpeed = 5; this.scene.moveAnimations = false; this.scene.showLevelUpStats = false; - this.scene.expGainsSpeed = 3; + this.scene.expGainsSpeed = ExpGainsSpeed.SKIP; this.scene.expParty = ExpNotification.SKIP; this.scene.hpBarSpeed = 3; this.scene.enableTutorials = false; @@ -178,6 +187,39 @@ export default class GameManager { console.log("===finished run to final boss encounter==="); } + /** + * Runs the game to a mystery encounter phase. + * @param encounterType if specified, will expect encounter to have been spawned + * @param species Optional array of species for party. + * @returns A promise that resolves when the EncounterPhase ends. + */ + async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) { + if (!isNullOrUndefined(encounterType)) { + this.override.disableTrainerWaves(); + this.override.mysteryEncounter(encounterType!); + } + + await this.runToTitle(); + + this.onNextPrompt("TitlePhase", Mode.TITLE, () => { + this.scene.gameMode = getGameMode(GameModes.CLASSIC); + const starters = generateStarter(this.scene, species); + const selectStarterPhase = new SelectStarterPhase(this.scene); + this.scene.pushPhase(new EncounterPhase(this.scene, false)); + selectStarterPhase.initBattle(starters); + }, () => this.isCurrentPhase(EncounterPhase)); + + this.onNextPrompt("EncounterPhase", Mode.MESSAGE, () => { + const handler = this.scene.ui.getHandler() as BattleMessageUiHandler; + handler.processInput(Button.ACTION); + }, () => this.isCurrentPhase(MysteryEncounterPhase), true); + + await this.phaseInterceptor.run(EncounterPhase); + if (!isNullOrUndefined(encounterType)) { + expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); + } + } + /** * @deprecated Use `game.classicMode.startBattle()` or `game.dailyMode.startBattle()` instead * diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 20a3fd179fdb..700d93082d8c 100644 --- a/src/test/utils/gameManagerUtils.ts +++ b/src/test/utils/gameManagerUtils.ts @@ -7,6 +7,7 @@ import { PlayerPokemon } from "#app/field/pokemon"; import { GameModes, getGameMode } from "#app/game-mode"; import { Starter } from "#app/ui/starter-select-ui-handler"; import { Species } from "#enums/species"; +import Battle, { BattleType } from "#app/battle"; /** Function to convert Blob to string */ export function blobToString(blob) { @@ -89,3 +90,23 @@ export function getMovePosition(scene: BattleScene, pokemonIndex: 0 | 1, move: M console.log(`Move position for ${Moves[move]} (=${move}):`, index); return index; } + +/** + * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase + * @param scene + * @param species + */ +export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: Species[]) { + const starters = generateStarter(scene, species); + starters.forEach((starter) => { + const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); + const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterGender = Gender.MALE; + const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); + const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature); + starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); + scene.getParty().push(starterPokemon); + }); + + scene.currentBattle = new Battle(getGameMode(GameModes.CLASSIC), 5, BattleType.WILD, undefined, false); +} diff --git a/src/test/utils/gameWrapper.ts b/src/test/utils/gameWrapper.ts index 7c0ecac7c128..0ef5c4d46111 100644 --- a/src/test/utils/gameWrapper.ts +++ b/src/test/utils/gameWrapper.ts @@ -229,7 +229,7 @@ export default class GameWrapper { }; this.scene.make = new MockGameObjectCreator(mockTextureManager); this.scene.time = new MockClock(this.scene); - this.scene.remove = vi.fn(); + this.scene.remove = vi.fn(); // TODO: this should be stubbed differently } } diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index a17b841b6821..686de58e874c 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -10,6 +10,8 @@ import { ModifierOverride } from "#app/modifier/modifier-type"; import Overrides from "#app/overrides"; import { vi } from "vitest"; import { GameManagerHelper } from "./gameManagerHelper"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Helper to handle overrides in tests @@ -323,6 +325,41 @@ export class OverridesHelper extends GameManagerHelper { return this; } + /** + * Override the encounter chance for a mystery encounter. + * @param percentage the encounter chance in % + * @returns spy instance + */ + mysteryEncounterChance(percentage: number) { + const maxRate: number = 256; // 100% + const rate = maxRate * (percentage / 100); + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); + return this; + } + + /** + * Override the encounter chance for a mystery encounter. + * @returns spy instance + * @param tier + */ + mysteryEncounterTier(tier: MysteryEncounterTier) { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); + this.log(`Mystery encounter tier set to ${tier}!`); + return this; + } + + /** + * Override the encounter that spawns for the scene + * @param encounterType + * @returns spy instance + */ + mysteryEncounter(encounterType: MysteryEncounterType) { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); + this.log(`Mystery encounter override set to ${encounterType}!`); + return this; + } + private log(...params: any[]) { console.log("Overrides:", ...params); } diff --git a/src/test/utils/helpers/settingsHelper.ts b/src/test/utils/helpers/settingsHelper.ts index 8fca2a34d476..c611a7051078 100644 --- a/src/test/utils/helpers/settingsHelper.ts +++ b/src/test/utils/helpers/settingsHelper.ts @@ -1,6 +1,7 @@ import { PlayerGender } from "#app/enums/player-gender"; import { BattleStyle } from "#app/enums/battle-style"; import { GameManagerHelper } from "./gameManagerHelper"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; /** * Helper to handle settings for tests @@ -38,6 +39,15 @@ export class SettingsHelper extends GameManagerHelper { this.log(`Gender set to: ${PlayerGender[gender]} (=${gender})` ); } + /** + * Change the exp gains speed + * @param speed the {@linkcode ExpGainsSpeed} to set + */ + expGainsSpeed(speed: ExpGainsSpeed) { + this.game.scene.expGainsSpeed = speed; + this.log(`Exp Gains Speed set to: ${ExpGainsSpeed[speed]} (=${speed})` ); + } + private log(...params: any[]) { console.log("Settings:", ...params); } diff --git a/src/test/utils/mocks/mockGameObject.ts b/src/test/utils/mocks/mockGameObject.ts index 9138e0f687a0..4c243ec9ca13 100644 --- a/src/test/utils/mocks/mockGameObject.ts +++ b/src/test/utils/mocks/mockGameObject.ts @@ -1,3 +1,3 @@ export interface MockGameObject { - + name: string; } diff --git a/src/test/utils/mocks/mockTextureManager.ts b/src/test/utils/mocks/mockTextureManager.ts index ca8065bef97b..ce19d6b6432d 100644 --- a/src/test/utils/mocks/mockTextureManager.ts +++ b/src/test/utils/mocks/mockTextureManager.ts @@ -7,7 +7,6 @@ import MockSprite from "#test/utils/mocks/mocksContainer/mockSprite"; import MockText from "#test/utils/mocks/mocksContainer/mockText"; import MockTexture from "#test/utils/mocks/mocksContainer/mockTexture"; import { MockGameObject } from "./mockGameObject"; -import { vi } from "vitest"; import { MockVideoGameObject } from "./mockVideoGameObject"; /** @@ -36,7 +35,7 @@ export default class MockTextureManager { text: this.text.bind(this), bitmapText: this.text.bind(this), displayList: this.displayList, - video: vi.fn(() => new MockVideoGameObject()), + video: () => new MockVideoGameObject(), }; } diff --git a/src/test/utils/mocks/mockVideoGameObject.ts b/src/test/utils/mocks/mockVideoGameObject.ts index d8155e23b6c8..d11fb5a44ce9 100644 --- a/src/test/utils/mocks/mockVideoGameObject.ts +++ b/src/test/utils/mocks/mockVideoGameObject.ts @@ -2,6 +2,8 @@ import { MockGameObject } from "./mockGameObject"; /** Mocks video-related stuff */ export class MockVideoGameObject implements MockGameObject { + public name: string; + constructor() {} public play = () => null; @@ -9,4 +11,5 @@ export class MockVideoGameObject implements MockGameObject { public setOrigin = () => null; public setScale = () => null; public setVisible = () => null; + public setLoop = () => null; } diff --git a/src/test/utils/mocks/mocksContainer/mockContainer.ts b/src/test/utils/mocks/mocksContainer/mockContainer.ts index e13cef0e43ec..05dad327dc64 100644 --- a/src/test/utils/mocks/mocksContainer/mockContainer.ts +++ b/src/test/utils/mocks/mocksContainer/mockContainer.ts @@ -13,7 +13,7 @@ export default class MockContainer implements MockGameObject { public frame; protected textureManager; public list: MockGameObject[] = []; - private name?: string; + public name: string; constructor(textureManager: MockTextureManager, x, y) { this.x = x; @@ -35,6 +35,10 @@ export default class MockContainer implements MockGameObject { // same as remove or destroy } + removeBetween(startIndex, endIndex, destroyChild) { + // Removes multiple children across an index range + } + addedToScene() { // This callback is invoked when this Game Object is added to a Scene. } @@ -151,6 +155,10 @@ export default class MockContainer implements MockGameObject { // Sends this Game Object to the back of its parent's display list. } + moveTo(obj) { + // Moves this Game Object to the given index in the list. + } + moveAbove(obj) { // Moves this Game Object to be above the given Game Object in the display list. } @@ -159,9 +167,9 @@ export default class MockContainer implements MockGameObject { // Moves this Game Object to be below the given Game Object in the display list. } - setName = (name: string) => { + setName(name: string) { this.name = name; - }; + } bringToTop(obj) { // Brings this Game Object to the top of its parents display list. @@ -205,5 +213,9 @@ export default class MockContainer implements MockGameObject { return this.list; } + getByName(key: string) { + return this.list.find(v => v.name === key) ?? new MockContainer(this.textureManager, 0, 0); + } + disableInteractive = () => null; } diff --git a/src/test/utils/mocks/mocksContainer/mockGraphics.ts b/src/test/utils/mocks/mocksContainer/mockGraphics.ts index e026b212e16b..b20faf4ed6a8 100644 --- a/src/test/utils/mocks/mocksContainer/mockGraphics.ts +++ b/src/test/utils/mocks/mocksContainer/mockGraphics.ts @@ -3,6 +3,7 @@ import { MockGameObject } from "../mockGameObject"; export default class MockGraphics implements MockGameObject { private scene; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, config) { this.scene = textureManager.scene; } diff --git a/src/test/utils/mocks/mocksContainer/mockRectangle.ts b/src/test/utils/mocks/mocksContainer/mockRectangle.ts index 26c2f74ea424..48cd2cb1380a 100644 --- a/src/test/utils/mocks/mocksContainer/mockRectangle.ts +++ b/src/test/utils/mocks/mocksContainer/mockRectangle.ts @@ -4,6 +4,7 @@ export default class MockRectangle implements MockGameObject { private fillColor; private scene; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, x, y, width, height, fillColor) { this.fillColor = fillColor; diff --git a/src/test/utils/mocks/mocksContainer/mockSprite.ts b/src/test/utils/mocks/mocksContainer/mockSprite.ts index 83ec39511513..a55b218d0c21 100644 --- a/src/test/utils/mocks/mocksContainer/mockSprite.ts +++ b/src/test/utils/mocks/mocksContainer/mockSprite.ts @@ -14,6 +14,7 @@ export default class MockSprite implements MockGameObject { public scene; public anims; public list: MockGameObject[] = []; + public name: string; constructor(textureManager, x, y, texture) { this.textureManager = textureManager; this.scene = textureManager.scene; diff --git a/src/test/utils/mocks/mocksContainer/mockText.ts b/src/test/utils/mocks/mocksContainer/mockText.ts index 5462056f1e50..f0854fcd90a4 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -10,17 +10,18 @@ export default class MockText implements MockGameObject { public list: MockGameObject[] = []; public style; public text = ""; - private name?: string; + public name: string; public color?: string; constructor(textureManager, x, y, content, styleOptions) { this.scene = textureManager.scene; this.textureManager = textureManager; this.style = {}; - // Phaser.GameObjects.TextStyle.prototype.setStyle = () => null; + // Phaser.GameObjects.TextStyle.prototype.setStyle = () => this; // Phaser.GameObjects.Text.prototype.updateText = () => null; // Phaser.Textures.TextureManager.prototype.addCanvas = () => {}; UI.prototype.showText = this.showText; + UI.prototype.showDialogue = this.showDialogue; this.text = ""; this.phaserText = ""; // super(scene, x, y); @@ -78,13 +79,20 @@ export default class MockText implements MockGameObject { return result; } - showText(text, delay, callback, callbackDelay, prompt, promptDelay) { + showText(text: string, delay?: integer | null, callback?: Function | null, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) { this.scene.messageWrapper.showText(text, delay, callback, callbackDelay, prompt, promptDelay); if (callback) { callback(); } } + showDialogue(keyOrText: string, name: string | undefined, delay: integer | null = 0, callback: Function, callbackDelay?: integer, promptDelay?: integer) { + this.scene.messageWrapper.showDialogue(keyOrText, name, delay, callback, callbackDelay, promptDelay); + if (callback) { + callback(); + } + } + setScale(scale) { // return this.phaserText.setScale(scale); } @@ -192,9 +200,9 @@ export default class MockText implements MockGameObject { }; } - setColor = (color: string) => { + setColor(color: string) { this.color = color; - }; + } setInteractive = () => null; @@ -222,9 +230,9 @@ export default class MockText implements MockGameObject { // return this.phaserText.setAlpha(alpha); } - setName = (name: string) => { + setName(name: string) { this.name = name; - }; + } setAlign(align) { // return this.phaserText.setAlign(align); @@ -248,6 +256,14 @@ export default class MockText implements MockGameObject { }; } + disableInteractive() { + // Disables interaction with this Game Object. + } + + clearTint() { + // Clears tint on this Game Object. + } + add(obj) { // Adds a child to this Game Object. this.list.push(obj); diff --git a/src/test/utils/mocks/mocksContainer/mockTexture.ts b/src/test/utils/mocks/mocksContainer/mockTexture.ts index cb31480cc602..bedd1d2c84a8 100644 --- a/src/test/utils/mocks/mocksContainer/mockTexture.ts +++ b/src/test/utils/mocks/mocksContainer/mockTexture.ts @@ -12,6 +12,7 @@ export default class MockTexture implements MockGameObject { public source; public frames: object; public firstFrame: string; + public name: string; constructor(manager, key: string, source) { this.manager = manager; diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index a89d1788be9c..46bb757c867e 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -43,6 +43,24 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; import { PartyHealPhase } from "#app/phases/party-heal-phase"; import UI, { Mode } from "#app/ui/ui"; +import { + MysteryEncounterBattlePhase, + MysteryEncounterOptionSelectedPhase, + MysteryEncounterPhase, + MysteryEncounterRewardsPhase, + PostMysteryEncounterPhase +} from "#app/phases/mystery-encounter-phases"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; +import { PartyExpPhase } from "#app/phases/party-exp-phase"; + +export interface PromptHandler { + phaseTarget?: string; + mode?: Mode; + callback?: () => void; + expireFn?: () => void; + awaitingActionInput?: boolean; +} +import { ExpPhase } from "#app/phases/exp-phase"; export default class PhaseInterceptor { public scene; @@ -52,7 +70,7 @@ export default class PhaseInterceptor { private interval; private promptInterval; private intervalRun; - private prompts; + private prompts: PromptHandler[]; private phaseFrom; private inProgress; private originalSetMode; @@ -104,10 +122,18 @@ export default class PhaseInterceptor { [EndEvolutionPhase, this.startPhase], [LevelCapPhase, this.startPhase], [AttemptRunPhase, this.startPhase], + [MysteryEncounterPhase, this.startPhase], + [MysteryEncounterOptionSelectedPhase, this.startPhase], + [MysteryEncounterBattlePhase, this.startPhase], + [MysteryEncounterRewardsPhase, this.startPhase], + [PostMysteryEncounterPhase, this.startPhase], + [ModifierRewardPhase, this.startPhase], + [PartyExpPhase, this.startPhase], + [ExpPhase, this.startPhase], ]; private endBySetMode = [ - TitlePhase, SelectGenderPhase, CommandPhase + TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase, MysteryEncounterPhase, PostMysteryEncounterPhase ]; /** @@ -320,7 +346,7 @@ export default class PhaseInterceptor { console.log("setMode", `${Mode[mode]} (=${mode})`, args); const ret = this.originalSetMode.apply(instance, [mode, ...args]); if (!this.phases[currentPhase.constructor.name]) { - throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptior PHASES list`); + throw new Error(`missing ${currentPhase.constructor.name} in phaseInterceptor PHASES list`); } if (this.phases[currentPhase.constructor.name].endBySetMode) { this.inProgress?.callback(); @@ -338,12 +364,15 @@ export default class PhaseInterceptor { const actionForNextPrompt = this.prompts[0]; const expireFn = actionForNextPrompt.expireFn && actionForNextPrompt.expireFn(); const currentMode = this.scene.ui.getMode(); - const currentPhase = this.scene.getCurrentPhase().constructor.name; + const currentPhase = this.scene.getCurrentPhase()?.constructor.name; const currentHandler = this.scene.ui.getHandler(); if (expireFn) { this.prompts.shift(); } else if (currentMode === actionForNextPrompt.mode && currentPhase === actionForNextPrompt.phaseTarget && currentHandler.active && (!actionForNextPrompt.awaitingActionInput || (actionForNextPrompt.awaitingActionInput && currentHandler.awaitingActionInput))) { - this.prompts.shift().callback(); + const prompt = this.prompts.shift(); + if (prompt?.callback) { + prompt.callback(); + } } } }); @@ -355,6 +384,7 @@ export default class PhaseInterceptor { * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. + * @param awaitingActionInput */ addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void, awaitingActionInput: boolean = false) { this.prompts.push({ diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index bf806cd053a4..3bb5c240d940 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -11,7 +11,7 @@ import { initSpecies } from "#app/data/pokemon-species"; import { initAchievements } from "#app/system/achv"; import { initVouchers } from "#app/system/voucher"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; - +import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; import { beforeAll, vi } from "vitest"; /** Mock the override import to always return default values, ignoring any custom overrides. */ @@ -35,6 +35,7 @@ initSpecies(); initMoves(); initAbilities(); initLoggedInUser(); +initMysteryEncounters(); global.testFailed = false; diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index c7b82dc826e6..8e7e5bc40602 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -11,8 +11,11 @@ import { Stat } from "#enums/stat"; import BattleFlyout from "./battle-flyout"; import { WindowVariant, addWindow } from "./ui-theme"; import i18next from "i18next"; +import { ExpGainsSpeed } from "#app/enums/exp-gains-speed"; export default class BattleInfo extends Phaser.GameObjects.Container { + public static readonly EXP_GAINS_DURATION_BASE = 1650; + private baseY: number; private player: boolean; @@ -702,7 +705,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { instant = true; } const durationMultiplier = Phaser.Tweens.Builders.GetEaseFunction("Sine.easeIn")(1 - (Math.max(this.lastLevel - 100, 0) / 150)); - const duration = this.visible && !instant ? (((levelExp - this.lastLevelExp) / relLevelExp) * 1650) * durationMultiplier * levelDurationMultiplier : 0; + let duration = this.visible && !instant ? (((levelExp - this.lastLevelExp) / relLevelExp) * BattleInfo.EXP_GAINS_DURATION_BASE) * durationMultiplier * levelDurationMultiplier : 0; + const speed = (this.scene as BattleScene).expGainsSpeed; + if (speed && speed >= ExpGainsSpeed.DEFAULT) { + duration = speed >= ExpGainsSpeed.SKIP ? ExpGainsSpeed.DEFAULT : duration / Math.pow(2, speed); + } if (ratio === 1) { this.lastLevelExp = 0; this.lastLevel++; diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 60db9d19eefd..3bf551193353 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -10,6 +10,7 @@ import i18next from "i18next"; import {Button} from "#enums/buttons"; import Pokemon, { PokemonMove } from "#app/field/pokemon"; import { CommandPhase } from "#app/phases/command-phase"; +import { BattleType } from "#app/battle"; export default class FightUiHandler extends UiHandler { public static readonly MOVES_CONTAINER_NAME = "moves"; @@ -120,8 +121,12 @@ export default class FightUiHandler extends UiHandler { ui.playError(); } } else { - ui.setMode(Mode.COMMAND, this.fieldIndex); - success = true; + // Cannot back out of fight menu if skipToFightInput is enabled + const { battleType, mysteryEncounter } = this.scene.currentBattle; + if (battleType !== BattleType.MYSTERY_ENCOUNTER || !mysteryEncounter?.skipToFightInput) { + ui.setMode(Mode.COMMAND, this.fieldIndex); + success = true; + } } } else { switch (button) { diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index a78887e1581e..93e00cb6b703 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -29,10 +29,13 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { if (delay === null || delay === undefined) { delay = 20; } + + // Pattern matching regex that checks for @c{}, @f{}, @s{}, and @f{} patterns within message text and parses them to their respective behaviors. const charVarMap = new Map(); const delayMap = new Map(); const soundMap = new Map(); - const actionPattern = /@(c|d|s)\{(.*?)\}/; + const fadeMap = new Map(); + const actionPattern = /@(c|d|s|f)\{(.*?)\}/; let actionMatch: RegExpExecArray | null; while ((actionMatch = actionPattern.exec(text))) { switch (actionMatch[1]) { @@ -45,6 +48,9 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { case "s": soundMap.set(actionMatch.index, actionMatch[2]); break; + case "f": + fadeMap.set(actionMatch.index, parseInt(actionMatch[2])); + break; } text = text.slice(0, actionMatch.index) + text.slice(actionMatch.index + actionMatch[2].length + 4); } @@ -103,6 +109,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { const charVar = charVarMap.get(charIndex); const charSound = soundMap.get(charIndex); const charDelay = delayMap.get(charIndex); + const charFade = fadeMap.get(charIndex); this.message.setText(text.slice(0, charIndex)); const advance = () => { if (charVar) { @@ -134,6 +141,19 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { advance(); } }); + } else if (charFade) { + this.textTimer!.paused = true; + this.scene.time.delayedCall(150, () => { + this.scene.ui.fadeOut(750).then(() => { + const delay = Utils.getFrameMs(charFade); + this.scene.time.delayedCall(delay, () => { + this.scene.ui.fadeIn(500).then(() => { + this.textTimer!.paused = false; + advance(); + }); + }); + }); + }); } else { advance(); } diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index ca5d27f96a4a..c9d3f1957208 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -4,15 +4,17 @@ import { getPokeballAtlasKey, PokeballType } from "../data/pokeball"; import { addTextObject, getTextStyleOptions, getModifierTierTextTint, getTextColor, TextStyle } from "./text"; import AwaitableUiHandler from "./awaitable-ui-handler"; import { Mode } from "./ui"; -import { LockModifierTiersModifier, PokemonHeldItemModifier } from "../modifier/modifier"; +import { LockModifierTiersModifier, PokemonHeldItemModifier, HealShopCostModifier } from "../modifier/modifier"; import { handleTutorial, Tutorial } from "../tutorial"; -import {Button} from "#enums/buttons"; +import { Button } from "#enums/buttons"; import MoveInfoOverlay from "./move-info-overlay"; import { allMoves } from "../data/move"; import * as Utils from "./../utils"; import Overrides from "#app/overrides"; import i18next from "i18next"; import { ShopCursorTarget } from "#app/enums/shop-cursor-target"; +import { IntegerHolder } from "./../utils"; +import Phaser from "phaser"; export const SHOP_OPTIONS_ROW_LIMIT = 6; @@ -22,13 +24,18 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { private lockRarityButtonContainer: Phaser.GameObjects.Container; private transferButtonContainer: Phaser.GameObjects.Container; private checkButtonContainer: Phaser.GameObjects.Container; + private continueButtonContainer: Phaser.GameObjects.Container; private rerollCostText: Phaser.GameObjects.Text; private lockRarityButtonText: Phaser.GameObjects.Text; - private moveInfoOverlay : MoveInfoOverlay; - private moveInfoOverlayActive : boolean = false; + private moveInfoOverlay: MoveInfoOverlay; + private moveInfoOverlayActive: boolean = false; private rowCursor: integer = 0; private player: boolean; + /** + * If reroll cost is negative, it is assumed there are 0 items in the shop. + * It will cause reroll button to be disabled, and a "Continue" button to show in the place of shop items + */ private rerollCost: integer; private transferButtonWidth: integer; private checkButtonWidth: integer; @@ -105,6 +112,15 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonText.setOrigin(0, 0); this.lockRarityButtonContainer.add(this.lockRarityButtonText); + this.continueButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 12), -(this.scene.game.canvas.height / 12)); + this.continueButtonContainer.setVisible(false); + ui.add(this.continueButtonContainer); + + // Create continue button + const continueButtonText = addTextObject(this.scene, -24, 5, i18next.t("modifierSelectUiHandler:continueNextWaveButton"), TextStyle.MESSAGE); + continueButtonText.setName("text-continue-btn"); + this.continueButtonContainer.add(continueButtonText); + // prepare move overlay const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { @@ -113,7 +129,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { onSide: true, right: true, x: 1, - y: -MoveInfoOverlay.getHeight(overlayScale, true) -1, + y: -MoveInfoOverlay.getHeight(overlayScale, true) - 1, width: (this.scene.game.canvas.width / 6) - 2, }); ui.add(this.moveInfoOverlay); @@ -134,7 +150,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { return false; } - if (args.length !== 4 || !(args[1] instanceof Array) || !args[1].length || !(args[2] instanceof Function)) { + if (args.length !== 4 || !(args[1] instanceof Array) || !(args[2] instanceof Function)) { return false; } @@ -159,6 +175,9 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.lockRarityButtonContainer.setVisible(false); this.lockRarityButtonContainer.setAlpha(0); + this.continueButtonContainer.setVisible(false); + this.continueButtonContainer.setAlpha(0); + this.rerollButtonContainer.setPositionRelative(this.lockRarityButtonContainer, 0, canLockRarities ? -12 : 0); this.rerollCost = args[3] as integer; @@ -166,8 +185,11 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); const typeOptions = args[1] as ModifierTypeOption[]; - const shopTypeOptions = !this.scene.gameMode.hasNoShop - ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)) + const removeHealShop = this.scene.gameMode.hasNoShop; + const baseShopCost = new IntegerHolder(this.scene.getWaveMoneyAmount(1)); + this.scene.applyModifier(HealShopCostModifier, true, baseShopCost); + const shopTypeOptions = !removeHealShop + ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, baseShopCost.value) : []; const optionsYOffset = shopTypeOptions.length >= SHOP_OPTIONS_ROW_LIMIT ? -8 : -24; @@ -180,6 +202,11 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.options.push(option); } + // Set "Continue" button height based on number of rows in healing items shop + const continueButton = this.continueButtonContainer.getAt(0); + continueButton.y = optionsYOffset - 5; + continueButton.setVisible(this.options.length === 0); + for (let m = 0; m < shopTypeOptions.length; m++) { const row = m < SHOP_OPTIONS_ROW_LIMIT ? 0 : 1; const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT; @@ -243,16 +270,24 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.rerollButtonContainer.setAlpha(0); this.checkButtonContainer.setAlpha(0); this.lockRarityButtonContainer.setAlpha(0); + this.continueButtonContainer.setAlpha(0); this.rerollButtonContainer.setVisible(true); this.checkButtonContainer.setVisible(true); + this.continueButtonContainer.setVisible(this.rerollCost < 0); this.lockRarityButtonContainer.setVisible(canLockRarities); this.scene.tweens.add({ - targets: [ this.rerollButtonContainer, this.lockRarityButtonContainer, this.checkButtonContainer ], + targets: [ this.lockRarityButtonContainer, this.checkButtonContainer, this.continueButtonContainer ], alpha: 1, duration: 250 }); + this.scene.tweens.add({ + targets: [this.rerollButtonContainer], + alpha: this.rerollCost < 0 ? 0.5 : 1, + duration: 250 + }); + const updateCursorTarget = () => { if (this.scene.shopCursorTarget === ShopCursorTarget.CHECK_TEAM) { this.setRowCursor(0); @@ -418,6 +453,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { // the modifier selection has been updated, always hide the overlay this.moveInfoOverlay.clear(); if (this.rowCursor) { + if (this.rowCursor === 1 && options.length === 0) { + // Continue button when no shop items + this.cursorObj.setScale(1.25); + this.cursorObj.setPosition((this.scene.game.canvas.width / 18) + 23, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22)); + ui.showText(i18next.t("modifierSelectUiHandler:continueNextWaveDescription")); + return ret; + } + const sliceWidth = (this.scene.game.canvas.width / 6) / (options.length + 2); if (this.rowCursor < 2) { this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 20, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22)); @@ -435,10 +478,10 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.cursorObj.setPosition(6, this.lockRarityButtonContainer.visible ? -72 : -60); ui.showText(i18next.t("modifierSelectUiHandler:rerollDesc")); } else if (cursor === 1) { - this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth)/6 - 30, -60); + this.cursorObj.setPosition((this.scene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30, -60); ui.showText(i18next.t("modifierSelectUiHandler:transferDesc")); } else if (cursor === 2) { - this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth)/6 - 10, -60); + this.cursorObj.setPosition((this.scene.game.canvas.width - this.checkButtonWidth) / 6 - 10, -60); ui.showText(i18next.t("modifierSelectUiHandler:checkTeamDesc")); } else { this.cursorObj.setPosition(6, -60); @@ -454,7 +497,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { if (rowCursor !== lastRowCursor) { this.rowCursor = rowCursor; let newCursor = Math.round(this.cursor / Math.max(this.getRowItems(lastRowCursor) - 1, 1) * (this.getRowItems(rowCursor) - 1)); + if (rowCursor === 1 && this.options.length === 0) { + // Handle empty shop + newCursor = 0; + } if (rowCursor === 0) { + if (this.options.length === 0) { + newCursor = 1; + } if (newCursor === 0 && !this.rerollButtonContainer.visible) { newCursor = 1; } @@ -495,6 +545,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { } updateRerollCostText(): void { + const rerollDisabled = this.rerollCost < 0; + if (rerollDisabled) { + this.rerollCostText.setVisible(false); + return; + } else { + this.rerollCostText.setVisible(true); + } const canReroll = this.scene.money >= this.rerollCost; const formattedMoney = Utils.formatMoney(this.scene.moneyFormat, this.rerollCost); @@ -539,7 +596,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { onComplete: () => options.forEach(o => o.destroy()) }); - [ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer ].forEach(container => { + [ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer, this.continueButtonContainer ].forEach(container => { if (container.visible) { this.scene.tweens.add({ targets: container, diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts new file mode 100644 index 000000000000..307bab0a3af5 --- /dev/null +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -0,0 +1,623 @@ +import BattleScene from "../battle-scene"; +import { addBBCodeTextObject, getBBCodeFrag, TextStyle } from "./text"; +import { Mode } from "./ui"; +import UiHandler from "./ui-handler"; +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 * as Utils from "../utils"; +import { isNullOrUndefined } from "../utils"; +import { getPokeballAtlasKey } from "../data/pokeball"; +import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import i18next from "i18next"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; + +export default class MysteryEncounterUiHandler extends UiHandler { + private cursorContainer: Phaser.GameObjects.Container; + private cursorObj?: Phaser.GameObjects.Image; + + private optionsContainer: Phaser.GameObjects.Container; + // Length = max number of allowable options (4) + private optionScrollTweens: (Phaser.Tweens.Tween | null)[] = new Array(4).fill(null); + + private tooltipWindow: Phaser.GameObjects.NineSlice; + private tooltipContainer: Phaser.GameObjects.Container; + private tooltipScrollTween?: Phaser.Tweens.Tween; + + private descriptionWindow: Phaser.GameObjects.NineSlice; + private descriptionContainer: Phaser.GameObjects.Container; + private descriptionScrollTween?: Phaser.Tweens.Tween; + private rarityBall: Phaser.GameObjects.Sprite; + + private dexProgressWindow: Phaser.GameObjects.NineSlice; + private dexProgressContainer: Phaser.GameObjects.Container; + private showDexProgress: boolean = false; + + private overrideSettings?: OptionSelectSettings; + private encounterOptions: MysteryEncounterOption[] = []; + private optionsMeetsReqs: boolean[]; + + protected viewPartyIndex: integer = 0; + + protected blockInput: boolean = true; + + constructor(scene: BattleScene) { + super(scene, Mode.MYSTERY_ENCOUNTER); + } + + override setup() { + const ui = this.getUi(); + + this.cursorContainer = this.scene.add.container(18, -38.7); + this.cursorContainer.setVisible(false); + ui.add(this.cursorContainer); + this.optionsContainer = this.scene.add.container(12, -38.7); + this.optionsContainer.setVisible(false); + ui.add(this.optionsContainer); + this.dexProgressContainer = this.scene.add.container(214, -43); + this.dexProgressContainer.setVisible(false); + ui.add(this.dexProgressContainer); + this.descriptionContainer = this.scene.add.container(0, -152); + this.descriptionContainer.setVisible(false); + ui.add(this.descriptionContainer); + this.tooltipContainer = this.scene.add.container(210, -48); + this.tooltipContainer.setVisible(false); + ui.add(this.tooltipContainer); + + this.setCursor(this.getCursor()); + + this.descriptionWindow = addWindow(this.scene, 0, 0, 150, 105, false, false, 0, 0, WindowVariant.THIN); + this.descriptionContainer.add(this.descriptionWindow); + + this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN); + this.tooltipContainer.add(this.tooltipWindow); + + this.dexProgressWindow = addWindow(this.scene, 0, 0, 24, 28, false, false, 0, 0, WindowVariant.THIN); + this.dexProgressContainer.add(this.dexProgressWindow); + + this.rarityBall = this.scene.add.sprite(141, 9, "pb"); + this.rarityBall.setScale(0.75); + this.descriptionContainer.add(this.rarityBall); + + const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar"); + dexProgressIndicator.setScale(0.80); + this.dexProgressContainer.add(dexProgressIndicator); + this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); + } + + override show(args: any[]): boolean { + super.show(args); + + this.overrideSettings = args[0] as OptionSelectSettings ?? {}; + const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription; + const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription; + const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; + + this.cursorContainer.setVisible(true); + this.descriptionContainer.setVisible(showDescriptionContainer); + this.optionsContainer.setVisible(true); + this.dexProgressContainer.setVisible(true); + this.displayEncounterOptions(slideInDescription); + const cursor = this.getCursor(); + if (cursor === (this.optionsContainer?.length || 0) - 1) { + // Always resets cursor on view party button if it was last there + this.setCursor(cursor); + } else { + this.setCursor(startingCursorIndex); + } + if (this.blockInput) { + setTimeout(() => { + this.unblockInput(); + }, 1000); + } + this.displayOptionTooltip(); + + return true; + } + + override processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + + const cursor = this.getCursor(); + + if (button === Button.CANCEL || button === Button.ACTION) { + if (button === Button.ACTION) { + const selected = this.encounterOptions[cursor]; + if (cursor === this.viewPartyIndex) { + // Handle view party + success = true; + const overrideSettings: OptionSelectSettings = { + ...this.overrideSettings, + slideInDescription: false + }; + this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => { + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings); + setTimeout(() => { + this.setCursor(this.viewPartyIndex); + this.unblockInput(); + }, 300); + }); + } else if (this.blockInput || (!this.optionsMeetsReqs[cursor] && (selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL))) { + success = false; + } else { + if ((this.scene.getCurrentPhase() as MysteryEncounterPhase).handleOptionSelect(selected, cursor)) { + success = true; + } else { + ui.playError(); + } + } + } else { + // TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk + } + } else { + switch (this.optionsContainer.getAll()?.length) { + default: + case 3: + success = this.handleTwoOptionMoveInput(button); + break; + case 4: + success = this.handleThreeOptionMoveInput(button); + break; + case 5: + success = this.handleFourOptionMoveInput(button); + break; + } + + this.displayOptionTooltip(); + } + + if (success) { + ui.playSelect(); + } + + return success; + } + + private handleTwoOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor < this.viewPartyIndex) { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } + break; + case Button.LEFT: + if (cursor > 0) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor < this.viewPartyIndex) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + private handleThreeOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor === 2) { + success = this.setCursor(cursor - 2); + } else { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else { + success = this.setCursor(2); + } + break; + case Button.LEFT: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else if (cursor === 1) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor === 1) { + success = this.setCursor(this.viewPartyIndex); + } else if (cursor < 1) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + private handleFourOptionMoveInput(button: Button): boolean { + let success = false; + const cursor = this.getCursor(); + switch (button) { + case Button.UP: + if (cursor >= 2 && cursor !== this.viewPartyIndex) { + success = this.setCursor(cursor - 2); + } else { + success = this.setCursor(this.viewPartyIndex); + } + break; + case Button.DOWN: + if (cursor <= 1) { + success = this.setCursor(cursor + 2); + } else if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } + break; + case Button.LEFT: + if (cursor === this.viewPartyIndex) { + success = this.setCursor(1); + } else if (cursor % 2 === 1) { + success = this.setCursor(cursor - 1); + } + break; + case Button.RIGHT: + if (cursor === 1) { + success = this.setCursor(this.viewPartyIndex); + } else if (cursor % 2 === 0 && cursor !== this.viewPartyIndex) { + success = this.setCursor(cursor + 1); + } + break; + } + + return success; + } + + /** + * When ME UI first displays, the option buttons will be disabled temporarily to prevent player accidentally clicking through hastily + * This method is automatically called after a short delay but can also be called manually + */ + unblockInput() { + if (this.blockInput) { + this.blockInput = false; + for (let i = 0; i < this.optionsContainer.length - 1; i++) { + const optionMode = this.encounterOptions[i].optionMode; + if (!this.optionsMeetsReqs[i] && (optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + continue; + } + (this.optionsContainer.getAt(i) as Phaser.GameObjects.Text).setAlpha(1); + } + } + } + + override getCursor(): integer { + return this.cursor ? this.cursor : 0; + } + + override setCursor(cursor: integer): boolean { + const prevCursor = this.getCursor(); + const changed = prevCursor !== cursor; + if (changed) { + this.cursor = cursor; + } + + this.viewPartyIndex = this.optionsContainer.getAll()?.length - 1; + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.image(0, 0, "cursor"); + this.cursorContainer.add(this.cursorObj); + } + + if (cursor === this.viewPartyIndex) { + this.cursorObj.setPosition(246, -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 + this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); + } else if (this.optionsContainer.getAll()?.length === 5) { // 4 Options + this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0)); + } + + return changed; + } + + displayEncounterOptions(slideInDescription: boolean = true): void { + this.getUi().clearText(); + const mysteryEncounter = this.scene.currentBattle.mysteryEncounter!; + this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options; + this.optionsMeetsReqs = []; + + const titleText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.title, TextStyle.TOOLTIP_TITLE); + const descriptionText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.description, TextStyle.TOOLTIP_CONTENT); + const queryText: string | null = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue?.query, TextStyle.TOOLTIP_CONTENT); + + // Clear options container (except cursor) + this.optionsContainer.removeAll(true); + + // Options Window + for (let i = 0; i < this.encounterOptions.length; i++) { + const option = this.encounterOptions[i]; + + let optionText: BBCodeText; + switch (this.encounterOptions.length) { + default: + case 2: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + case 3: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + case 4: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { fontSize: "80px", lineSpacing: -8 }); + break; + } + + this.optionsMeetsReqs.push(option.meetsRequirements(this.scene)); + const optionDialogue = option.dialogue!; + const label = !this.optionsMeetsReqs[i] && optionDialogue.disabledButtonLabel ? optionDialogue.disabledButtonLabel : optionDialogue.buttonLabel; + let text: string | null; + if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + // Options with special requirements that are met are automatically colored green + text = getEncounterText(this.scene, label, TextStyle.SUMMARY_GREEN); + } else { + text = getEncounterText(this.scene, label, optionDialogue.style ? optionDialogue.style : TextStyle.WINDOW); + } + + if (text) { + optionText.setText(text); + } + + if (!this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { + optionText.setAlpha(0.5); + } + if (this.blockInput) { + optionText.setAlpha(0.5); + } + + // Sets up the mask that hides the option text to give an illusion of scrolling + const nonScrollWidth = 90; + const optionTextMaskRect = this.scene.make.graphics({}); + optionTextMaskRect.setScale(6); + optionTextMaskRect.fillStyle(0xFFFFFF); + optionTextMaskRect.beginPath(); + optionTextMaskRect.fillRect(optionText.x + 11, optionText.y + 140, nonScrollWidth, 18); + + const optionTextMask = optionTextMaskRect.createGeometryMask(); + optionText.setMask(optionTextMask); + + const optionTextWidth = optionText.displayWidth; + + const tween = this.optionScrollTweens[i]; + if (tween) { + tween.remove(); + this.optionScrollTweens[i] = null; + } + + // Animates the option text scrolling sideways + if (optionTextWidth > nonScrollWidth) { + this.optionScrollTweens[i] = this.scene.tweens.add({ + targets: optionText, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((optionTextWidth - nonScrollWidth) / 15 * 2000), + x: `-=${(optionTextWidth - nonScrollWidth)}` + }); + } + + this.optionsContainer.add(optionText); + } + + // View Party Button + const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag(i18next.t("mysteryEncounterMessages:view_party_button"), TextStyle.PARTY), TextStyle.PARTY); + this.optionsContainer.add(viewPartyText); + + // Description Window + const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText ?? "", TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); + this.descriptionContainer.add(titleTextObject); + titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5); + + // Rarity of encounter + const index = mysteryEncounter.encounterTier === MysteryEncounterTier.COMMON ? 0 : + mysteryEncounter.encounterTier === MysteryEncounterTier.GREAT ? 1 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ULTRA ? 2 : + mysteryEncounter.encounterTier === MysteryEncounterTier.ROGUE ? 3 : 4; + const ballType = getPokeballAtlasKey(index); + this.rarityBall.setTexture("pb", ballType); + + const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + + // Sets up the mask that hides the description text to give an illusion of scrolling + const descriptionTextMaskRect = this.scene.make.graphics({}); + descriptionTextMaskRect.setScale(6); + descriptionTextMaskRect.fillStyle(0xFFFFFF); + descriptionTextMaskRect.beginPath(); + descriptionTextMaskRect.fillRect(6, 53, 206, 57); + + const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask(); + + descriptionTextObject.setMask(abilityDescriptionTextMask); + + const descriptionLineCount = Math.floor(descriptionTextObject.displayHeight / 10); + + if (this.descriptionScrollTween) { + this.descriptionScrollTween.remove(); + this.descriptionScrollTween = undefined; + } + + // Animates the description text moving upwards + if (descriptionLineCount > 6) { + this.descriptionScrollTween = this.scene.tweens.add({ + targets: descriptionTextObject, + delay: Utils.fixedInt(2000), + loop: -1, + hold: Utils.fixedInt(2000), + duration: Utils.fixedInt((descriptionLineCount - 6) * 2000), + y: `-=${10 * (descriptionLineCount - 6)}` + }); + } + + this.descriptionContainer.add(descriptionTextObject); + + const queryTextObject = addBBCodeTextObject(this.scene, 0, 0, queryText ?? "", TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + this.descriptionContainer.add(queryTextObject); + queryTextObject.setPosition(75 - queryTextObject.displayWidth / 2, 90); + + // Slide in description container + if (slideInDescription) { + this.descriptionContainer.x -= 150; + this.scene.tweens.add({ + targets: this.descriptionContainer, + x: "+=150", + ease: "Sine.easeInOut", + duration: 1000 + }); + } + } + + /** + * Updates and displays the tooltip for a given option + * The tooltip will auto wrap and scroll if it is too long + */ + private displayOptionTooltip() { + const cursor = this.getCursor(); + // Clear tooltip box + if (this.tooltipContainer.length > 1) { + this.tooltipContainer.removeBetween(1, this.tooltipContainer.length, true); + } + this.tooltipContainer.setVisible(true); + + if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { + // Ignore hovers on view party button + // Hide dex progress if visible + this.showHideDexProgress(false); + return; + } + + let text: string | null; + const cursorOption = this.encounterOptions[cursor]; + const optionDialogue = cursorOption.dialogue!; + if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) { + text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT); + } else { + text = getEncounterText(this.scene, optionDialogue.buttonTooltip, TextStyle.TOOLTIP_CONTENT); + } + + // Auto-color options green/blue for good/bad by looking for (+)/(-) + if (text) { + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0]; + text = text.replace(/(\(\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString); + text = text.replace(/(\(\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString); + } + + if (text) { + const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" }); + this.tooltipContainer.add(tooltipTextObject); + + // Sets up the mask that hides the description text to give an illusion of scrolling + const tooltipTextMaskRect = this.scene.make.graphics({}); + tooltipTextMaskRect.setScale(6); + tooltipTextMaskRect.fillStyle(0xFFFFFF); + tooltipTextMaskRect.beginPath(); + tooltipTextMaskRect.fillRect(this.tooltipContainer.x, this.tooltipContainer.y + 188.5, 150, 32); + + const textMask = tooltipTextMaskRect.createGeometryMask(); + tooltipTextObject.setMask(textMask); + + const tooltipLineCount = Math.floor(tooltipTextObject.displayHeight / 11.2); + + if (this.tooltipScrollTween) { + this.tooltipScrollTween.remove(); + this.tooltipScrollTween = undefined; + } + + // Animates the tooltip text moving upwards + if (tooltipLineCount > 3) { + this.tooltipScrollTween = this.scene.tweens.add({ + targets: tooltipTextObject, + delay: Utils.fixedInt(1200), + loop: -1, + hold: Utils.fixedInt(1200), + duration: Utils.fixedInt((tooltipLineCount - 3) * 1200), + y: `-=${11.2 * (tooltipLineCount - 3)}` + }); + } + } + + // Dex progress indicator + if (cursorOption.hasDexProgress && !this.showDexProgress) { + this.showHideDexProgress(true); + } else if (!cursorOption.hasDexProgress) { + this.showHideDexProgress(false); + } + } + + override clear(): void { + super.clear(); + this.overrideSettings = undefined; + this.optionsContainer.setVisible(false); + this.optionsContainer.removeAll(true); + this.dexProgressContainer.setVisible(false); + this.descriptionContainer.setVisible(false); + this.tooltipContainer.setVisible(false); + // Keeps container background and pokeball + this.descriptionContainer.removeBetween(2, this.descriptionContainer.length, true); + this.getUi().getMessageHandler().clearText(); + this.eraseCursor(); + } + + private eraseCursor(): void { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = undefined; + } + + /** + * Will show or hide the Dex progress icon for an option that has dex progress + * @param show - if true does show, if false does hide + */ + private showHideDexProgress(show: boolean) { + if (show && !this.showDexProgress) { + this.showDexProgress = true; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -63, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + this.dexProgressContainer.on("pointerover", () => { + (this.scene as BattleScene).ui.showTooltip("", i18next.t("mysteryEncounterMessages:affects_pokedex"), true); + }); + this.dexProgressContainer.on("pointerout", () => { + (this.scene as BattleScene).ui.hideTooltip(); + }); + } + }); + } else if (!show && this.showDexProgress) { + this.showDexProgress = false; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -43, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + this.dexProgressContainer.off("pointerover"); + this.dexProgressContainer.off("pointerout"); + } + }); + } + } +} diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 270163a3d3ec..a793dad6a737 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -90,7 +90,12 @@ export enum PartyUiMode { * Indicates that the party UI is open to check the team. This * type of selection can be cancelled. */ - CHECK + CHECK, + /** + * Indicates that the party UI is open to select a party member for an arbitrary effect. + * This is generally used in for Mystery Encounter or special effects that require the player to select a Pokemon + */ + SELECT } export enum PartyOption { @@ -107,6 +112,7 @@ export enum PartyOption { UNSPLICE, RELEASE, RENAME, + SELECT, SCROLL_UP = 1000, SCROLL_DOWN = 1001, FORM_CHANGE_ITEM = 2000, @@ -210,7 +216,7 @@ export default class PartyUiHandler extends MessageUiHandler { public static NoEffectMessage = i18next.t("partyUiHandler:anyEffect"); - private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME]; + private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.RENAME, PartyOption.SELECT]; constructor(scene: BattleScene) { super(scene, Mode.PARTY); @@ -461,8 +467,8 @@ export default class PartyUiHandler extends MessageUiHandler { } else if (option === PartyOption.UNPAUSE_EVOLUTION) { this.clearOptions(); ui.playSelect(); - pokemon.pauseEvolutions = false; - this.showText(i18next.t("partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon) }), undefined, () => this.showText("", 0), null, true); + pokemon.pauseEvolutions = !pokemon.pauseEvolutions; + this.showText(i18next.t(pokemon.pauseEvolutions? "partyUiHandler:pausedEvolutions" : "partyUiHandler:unpausedEvolutions", { pokemonName: getPokemonNameWithAffix(pokemon) }), undefined, () => this.showText("", 0), null, true); } else if (option === PartyOption.UNSPLICE) { this.clearOptions(); ui.playSelect(); @@ -523,6 +529,9 @@ export default class PartyUiHandler extends MessageUiHandler { return true; } else if (option === PartyOption.CANCEL) { return this.processInput(Button.CANCEL); + } else if (option === PartyOption.SELECT) { + ui.playSelect(); + return true; } } else if (button === Button.CANCEL) { this.clearOptions(); @@ -872,12 +881,15 @@ export default class PartyUiHandler extends MessageUiHandler { } } break; + case PartyUiMode.SELECT: + this.options.push(PartyOption.SELECT); + break; } this.options.push(PartyOption.SUMMARY); this.options.push(PartyOption.RENAME); - if (pokemon.pauseEvolutions && (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) { + if ((pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) || (pokemon.isFusion() && pokemon.fusionSpecies && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId)))) { this.options.push(PartyOption.UNPAUSE_EVOLUTION); } @@ -964,6 +976,8 @@ export default class PartyUiHandler extends MessageUiHandler { if (formChangeItemModifiers && option >= PartyOption.FORM_CHANGE_ITEM) { const modifier = formChangeItemModifiers[option - PartyOption.FORM_CHANGE_ITEM]; optionName = `${modifier.active ? i18next.t("partyUiHandler:DEACTIVATE") : i18next.t("partyUiHandler:ACTIVATE")} ${modifier.type.name}`; + } else if (option === PartyOption.UNPAUSE_EVOLUTION) { + optionName = `${pokemon.pauseEvolutions ? i18next.t("partyUiHandler:UNPAUSE_EVOLUTION") : i18next.t("partyUiHandler:PAUSE_EVOLUTION")}`; } else { if (this.localizedOptions.includes(option)) { optionName = i18next.t(`partyUiHandler:${PartyOption[option]}`); diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index b7ad5f5adec9..8f0437002d44 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -180,9 +180,9 @@ export default class RunInfoUiHandler extends UiHandler { private async parseRunResult() { const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderStr = PlayerGender[genderIndex]; - const runResultTextStyle = this.isVictory ? TextStyle.SUMMARY : TextStyle.SUMMARY_RED; + const runResultTextStyle = this.isVictory ? TextStyle.PERFECT_IV : TextStyle.SUMMARY_RED; const runResultTitle = this.isVictory ? i18next.t("runHistory:victory") : i18next.t("runHistory:defeated", { context: genderStr }); - const runResultText = addBBCodeTextObject(this.scene, 6, 5, `${runResultTitle} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, runResultTextStyle, {fontSize : "65px", lineSpacing: 0.1}); + const runResultText = addTextObject(this.scene, 6, 5, `${runResultTitle} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, runResultTextStyle, {fontSize : "65px", lineSpacing: 0.1}); if (this.isVictory) { const hallofFameInstructionContainer = this.scene.add.container(0, 0); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index ac6af5cbb04e..ee56a3631ddc 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -13,7 +13,7 @@ import { allMoves } from "../data/move"; import { Nature, getNatureName } from "../data/nature"; import { pokemonFormChanges } from "../data/pokemon-forms"; import { LevelMoves, pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "../data/pokemon-level-moves"; -import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities, getPokerusStarters } from "../data/pokemon-species"; +import PokemonSpecies, { allSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities, POKERUS_STARTER_COUNT, getPokerusStarters } from "../data/pokemon-species"; import { Type } from "../data/type"; import { GameModes } from "../game-mode"; import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "../system/game-data"; @@ -631,7 +631,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { starterBoxContainer.add(this.starterSelectScrollBar); - this.pokerusCursorObjs = new Array(3).fill(null).map(() => { + this.pokerusCursorObjs = new Array(POKERUS_STARTER_COUNT).fill(null).map(() => { const cursorObj = this.scene.add.image(0, 0, "select_cursor_pokerus"); cursorObj.setVisible(false); cursorObj.setOrigin(0, 0); diff --git a/src/ui/text.ts b/src/ui/text.ts index 99a0436bba30..58b6343144ac 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -226,6 +226,34 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui return `[color=${getTextColor(textStyle, false, uiTheme)}][shadow=${getTextColor(textStyle, true, uiTheme)}]${content}`; } +/** + * Should only be used with BBCodeText (see {@linkcode addBBCodeTextObject()}) + * This does NOT work with UI showText() or showDialogue() methods. + * Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content: + * @[]{} + * + * Example: passing a content string of "@[SUMMARY_BLUE]{blue text} primaryStyle text @[SUMMARY_RED]{red text}" will result in: + * - "blue text" with TextStyle.SUMMARY_BLUE applied + * - " primaryStyle text " with primaryStyle TextStyle applied + * - "red text" with TextStyle.SUMMARY_RED applied + * @param content string with styling that need to be applied for BBCodeTextObject + * @param primaryStyle Primary style is required in order to escape BBCode styling properly. + * @param uiTheme + */ +export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { + // Apply primary styling before anything else + let text = getBBCodeFrag(content, primaryStyle, uiTheme) + "[/color][/shadow]"; + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))!][0]; + + // Set custom colors + text = text.replace(/@\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { + return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle], uiTheme) + "[/color][/shadow]" + primaryStyleString; + }); + + // Remove extra style block at the end + return text.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, ""); +} + export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: UiTheme = UiTheme.DEFAULT): string { const isLegacyTheme = uiTheme === UiTheme.LEGACY; switch (textStyle) { diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 82b3ee6b4fa5..0f4fa52e41e6 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -53,6 +53,7 @@ import EggSummaryUiHandler from "./egg-summary-ui-handler"; import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; import { Device } from "#enums/devices"; +import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; export enum Mode { MESSAGE, @@ -97,6 +98,7 @@ export enum Mode { TEST_DIALOGUE, AUTO_COMPLETE, ADMIN, + MYSTERY_ENCOUNTER } const transitionModes = [ @@ -137,6 +139,7 @@ const noTransitionModes = [ Mode.TEST_DIALOGUE, Mode.AUTO_COMPLETE, Mode.ADMIN, + Mode.MYSTERY_ENCOUNTER ]; export default class UI extends Phaser.GameObjects.Container { @@ -204,6 +207,7 @@ export default class UI extends Phaser.GameObjects.Container { new TestDialogueUiHandler(scene, Mode.TEST_DIALOGUE), new AutoCompleteUiHandler(scene), new AdminUiHandler(scene), + new MysteryEncounterUiHandler(scene), ]; } diff --git a/src/utils.ts b/src/utils.ts index 7decf9bb4c0b..a8206bf4dcfb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -587,6 +587,14 @@ export function isNullOrUndefined(object: any): boolean { return null === object || undefined === object; } +/** + * Capitalizes the first letter of a string + * @param str + */ +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + /** * This function is used in the context of a Pokémon battle game to calculate the actual integer damage value from a float result. * Many damage calculation formulas involve various parameters and result in float values.