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 00000000000..a630fe549db 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 00000000000..4be7f0756ee --- /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 00000000000..bb22f721d9a --- /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 00000000000..21f3bec585f --- /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 00000000000..00e552dc503 --- /dev/null +++ b/public/battle-anims/encounter-smokescreen.json @@ -0,0 +1,1694 @@ +{ + "graphic": "PRAS- Smokescreen", + "frames": [ + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 12.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 21.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 17.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 13.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 21, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -16, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 5.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -20, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -24, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": -2.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 4.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -6.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -28, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 23, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -10.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -32, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 19, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -14.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -36, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -18.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -11, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": -1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": -5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "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": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ] + ], + "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 c347790b92f..b318f79b0a0 100644 --- a/public/images/items.json +++ b/public/images/items.json @@ -4,8 +4,8 @@ "image": "items.png", "format": "RGBA8888", "size": { - "w": 425, - "h": 425 + "w": 428, + "h": 428 }, "scale": 1, "frames": [ @@ -387,6 +387,27 @@ "h": 28 } }, + { + "filename": "mega_bracelet", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 16 + }, + "frame": { + "x": 0, + "y": 412, + "w": 20, + "h": 16 + } + }, { "filename": "ability_charm", "rotated": false, @@ -619,7 +640,7 @@ } }, { - "filename": "elixir", + "filename": "catching_charm", "rotated": false, "trimmed": true, "sourceSize": { @@ -627,15 +648,15 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, + "x": 5, "y": 4, - "w": 18, + "w": 21, "h": 24 }, "frame": { "x": 407, "y": 0, - "w": 18, + "w": 21, "h": 24 } }, @@ -892,7 +913,7 @@ } }, { - "filename": "coupon", + "filename": "exp_balance", "rotated": false, "trimmed": true, "sourceSize": { @@ -901,36 +922,15 @@ }, "spriteSourceSize": { "x": 4, - "y": 7, - "w": 23, - "h": 19 + "y": 5, + "w": 24, + "h": 22 }, "frame": { "x": 22, "y": 406, - "w": 23, - "h": 19 - } - }, - { - "filename": "golden_mystic_ticket", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 - }, - "frame": { - "x": 45, - "y": 406, - "w": 23, - "h": 19 + "w": 24, + "h": 22 } }, { @@ -955,7 +955,7 @@ } }, { - "filename": "mega_bracelet", + "filename": "relic_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -963,20 +963,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, + "x": 7, + "y": 9, + "w": 17, "h": 16 }, "frame": { "x": 28, "y": 70, - "w": 20, + "w": 17, "h": 16 } }, { - "filename": "calcium", + "filename": "abomasite", "rotated": false, "trimmed": true, "sourceSize": { @@ -985,57 +985,15 @@ }, "spriteSourceSize": { "x": 8, - "y": 4, + "y": 8, "w": 16, - "h": 24 + "h": 16 }, "frame": { - "x": 39, - "y": 86, + "x": 45, + "y": 70, "w": 16, - "h": 24 - } - }, - { - "filename": "carbos", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 39, - "y": 110, - "w": 16, - "h": 24 - } - }, - { - "filename": "catching_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 21, - "h": 24 - }, - "frame": { - "x": 39, - "y": 134, - "w": 21, - "h": 24 + "h": 16 } }, { @@ -1054,7 +1012,7 @@ }, "frame": { "x": 39, - "y": 158, + "y": 86, "w": 24, "h": 24 } @@ -1075,7 +1033,7 @@ }, "frame": { "x": 39, - "y": 182, + "y": 110, "w": 24, "h": 24 } @@ -1094,6 +1052,69 @@ "w": 24, "h": 24 }, + "frame": { + "x": 39, + "y": 134, + "w": 24, + "h": 24 + } + }, + { + "filename": "golden_punch", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 39, + "y": 158, + "w": 24, + "h": 24 + } + }, + { + "filename": "gracidea", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 39, + "y": 182, + "w": 24, + "h": 24 + } + }, + { + "filename": "grip_claw", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, "frame": { "x": 44, "y": 206, @@ -1102,7 +1123,7 @@ } }, { - "filename": "golden_punch", + "filename": "icicle_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1123,7 +1144,7 @@ } }, { - "filename": "gracidea", + "filename": "insect_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1143,69 +1164,6 @@ "h": 24 } }, - { - "filename": "grip_claw", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 46, - "y": 278, - "w": 24, - "h": 24 - } - }, - { - "filename": "icicle_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 46, - "y": 302, - "w": 24, - "h": 24 - } - }, - { - "filename": "insect_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 46, - "y": 326, - "w": 24, - "h": 24 - } - }, { "filename": "iron_plate", "rotated": false, @@ -1222,7 +1180,7 @@ }, "frame": { "x": 46, - "y": 350, + "y": 278, "w": 24, "h": 24 } @@ -1243,137 +1201,11 @@ }, "frame": { "x": 46, - "y": 374, + "y": 302, "w": 24, "h": 24 } }, - { - "filename": "abomasite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 48, - "y": 70, - "w": 16, - "h": 16 - } - }, - { - "filename": "ether", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 - }, - "frame": { - "x": 55, - "y": 86, - "w": 18, - "h": 24 - } - }, - { - "filename": "full_restore", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 - }, - "frame": { - "x": 55, - "y": 110, - "w": 18, - "h": 24 - } - }, - { - "filename": "hp_up", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 60, - "y": 134, - "w": 16, - "h": 24 - } - }, - { - "filename": "iron", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 63, - "y": 158, - "w": 16, - "h": 24 - } - }, - { - "filename": "kings_rock", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 23, - "h": 24 - }, - "frame": { - "x": 63, - "y": 182, - "w": 23, - "h": 24 - } - }, { "filename": "lucky_punch_great", "rotated": false, @@ -1389,8 +1221,8 @@ "h": 24 }, "frame": { - "x": 68, - "y": 206, + "x": 46, + "y": 326, "w": 24, "h": 24 } @@ -1410,8 +1242,8 @@ "h": 24 }, "frame": { - "x": 68, - "y": 230, + "x": 46, + "y": 350, "w": 24, "h": 24 } @@ -1431,8 +1263,8 @@ "h": 24 }, "frame": { - "x": 69, - "y": 254, + "x": 46, + "y": 374, "w": 24, "h": 24 } @@ -1452,112 +1284,7 @@ "h": 24 }, "frame": { - "x": 70, - "y": 278, - "w": 24, - "h": 24 - } - }, - { - "filename": "meadow_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 70, - "y": 302, - "w": 24, - "h": 24 - } - }, - { - "filename": "mind_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 70, - "y": 326, - "w": 24, - "h": 24 - } - }, - { - "filename": "muscle_band", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 70, - "y": 350, - "w": 24, - "h": 24 - } - }, - { - "filename": "pixie_plate", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 70, - "y": 374, - "w": 24, - "h": 24 - } - }, - { - "filename": "salac_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 24, - "h": 24 - }, - "frame": { - "x": 68, + "x": 46, "y": 398, "w": 24, "h": 24 @@ -1585,7 +1312,7 @@ } }, { - "filename": "lure", + "filename": "calcium", "rotated": false, "trimmed": true, "sourceSize": { @@ -1595,39 +1322,18 @@ "spriteSourceSize": { "x": 8, "y": 4, - "w": 17, - "h": 24 - }, - "frame": { - "x": 92, - "y": 398, - "w": 17, - "h": 24 - } - }, - { - "filename": "max_elixir", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, + "w": 16, "h": 24 }, "frame": { "x": 59, "y": 27, - "w": 18, + "w": 16, "h": 24 } }, { - "filename": "scanner", + "filename": "meadow_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1641,14 +1347,14 @@ "h": 24 }, "frame": { - "x": 77, + "x": 75, "y": 26, "w": 24, "h": 24 } }, { - "filename": "silk_scarf", + "filename": "mind_plate", "rotated": false, "trimmed": true, "sourceSize": { @@ -1662,12 +1368,33 @@ "h": 24 }, "frame": { - "x": 101, + "x": 99, "y": 26, "w": 24, "h": 24 } }, + { + "filename": "revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 12, + "h": 17 + }, + "frame": { + "x": 123, + "y": 26, + "w": 12, + "h": 17 + } + }, { "filename": "big_mushroom", "rotated": false, @@ -1732,7 +1459,7 @@ } }, { - "filename": "sky_plate", + "filename": "absolite", "rotated": false, "trimmed": true, "sourceSize": { @@ -1740,20 +1467,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 61, + "y": 70, + "w": 16, + "h": 16 + } + }, + { + "filename": "carbos", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, "y": 4, - "w": 24, + "w": 16, "h": 24 }, "frame": { - "x": 125, - "y": 36, - "w": 24, + "x": 63, + "y": 86, + "w": 16, "h": 24 } }, { - "filename": "choice_specs", + "filename": "elixir", "rotated": false, "trimmed": true, "sourceSize": { @@ -1761,16 +1509,100 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 24, - "h": 18 + "x": 7, + "y": 4, + "w": 18, + "h": 24 }, "frame": { - "x": 125, - "y": 60, - "w": 24, - "h": 18 + "x": 63, + "y": 110, + "w": 18, + "h": 24 + } + }, + { + "filename": "ether", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 18, + "h": 24 + }, + "frame": { + "x": 63, + "y": 134, + "w": 18, + "h": 24 + } + }, + { + "filename": "full_restore", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 18, + "h": 24 + }, + "frame": { + "x": 63, + "y": 158, + "w": 18, + "h": 24 + } + }, + { + "filename": "kings_rock", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 23, + "h": 24 + }, + "frame": { + "x": 63, + "y": 182, + "w": 23, + "h": 24 + } + }, + { + "filename": "max_elixir", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 18, + "h": 24 + }, + "frame": { + "x": 68, + "y": 206, + "w": 18, + "h": 24 } }, { @@ -1788,14 +1620,140 @@ "h": 24 }, "frame": { - "x": 149, - "y": 36, + "x": 68, + "y": 230, "w": 18, "h": 24 } }, { - "filename": "adamant_crystal", + "filename": "lure", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 17, + "h": 24 + }, + "frame": { + "x": 69, + "y": 254, + "w": 17, + "h": 24 + } + }, + { + "filename": "hp_up", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 70, + "y": 278, + "w": 16, + "h": 24 + } + }, + { + "filename": "iron", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 70, + "y": 302, + "w": 16, + "h": 24 + } + }, + { + "filename": "max_lure", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 17, + "h": 24 + }, + "frame": { + "x": 70, + "y": 326, + "w": 17, + "h": 24 + } + }, + { + "filename": "max_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 4, + "w": 18, + "h": 24 + }, + "frame": { + "x": 70, + "y": 350, + "w": 18, + "h": 24 + } + }, + { + "filename": "max_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 22, + "h": 24 + }, + "frame": { + "x": 70, + "y": 374, + "w": 22, + "h": 24 + } + }, + { + "filename": "muscle_band", "rotated": false, "trimmed": true, "sourceSize": { @@ -1804,15 +1762,183 @@ }, "spriteSourceSize": { "x": 4, - "y": 6, - "w": 23, - "h": 21 + "y": 4, + "w": 24, + "h": 24 }, "frame": { - "x": 149, - "y": 60, + "x": 70, + "y": 398, + "w": 24, + "h": 24 + } + }, + { + "filename": "pixie_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 79, + "y": 73, + "w": 24, + "h": 24 + } + }, + { + "filename": "reveal_glass", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, "w": 23, - "h": 21 + "h": 24 + }, + "frame": { + "x": 103, + "y": 73, + "w": 23, + "h": 24 + } + }, + { + "filename": "salac_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 81, + "y": 97, + "w": 24, + "h": 24 + } + }, + { + "filename": "scanner", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 81, + "y": 121, + "w": 24, + "h": 24 + } + }, + { + "filename": "silk_scarf", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 81, + "y": 145, + "w": 24, + "h": 24 + } + }, + { + "filename": "oval_charm", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 21, + "h": 24 + }, + "frame": { + "x": 105, + "y": 97, + "w": 21, + "h": 24 + } + }, + { + "filename": "shiny_charm", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 21, + "h": 24 + }, + "frame": { + "x": 105, + "y": 121, + "w": 21, + "h": 24 + } + }, + { + "filename": "sky_plate", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 4, + "w": 24, + "h": 24 + }, + "frame": { + "x": 105, + "y": 145, + "w": 24, + "h": 24 } }, { @@ -1830,8 +1956,8 @@ "h": 24 }, "frame": { - "x": 167, - "y": 21, + "x": 86, + "y": 169, "w": 24, "h": 24 } @@ -1851,8 +1977,8 @@ "h": 24 }, "frame": { - "x": 191, - "y": 21, + "x": 86, + "y": 193, "w": 24, "h": 24 } @@ -1872,8 +1998,8 @@ "h": 24 }, "frame": { - "x": 215, - "y": 21, + "x": 86, + "y": 217, "w": 24, "h": 24 } @@ -1893,8 +2019,8 @@ "h": 24 }, "frame": { - "x": 239, - "y": 21, + "x": 86, + "y": 241, "w": 24, "h": 24 } @@ -1914,8 +2040,8 @@ "h": 24 }, "frame": { - "x": 263, - "y": 21, + "x": 86, + "y": 265, "w": 24, "h": 24 } @@ -1935,14 +2061,14 @@ "h": 24 }, "frame": { - "x": 287, - "y": 21, + "x": 86, + "y": 289, "w": 24, "h": 24 } }, { - "filename": "silver_powder", + "filename": "red_orb", "rotated": false, "trimmed": true, "sourceSize": { @@ -1950,20 +2076,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 4, - "y": 11, - "w": 24, - "h": 15 + "x": 6, + "y": 4, + "w": 20, + "h": 24 }, "frame": { - "x": 167, - "y": 45, - "w": 24, - "h": 15 + "x": 110, + "y": 169, + "w": 20, + "h": 24 } }, { - "filename": "max_revive", + "filename": "black_belt", "rotated": false, "trimmed": true, "sourceSize": { @@ -1974,13 +2100,34 @@ "x": 5, "y": 4, "w": 22, - "h": 24 + "h": 23 }, "frame": { - "x": 311, - "y": 21, + "x": 110, + "y": 193, "w": 22, - "h": 24 + "h": 23 + } + }, + { + "filename": "bug_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 110, + "y": 216, + "w": 22, + "h": 23 } }, { @@ -1998,8 +2145,8 @@ "h": 23 }, "frame": { - "x": 333, - "y": 20, + "x": 110, + "y": 239, "w": 24, "h": 23 } @@ -2019,8 +2166,8 @@ "h": 23 }, "frame": { - "x": 357, - "y": 20, + "x": 110, + "y": 262, "w": 24, "h": 23 } @@ -2040,281 +2187,8 @@ "h": 23 }, "frame": { - "x": 381, - "y": 20, - "w": 24, - "h": 23 - } - }, - { - "filename": "red_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 20, - "h": 24 - }, - "frame": { - "x": 405, - "y": 24, - "w": 20, - "h": 24 - } - }, - { - "filename": "amulet_coin", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 5, - "w": 23, - "h": 21 - }, - "frame": { - "x": 172, - "y": 60, - "w": 23, - "h": 21 - } - }, - { - "filename": "candy_overlay", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 12, - "w": 16, - "h": 15 - }, - "frame": { - "x": 191, - "y": 45, - "w": 16, - "h": 15 - } - }, - { - "filename": "dragon_scale", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 24, - "h": 18 - }, - "frame": { - "x": 207, - "y": 45, - "w": 24, - "h": 18 - } - }, - { - "filename": "exp_balance", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 - }, - "frame": { - "x": 231, - "y": 45, - "w": 24, - "h": 22 - } - }, - { - "filename": "exp_share", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 - }, - "frame": { - "x": 255, - "y": 45, - "w": 24, - "h": 22 - } - }, - { - "filename": "leppa_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 279, - "y": 45, - "w": 24, - "h": 23 - } - }, - { - "filename": "scope_lens", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 303, - "y": 45, - "w": 24, - "h": 23 - } - }, - { - "filename": "revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 10, - "y": 8, - "w": 12, - "h": 17 - }, - "frame": { - "x": 195, - "y": 60, - "w": 12, - "h": 17 - } - }, - { - "filename": "icy_reins_of_unity", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 - }, - "frame": { - "x": 207, - "y": 63, - "w": 24, - "h": 20 - } - }, - { - "filename": "metal_powder", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 - }, - "frame": { - "x": 231, - "y": 67, - "w": 24, - "h": 20 - } - }, - { - "filename": "peat_block", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 22 - }, - "frame": { - "x": 255, - "y": 67, - "w": 24, - "h": 22 - } - }, - { - "filename": "twisted_spoon", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 24, - "h": 23 - }, - "frame": { - "x": 279, - "y": 68, + "x": 110, + "y": 285, "w": 24, "h": 23 } @@ -2334,77 +2208,14 @@ "h": 23 }, "frame": { - "x": 303, - "y": 68, + "x": 87, + "y": 313, "w": 23, "h": 23 } }, { - "filename": "black_belt", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 327, - "y": 45, - "w": 22, - "h": 23 - } - }, - { - "filename": "griseous_core", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 326, - "y": 68, - "w": 23, - "h": 23 - } - }, - { - "filename": "reveal_glass", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 4, - "w": 23, - "h": 24 - }, - "frame": { - "x": 349, - "y": 43, - "w": 23, - "h": 24 - } - }, - { - "filename": "leek", + "filename": "leppa_berry", "rotated": false, "trimmed": true, "sourceSize": { @@ -2414,144 +2225,18 @@ "spriteSourceSize": { "x": 4, "y": 5, - "w": 23, + "w": 24, "h": 23 }, "frame": { - "x": 372, - "y": 43, - "w": 23, + "x": 110, + "y": 308, + "w": 24, "h": 23 } }, { - "filename": "rare_candy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 349, - "y": 67, - "w": 23, - "h": 23 - } - }, - { - "filename": "rarer_candy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 23 - }, - "frame": { - "x": 372, - "y": 66, - "w": 23, - "h": 23 - } - }, - { - "filename": "bug_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 395, - "y": 48, - "w": 22, - "h": 23 - } - }, - { - "filename": "auspicious_armor", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 21 - }, - "frame": { - "x": 395, - "y": 71, - "w": 23, - "h": 21 - } - }, - { - "filename": "binding_band", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 23, - "h": 20 - }, - "frame": { - "x": 372, - "y": 89, - "w": 23, - "h": 20 - } - }, - { - "filename": "healing_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 23, - "h": 22 - }, - "frame": { - "x": 349, - "y": 90, - "w": 23, - "h": 22 - } - }, - { - "filename": "black_glasses", + "filename": "choice_specs", "rotated": false, "trimmed": true, "sourceSize": { @@ -2561,14 +2246,35 @@ "spriteSourceSize": { "x": 4, "y": 8, - "w": 23, - "h": 17 + "w": 24, + "h": 18 }, "frame": { - "x": 395, - "y": 92, + "x": 135, + "y": 36, + "w": 24, + "h": 18 + } + }, + { + "filename": "coupon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, "w": 23, - "h": 17 + "h": 19 + }, + "frame": { + "x": 125, + "y": 54, + "w": 23, + "h": 19 } }, { @@ -2586,7 +2292,7 @@ "h": 23 }, "frame": { - "x": 73, + "x": 126, "y": 73, "w": 22, "h": 23 @@ -2607,8 +2313,8 @@ "h": 23 }, "frame": { - "x": 95, - "y": 73, + "x": 126, + "y": 96, "w": 22, "h": 23 } @@ -2628,12 +2334,33 @@ "h": 23 }, "frame": { - "x": 73, - "y": 96, + "x": 126, + "y": 119, "w": 22, "h": 23 } }, + { + "filename": "dragon_fang", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 21, + "h": 23 + }, + "frame": { + "x": 129, + "y": 142, + "w": 21, + "h": 23 + } + }, { "filename": "fairy_tera_shard", "rotated": false, @@ -2649,8 +2376,8 @@ "h": 23 }, "frame": { - "x": 95, - "y": 96, + "x": 130, + "y": 165, "w": 22, "h": 23 } @@ -2670,33 +2397,12 @@ "h": 23 }, "frame": { - "x": 117, - "y": 78, + "x": 132, + "y": 188, "w": 22, "h": 23 } }, - { - "filename": "blank_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 117, - "y": 101, - "w": 22, - "h": 22 - } - }, { "filename": "fire_stone", "rotated": false, @@ -2712,8 +2418,8 @@ "h": 23 }, "frame": { - "x": 139, - "y": 81, + "x": 132, + "y": 211, "w": 22, "h": 23 } @@ -2733,180 +2439,12 @@ "h": 23 }, "frame": { - "x": 161, - "y": 81, + "x": 134, + "y": 234, "w": 22, "h": 23 } }, - { - "filename": "quick_powder", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 - }, - "frame": { - "x": 139, - "y": 104, - "w": 24, - "h": 20 - } - }, - { - "filename": "big_nugget", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 163, - "y": 104, - "w": 20, - "h": 20 - } - }, - { - "filename": "max_lure", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 17, - "h": 24 - }, - "frame": { - "x": 183, - "y": 81, - "w": 17, - "h": 24 - } - }, - { - "filename": "rusted_sword", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 22 - }, - "frame": { - "x": 200, - "y": 83, - "w": 23, - "h": 22 - } - }, - { - "filename": "rusted_shield", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 24, - "h": 20 - }, - "frame": { - "x": 183, - "y": 105, - "w": 24, - "h": 20 - } - }, - { - "filename": "apicot_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 - }, - "frame": { - "x": 207, - "y": 105, - "w": 19, - "h": 20 - } - }, - { - "filename": "relic_crown", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 18 - }, - "frame": { - "x": 223, - "y": 87, - "w": 23, - "h": 18 - } - }, - { - "filename": "blue_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 226, - "y": 105, - "w": 20, - "h": 20 - } - }, { "filename": "flying_tera_shard", "rotated": false, @@ -2922,33 +2460,12 @@ "h": 23 }, "frame": { - "x": 246, - "y": 89, + "x": 134, + "y": 257, "w": 22, "h": 23 } }, - { - "filename": "blunder_policy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 19 - }, - "frame": { - "x": 246, - "y": 112, - "w": 22, - "h": 19 - } - }, { "filename": "focus_sash", "rotated": false, @@ -2964,8 +2481,8 @@ "h": 23 }, "frame": { - "x": 268, - "y": 91, + "x": 134, + "y": 280, "w": 22, "h": 23 } @@ -2985,159 +2502,12 @@ "h": 23 }, "frame": { - "x": 290, - "y": 91, + "x": 134, + "y": 303, "w": 22, "h": 23 } }, - { - "filename": "grass_tera_shard", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 312, - "y": 91, - "w": 22, - "h": 23 - } - }, - { - "filename": "full_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 4, - "w": 15, - "h": 23 - }, - "frame": { - "x": 334, - "y": 91, - "w": 15, - "h": 23 - } - }, - { - "filename": "sacred_ash", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 - }, - "frame": { - "x": 268, - "y": 114, - "w": 24, - "h": 20 - } - }, - { - "filename": "shadow_reins_of_unity", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 - }, - "frame": { - "x": 292, - "y": 114, - "w": 24, - "h": 20 - } - }, - { - "filename": "soft_sand", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 24, - "h": 20 - }, - "frame": { - "x": 316, - "y": 114, - "w": 24, - "h": 20 - } - }, - { - "filename": "eviolite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 15, - "h": 15 - }, - "frame": { - "x": 73, - "y": 119, - "w": 15, - "h": 15 - } - }, - { - "filename": "max_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 4, - "w": 18, - "h": 24 - }, - "frame": { - "x": 76, - "y": 134, - "w": 18, - "h": 24 - } - }, { "filename": "max_repel", "rotated": false, @@ -3153,33 +2523,12 @@ "h": 24 }, "frame": { - "x": 79, - "y": 158, + "x": 148, + "y": 54, "w": 16, "h": 24 } }, - { - "filename": "oval_charm", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 21, - "h": 24 - }, - "frame": { - "x": 86, - "y": 182, - "w": 21, - "h": 24 - } - }, { "filename": "pp_max", "rotated": false, @@ -3195,8 +2544,8 @@ "h": 24 }, "frame": { - "x": 92, - "y": 206, + "x": 148, + "y": 78, "w": 16, "h": 24 } @@ -3216,14 +2565,14 @@ "h": 24 }, "frame": { - "x": 92, - "y": 230, + "x": 148, + "y": 102, "w": 16, "h": 24 } }, { - "filename": "protein", + "filename": "aerodactylite", "rotated": false, "trimmed": true, "sourceSize": { @@ -3232,19 +2581,19 @@ }, "spriteSourceSize": { "x": 8, - "y": 4, + "y": 8, "w": 16, - "h": 24 + "h": 16 }, "frame": { - "x": 93, - "y": 254, + "x": 148, + "y": 126, "w": 16, - "h": 24 + "h": 16 } }, { - "filename": "repel", + "filename": "full_heal", "rotated": false, "trimmed": true, "sourceSize": { @@ -3252,20 +2601,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, + "x": 9, "y": 4, - "w": 16, - "h": 24 + "w": 15, + "h": 23 }, "frame": { - "x": 94, - "y": 278, - "w": 16, - "h": 24 + "x": 150, + "y": 142, + "w": 15, + "h": 23 } }, { - "filename": "shiny_charm", + "filename": "grass_tera_shard", "rotated": false, "trimmed": true, "sourceSize": { @@ -3275,18 +2624,18 @@ "spriteSourceSize": { "x": 6, "y": 4, - "w": 21, - "h": 24 + "w": 22, + "h": 23 }, "frame": { - "x": 94, - "y": 302, - "w": 21, - "h": 24 + "x": 152, + "y": 165, + "w": 22, + "h": 23 } }, { - "filename": "dragon_fang", + "filename": "griseous_core", "rotated": false, "trimmed": true, "sourceSize": { @@ -3296,13 +2645,34 @@ "spriteSourceSize": { "x": 5, "y": 5, - "w": 21, + "w": 23, "h": 23 }, "frame": { - "x": 94, - "y": 326, - "w": 21, + "x": 154, + "y": 188, + "w": 23, + "h": 23 + } + }, + { + "filename": "leek", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 154, + "y": 211, + "w": 23, "h": 23 } }, @@ -3321,8 +2691,8 @@ "h": 23 }, "frame": { - "x": 94, - "y": 349, + "x": 156, + "y": 234, "w": 22, "h": 23 } @@ -3342,14 +2712,14 @@ "h": 23 }, "frame": { - "x": 94, - "y": 372, + "x": 156, + "y": 257, "w": 22, "h": 23 } }, { - "filename": "prism_scale", + "filename": "macho_brace", "rotated": false, "trimmed": true, "sourceSize": { @@ -3357,83 +2727,20 @@ "h": 32 }, "spriteSourceSize": { - "x": 9, - "y": 8, - "w": 15, - "h": 15 - }, - "frame": { - "x": 88, - "y": 119, - "w": 15, - "h": 15 - } - }, - { - "filename": "super_lure", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 17, - "h": 24 - }, - "frame": { - "x": 94, - "y": 134, - "w": 17, - "h": 24 - } - }, - { - "filename": "super_repel", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 95, - "y": 158, - "w": 16, - "h": 24 - } - }, - { - "filename": "berry_pot", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, + "x": 4, "y": 5, - "w": 18, - "h": 22 + "w": 23, + "h": 23 }, "frame": { - "x": 340, - "y": 114, - "w": 18, - "h": 22 + "x": 156, + "y": 280, + "w": 23, + "h": 23 } }, { - "filename": "unknown", + "filename": "rare_candy", "rotated": false, "trimmed": true, "sourceSize": { @@ -3441,16 +2748,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 + "x": 4, + "y": 5, + "w": 23, + "h": 23 }, "frame": { - "x": 358, - "y": 112, - "w": 16, - "h": 24 + "x": 156, + "y": 303, + "w": 23, + "h": 23 } }, { @@ -3468,14 +2775,14 @@ "h": 23 }, "frame": { - "x": 374, - "y": 109, + "x": 88, + "y": 336, "w": 22, "h": 23 } }, { - "filename": "normal_tera_shard", + "filename": "scope_lens", "rotated": false, "trimmed": true, "sourceSize": { @@ -3483,57 +2790,36 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 - }, - "frame": { - "x": 396, - "y": 109, - "w": 22, - "h": 23 - } - }, - { - "filename": "zinc", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 4, - "w": 16, - "h": 24 - }, - "frame": { - "x": 107, - "y": 182, - "w": 16, - "h": 24 - } - }, - { - "filename": "hyper_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, + "x": 4, "y": 5, - "w": 17, + "w": 24, "h": 23 }, "frame": { - "x": 108, - "y": 206, - "w": 17, + "x": 110, + "y": 331, + "w": 24, + "h": 23 + } + }, + { + "filename": "twisted_spoon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 23 + }, + "frame": { + "x": 134, + "y": 326, + "w": 24, "h": 23 } }, @@ -3552,12 +2838,33 @@ "h": 23 }, "frame": { - "x": 108, - "y": 229, + "x": 158, + "y": 326, "w": 21, "h": 23 } }, + { + "filename": "silver_powder", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 11, + "w": 24, + "h": 15 + }, + "frame": { + "x": 88, + "y": 359, + "w": 24, + "h": 15 + } + }, { "filename": "leaf_stone", "rotated": false, @@ -3573,8 +2880,8 @@ "h": 23 }, "frame": { - "x": 109, - "y": 252, + "x": 92, + "y": 374, "w": 21, "h": 23 } @@ -3594,12 +2901,54 @@ "h": 23 }, "frame": { - "x": 110, - "y": 275, + "x": 94, + "y": 397, "w": 20, "h": 23 } }, + { + "filename": "binding_band", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 23, + "h": 20 + }, + "frame": { + "x": 112, + "y": 354, + "w": 23, + "h": 20 + } + }, + { + "filename": "normal_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 113, + "y": 374, + "w": 22, + "h": 23 + } + }, { "filename": "petaya_berry", "rotated": false, @@ -3615,12 +2964,54 @@ "h": 23 }, "frame": { - "x": 115, - "y": 298, + "x": 114, + "y": 397, "w": 22, "h": 23 } }, + { + "filename": "exp_share", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 22 + }, + "frame": { + "x": 135, + "y": 349, + "w": 24, + "h": 22 + } + }, + { + "filename": "peat_block", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 24, + "h": 22 + }, + "frame": { + "x": 135, + "y": 371, + "w": 24, + "h": 22 + } + }, { "filename": "poison_tera_shard", "rotated": false, @@ -3636,12 +3027,96 @@ "h": 23 }, "frame": { - "x": 115, - "y": 321, + "x": 159, + "y": 349, "w": 22, "h": 23 } }, + { + "filename": "adamant_crystal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 23, + "h": 21 + }, + "frame": { + "x": 159, + "y": 372, + "w": 23, + "h": 21 + } + }, + { + "filename": "rarer_candy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 23 + }, + "frame": { + "x": 136, + "y": 393, + "w": 23, + "h": 23 + } + }, + { + "filename": "healing_charm", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 23, + "h": 22 + }, + "frame": { + "x": 159, + "y": 393, + "w": 23, + "h": 22 + } + }, + { + "filename": "protein", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 159, + "y": 22, + "w": 16, + "h": 24 + } + }, { "filename": "psychic_tera_shard", "rotated": false, @@ -3657,8 +3132,8 @@ "h": 23 }, "frame": { - "x": 116, - "y": 344, + "x": 175, + "y": 21, "w": 22, "h": 23 } @@ -3678,8 +3153,8 @@ "h": 23 }, "frame": { - "x": 116, - "y": 367, + "x": 197, + "y": 21, "w": 22, "h": 23 } @@ -3699,14 +3174,35 @@ "h": 23 }, "frame": { - "x": 111, - "y": 123, + "x": 219, + "y": 21, "w": 22, "h": 23 } }, { - "filename": "steel_tera_shard", + "filename": "rusted_sword", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 22 + }, + "frame": { + "x": 241, + "y": 21, + "w": 23, + "h": 22 + } + }, + { + "filename": "amulet_coin", "rotated": false, "trimmed": true, "sourceSize": { @@ -3715,19 +3211,19 @@ }, "spriteSourceSize": { "x": 6, - "y": 4, - "w": 22, - "h": 23 + "y": 5, + "w": 23, + "h": 21 }, "frame": { - "x": 111, - "y": 146, - "w": 22, - "h": 23 + "x": 264, + "y": 21, + "w": 23, + "h": 21 } }, { - "filename": "stellar_tera_shard", + "filename": "auspicious_armor", "rotated": false, "trimmed": true, "sourceSize": { @@ -3735,20 +3231,41 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, - "w": 22, - "h": 23 + "x": 4, + "y": 5, + "w": 23, + "h": 21 }, "frame": { - "x": 133, - "y": 124, - "w": 22, - "h": 23 + "x": 287, + "y": 21, + "w": 23, + "h": 21 } }, { - "filename": "water_tera_shard", + "filename": "berry_juice", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 24, + "h": 23 + }, + "spriteSourceSize": { + "x": 1, + "y": 1, + "w": 22, + "h": 21 + }, + "frame": { + "x": 310, + "y": 21, + "w": 22, + "h": 21 + } + }, + { + "filename": "blank_memory", "rotated": false, "trimmed": true, "sourceSize": { @@ -3756,16 +3273,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 4, + "x": 5, + "y": 5, "w": 22, - "h": 23 + "h": 22 }, "frame": { - "x": 155, - "y": 124, + "x": 332, + "y": 20, "w": 22, - "h": 23 + "h": 22 } }, { @@ -3783,8 +3300,8 @@ "h": 22 }, "frame": { - "x": 133, - "y": 147, + "x": 354, + "y": 20, "w": 22, "h": 22 } @@ -3804,12 +3321,159 @@ "h": 22 }, "frame": { - "x": 155, - "y": 147, + "x": 376, + "y": 20, "w": 22, "h": 22 } }, + { + "filename": "dragon_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 24, + "h": 18 + }, + "frame": { + "x": 398, + "y": 24, + "w": 24, + "h": 18 + } + }, + { + "filename": "repel", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 164, + "y": 46, + "w": 16, + "h": 24 + } + }, + { + "filename": "steel_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 180, + "y": 44, + "w": 22, + "h": 23 + } + }, + { + "filename": "stellar_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 202, + "y": 44, + "w": 22, + "h": 23 + } + }, + { + "filename": "hyper_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 23 + }, + "frame": { + "x": 224, + "y": 44, + "w": 17, + "h": 23 + } + }, + { + "filename": "water_tera_shard", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 4, + "w": 22, + "h": 23 + }, + "frame": { + "x": 241, + "y": 43, + "w": 22, + "h": 23 + } + }, + { + "filename": "super_lure", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 17, + "h": 24 + }, + "frame": { + "x": 164, + "y": 70, + "w": 17, + "h": 24 + } + }, { "filename": "wide_lens", "rotated": false, @@ -3825,8 +3489,8 @@ "h": 23 }, "frame": { - "x": 177, - "y": 125, + "x": 181, + "y": 67, "w": 22, "h": 23 } @@ -3846,1247 +3510,8 @@ "h": 22 }, "frame": { - "x": 199, - "y": 125, - "w": 22, - "h": 22 - } - }, - { - "filename": "dire_hit", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 221, - "y": 125, - "w": 22, - "h": 22 - } - }, - { - "filename": "deep_sea_tooth", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 177, - "y": 148, - "w": 22, - "h": 21 - } - }, - { - "filename": "dna_splicers", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 199, - "y": 147, - "w": 22, - "h": 22 - } - }, - { - "filename": "dragon_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 221, - "y": 147, - "w": 22, - "h": 22 - } - }, - { - "filename": "electirizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 243, - "y": 131, - "w": 22, - "h": 22 - } - }, - { - "filename": "moon_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 - }, - "frame": { - "x": 265, - "y": 134, - "w": 23, - "h": 21 - } - }, - { - "filename": "n_lunarizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 - }, - "frame": { - "x": 288, - "y": 134, - "w": 23, - "h": 21 - } - }, - { - "filename": "n_solarizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 6, - "w": 23, - "h": 21 - }, - "frame": { - "x": 311, - "y": 134, - "w": 23, - "h": 21 - } - }, - { - "filename": "deep_sea_scale", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 - }, - "frame": { - "x": 243, - "y": 153, - "w": 22, - "h": 20 - } - }, - { - "filename": "mystic_ticket", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 - }, - "frame": { - "x": 265, - "y": 155, - "w": 23, - "h": 19 - } - }, - { - "filename": "pair_of_tickets", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 7, - "w": 23, - "h": 19 - }, - "frame": { - "x": 288, - "y": 155, - "w": 23, - "h": 19 - } - }, - { - "filename": "reviver_seed", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 8, - "w": 23, - "h": 20 - }, - "frame": { - "x": 311, - "y": 155, - "w": 23, - "h": 20 - } - }, - { - "filename": "electric_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 334, - "y": 136, - "w": 22, - "h": 22 - } - }, - { - "filename": "enigma_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 356, - "y": 136, - "w": 22, - "h": 22 - } - }, - { - "filename": "burn_drive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 334, - "y": 158, - "w": 23, - "h": 17 - } - }, - { - "filename": "fairy_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 378, - "y": 132, - "w": 22, - "h": 22 - } - }, - { - "filename": "fighting_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 400, - "y": 132, - "w": 22, - "h": 22 - } - }, - { - "filename": "chill_drive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 357, - "y": 158, - "w": 23, - "h": 17 - } - }, - { - "filename": "wellspring_mask", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 5, - "w": 23, - "h": 21 - }, - "frame": { - "x": 380, - "y": 154, - "w": 23, - "h": 21 - } - }, - { - "filename": "fire_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 403, - "y": 154, - "w": 22, - "h": 22 - } - }, - { - "filename": "flying_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 123, - "y": 169, - "w": 22, - "h": 22 - } - }, - { - "filename": "ganlon_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 145, - "y": 169, - "w": 22, - "h": 22 - } - }, - { - "filename": "ghost_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 167, - "y": 169, - "w": 22, - "h": 22 - } - }, - { - "filename": "grass_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 189, - "y": 169, - "w": 22, - "h": 22 - } - }, - { - "filename": "ground_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 211, - "y": 169, - "w": 22, - "h": 22 - } - }, - { - "filename": "shell_bell", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 23, - "h": 20 - }, - "frame": { - "x": 233, - "y": 173, - "w": 23, - "h": 20 - } - }, - { - "filename": "dubious_disc", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 19 - }, - "frame": { - "x": 256, - "y": 174, - "w": 22, - "h": 19 - } - }, - { - "filename": "fairy_feather", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 20 - }, - "frame": { - "x": 278, - "y": 174, - "w": 22, - "h": 20 - } - }, - { - "filename": "guard_spec", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 300, - "y": 175, - "w": 22, - "h": 22 - } - }, - { - "filename": "ice_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 322, - "y": 175, - "w": 22, - "h": 22 - } - }, - { - "filename": "ice_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 344, - "y": 175, - "w": 22, - "h": 22 - } - }, - { - "filename": "magmarizer", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 366, - "y": 175, - "w": 22, - "h": 22 - } - }, - { - "filename": "leftovers", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 15, - "h": 22 - }, - "frame": { - "x": 388, - "y": 175, - "w": 15, - "h": 22 - } - }, - { - "filename": "mini_black_hole", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 403, - "y": 176, - "w": 22, - "h": 22 - } - }, - { - "filename": "poison_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 125, - "y": 191, - "w": 22, - "h": 22 - } - }, - { - "filename": "protector", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 147, - "y": 191, - "w": 22, - "h": 22 - } - }, - { - "filename": "psychic_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 169, - "y": 191, - "w": 22, - "h": 22 - } - }, - { - "filename": "rock_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 191, - "y": 191, - "w": 22, - "h": 22 - } - }, - { - "filename": "hard_meteorite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 20, - "h": 22 - }, - "frame": { - "x": 213, - "y": 191, - "w": 20, - "h": 22 - } - }, - { - "filename": "liechi_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 233, - "y": 193, - "w": 22, - "h": 21 - } - }, - { - "filename": "scroll_of_darkness", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 255, - "y": 193, - "w": 22, - "h": 22 - } - }, - { - "filename": "douse_drive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 277, - "y": 194, - "w": 23, - "h": 17 - } - }, - { - "filename": "relic_band", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 9, - "w": 17, - "h": 16 - }, - "frame": { - "x": 125, - "y": 213, - "w": 17, - "h": 16 - } - }, - { - "filename": "shock_drive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 142, - "y": 213, - "w": 23, - "h": 17 - } - }, - { - "filename": "wise_glasses", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 4, - "y": 8, - "w": 23, - "h": 17 - }, - "frame": { - "x": 165, - "y": 213, - "w": 23, - "h": 17 - } - }, - { - "filename": "malicious_armor", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 - }, - "frame": { - "x": 188, - "y": 213, - "w": 22, - "h": 20 - } - }, - { - "filename": "scroll_of_waters", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 210, - "y": 213, - "w": 22, - "h": 22 - } - }, - { - "filename": "shed_shell", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 232, - "y": 214, - "w": 22, - "h": 22 - } - }, - { - "filename": "starf_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 254, - "y": 215, - "w": 22, - "h": 22 - } - }, - { - "filename": "steel_memory", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 300, - "y": 197, - "w": 22, - "h": 22 - } - }, - { - "filename": "thick_club", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 322, - "y": 197, - "w": 22, - "h": 22 - } - }, - { - "filename": "thunder_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 344, - "y": 197, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_bug", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 366, - "y": 197, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_dark", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 129, - "y": 230, - "w": 22, - "h": 22 - } - }, - { - "filename": "sharp_beak", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 21, - "h": 23 - }, - "frame": { - "x": 130, - "y": 252, - "w": 21, - "h": 23 - } - }, - { - "filename": "whipped_dream", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 4, - "w": 21, - "h": 23 - }, - "frame": { - "x": 130, - "y": 275, - "w": 21, - "h": 23 - } - }, - { - "filename": "tm_dragon", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 151, - "y": 230, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_electric", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 151, - "y": 252, - "w": 22, - "h": 22 - } - }, - { - "filename": "tm_fairy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 5, - "w": 22, - "h": 22 - }, - "frame": { - "x": 151, - "y": 274, + "x": 203, + "y": 67, "w": 22, "h": 22 } @@ -5106,12 +3531,54 @@ "h": 23 }, "frame": { - "x": 137, - "y": 298, + "x": 164, + "y": 94, "w": 17, "h": 23 } }, + { + "filename": "dire_hit", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 181, + "y": 90, + "w": 22, + "h": 22 + } + }, + { + "filename": "dna_splicers", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 203, + "y": 89, + "w": 22, + "h": 22 + } + }, { "filename": "sachet", "rotated": false, @@ -5127,12 +3594,138 @@ "h": 23 }, "frame": { - "x": 137, - "y": 321, + "x": 164, + "y": 117, "w": 18, "h": 23 } }, + { + "filename": "super_repel", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 225, + "y": 67, + "w": 16, + "h": 24 + } + }, + { + "filename": "dragon_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 241, + "y": 66, + "w": 22, + "h": 22 + } + }, + { + "filename": "sharp_beak", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 21, + "h": 23 + }, + "frame": { + "x": 182, + "y": 112, + "w": 21, + "h": 23 + } + }, + { + "filename": "electirizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 203, + "y": 111, + "w": 22, + "h": 22 + } + }, + { + "filename": "unknown", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 225, + "y": 91, + "w": 16, + "h": 24 + } + }, + { + "filename": "electric_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 241, + "y": 88, + "w": 22, + "h": 22 + } + }, { "filename": "super_potion", "rotated": false, @@ -5148,12 +3741,1188 @@ "h": 23 }, "frame": { - "x": 138, - "y": 344, + "x": 165, + "y": 140, "w": 17, "h": 23 } }, + { + "filename": "whipped_dream", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 4, + "w": 21, + "h": 23 + }, + "frame": { + "x": 182, + "y": 135, + "w": 21, + "h": 23 + } + }, + { + "filename": "enigma_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 203, + "y": 133, + "w": 22, + "h": 22 + } + }, + { + "filename": "zinc", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 4, + "w": 16, + "h": 24 + }, + "frame": { + "x": 225, + "y": 115, + "w": 16, + "h": 24 + } + }, + { + "filename": "fairy_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 241, + "y": 110, + "w": 22, + "h": 22 + } + }, + { + "filename": "aggronite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 225, + "y": 139, + "w": 16, + "h": 16 + } + }, + { + "filename": "fighting_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 241, + "y": 132, + "w": 22, + "h": 22 + } + }, + { + "filename": "berry_pot", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 18, + "h": 22 + }, + "frame": { + "x": 174, + "y": 163, + "w": 18, + "h": 22 + } + }, + { + "filename": "fire_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 177, + "y": 185, + "w": 22, + "h": 22 + } + }, + { + "filename": "flying_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 177, + "y": 207, + "w": 22, + "h": 22 + } + }, + { + "filename": "ganlon_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 178, + "y": 229, + "w": 22, + "h": 22 + } + }, + { + "filename": "ghost_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 178, + "y": 251, + "w": 22, + "h": 22 + } + }, + { + "filename": "grass_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 273, + "w": 22, + "h": 22 + } + }, + { + "filename": "ground_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 295, + "w": 22, + "h": 22 + } + }, + { + "filename": "guard_spec", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 179, + "y": 317, + "w": 22, + "h": 22 + } + }, + { + "filename": "hard_meteorite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 20, + "h": 22 + }, + "frame": { + "x": 181, + "y": 339, + "w": 20, + "h": 22 + } + }, + { + "filename": "ice_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 182, + "y": 361, + "w": 22, + "h": 22 + } + }, + { + "filename": "ice_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 182, + "y": 383, + "w": 22, + "h": 22 + } + }, + { + "filename": "black_glasses", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 182, + "y": 405, + "w": 23, + "h": 17 + } + }, + { + "filename": "leftovers", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 15, + "h": 22 + }, + "frame": { + "x": 192, + "y": 158, + "w": 15, + "h": 22 + } + }, + { + "filename": "icy_reins_of_unity", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 24, + "h": 20 + }, + "frame": { + "x": 207, + "y": 155, + "w": 24, + "h": 20 + } + }, + { + "filename": "apicot_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 19, + "h": 20 + }, + "frame": { + "x": 231, + "y": 155, + "w": 19, + "h": 20 + } + }, + { + "filename": "dawn_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 21 + }, + "frame": { + "x": 250, + "y": 154, + "w": 20, + "h": 21 + } + }, + { + "filename": "metal_powder", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 24, + "h": 20 + }, + "frame": { + "x": 207, + "y": 175, + "w": 24, + "h": 20 + } + }, + { + "filename": "quick_powder", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 24, + "h": 20 + }, + "frame": { + "x": 231, + "y": 175, + "w": 24, + "h": 20 + } + }, + { + "filename": "magmarizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 199, + "y": 195, + "w": 22, + "h": 22 + } + }, + { + "filename": "mini_black_hole", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 221, + "y": 195, + "w": 22, + "h": 22 + } + }, + { + "filename": "big_nugget", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 255, + "y": 175, + "w": 20, + "h": 20 + } + }, + { + "filename": "moon_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 23, + "h": 21 + }, + "frame": { + "x": 243, + "y": 195, + "w": 23, + "h": 21 + } + }, + { + "filename": "n_lunarizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 23, + "h": 21 + }, + "frame": { + "x": 200, + "y": 217, + "w": 23, + "h": 21 + } + }, + { + "filename": "n_solarizer", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 23, + "h": 21 + }, + "frame": { + "x": 200, + "y": 238, + "w": 23, + "h": 21 + } + }, + { + "filename": "poison_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 223, + "y": 217, + "w": 22, + "h": 22 + } + }, + { + "filename": "deep_sea_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 20 + }, + "frame": { + "x": 223, + "y": 239, + "w": 22, + "h": 20 + } + }, + { + "filename": "protector", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 245, + "y": 216, + "w": 22, + "h": 22 + } + }, + { + "filename": "deep_sea_tooth", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 245, + "y": 238, + "w": 22, + "h": 21 + } + }, + { + "filename": "dusk_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 266, + "y": 195, + "w": 21, + "h": 21 + } + }, + { + "filename": "psychic_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 267, + "y": 216, + "w": 22, + "h": 22 + } + }, + { + "filename": "liechi_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 267, + "y": 238, + "w": 22, + "h": 21 + } + }, + { + "filename": "rock_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 201, + "y": 259, + "w": 22, + "h": 22 + } + }, + { + "filename": "rusted_shield", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 6, + "w": 24, + "h": 20 + }, + "frame": { + "x": 223, + "y": 259, + "w": 24, + "h": 20 + } + }, + { + "filename": "sacred_ash", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 24, + "h": 20 + }, + "frame": { + "x": 247, + "y": 259, + "w": 24, + "h": 20 + } + }, + { + "filename": "scroll_of_darkness", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 201, + "y": 281, + "w": 22, + "h": 22 + } + }, + { + "filename": "scroll_of_waters", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 223, + "y": 279, + "w": 22, + "h": 22 + } + }, + { + "filename": "shadow_reins_of_unity", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 24, + "h": 20 + }, + "frame": { + "x": 245, + "y": 279, + "w": 24, + "h": 20 + } + }, + { + "filename": "shed_shell", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 201, + "y": 303, + "w": 22, + "h": 22 + } + }, + { + "filename": "starf_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 223, + "y": 301, + "w": 22, + "h": 22 + } + }, + { + "filename": "soft_sand", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 24, + "h": 20 + }, + "frame": { + "x": 245, + "y": 299, + "w": 24, + "h": 20 + } + }, + { + "filename": "steel_memory", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 201, + "y": 325, + "w": 22, + "h": 22 + } + }, + { + "filename": "thick_club", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 223, + "y": 323, + "w": 22, + "h": 22 + } + }, + { + "filename": "thunder_stone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 245, + "y": 319, + "w": 22, + "h": 22 + } + }, + { + "filename": "blue_orb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 271, + "y": 259, + "w": 20, + "h": 20 + } + }, + { + "filename": "tm_bug", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 269, + "y": 279, + "w": 22, + "h": 22 + } + }, + { + "filename": "black_sludge", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 24, + "h": 24 + }, + "spriteSourceSize": { + "x": 1, + "y": 2, + "w": 22, + "h": 19 + }, + "frame": { + "x": 269, + "y": 301, + "w": 22, + "h": 19 + } + }, + { + "filename": "wellspring_mask", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 5, + "w": 23, + "h": 21 + }, + "frame": { + "x": 267, + "y": 320, + "w": 23, + "h": 21 + } + }, + { + "filename": "burn_drive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 245, + "y": 341, + "w": 23, + "h": 17 + } + }, + { + "filename": "blunder_policy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 19 + }, + "frame": { + "x": 268, + "y": 341, + "w": 22, + "h": 19 + } + }, + { + "filename": "dubious_disc", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 19 + }, + "frame": { + "x": 223, + "y": 345, + "w": 22, + "h": 19 + } + }, { "filename": "lock_capsule", "rotated": false, @@ -5169,8 +4938,8 @@ "h": 22 }, "frame": { - "x": 154, - "y": 296, + "x": 204, + "y": 347, "w": 19, "h": 22 } @@ -5190,14 +4959,203 @@ "h": 22 }, "frame": { - "x": 138, - "y": 367, + "x": 204, + "y": 369, "w": 19, "h": 22 } }, { - "filename": "sitrus_berry", + "filename": "tm_dark", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 223, + "y": 364, + "w": 22, + "h": 22 + } + }, + { + "filename": "reviver_seed", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 8, + "w": 23, + "h": 20 + }, + "frame": { + "x": 245, + "y": 358, + "w": 23, + "h": 20 + } + }, + { + "filename": "fairy_feather", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 20 + }, + "frame": { + "x": 268, + "y": 360, + "w": 22, + "h": 20 + } + }, + { + "filename": "chill_drive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 245, + "y": 378, + "w": 23, + "h": 17 + } + }, + { + "filename": "douse_drive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 268, + "y": 380, + "w": 23, + "h": 17 + } + }, + { + "filename": "malicious_armor", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 20 + }, + "frame": { + "x": 223, + "y": 386, + "w": 22, + "h": 20 + } + }, + { + "filename": "tm_dragon", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 205, + "y": 406, + "w": 22, + "h": 22 + } + }, + { + "filename": "tm_electric", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 5, + "w": 22, + "h": 22 + }, + "frame": { + "x": 227, + "y": 406, + "w": 22, + "h": 22 + } + }, + { + "filename": "candy_overlay", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 12, + "w": 16, + "h": 15 + }, + "frame": { + "x": 205, + "y": 391, + "w": 16, + "h": 15 + } + }, + { + "filename": "quick_claw", "rotated": false, "trimmed": true, "sourceSize": { @@ -5206,14 +5164,98 @@ }, "spriteSourceSize": { "x": 6, + "y": 6, + "w": 19, + "h": 21 + }, + "frame": { + "x": 249, + "y": 395, + "w": 19, + "h": 21 + } + }, + { + "filename": "golden_mystic_ticket", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 268, + "y": 397, + "w": 23, + "h": 19 + } + }, + { + "filename": "relic_gold", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 11, + "w": 15, + "h": 11 + }, + "frame": { + "x": 249, + "y": 416, + "w": 15, + "h": 11 + } + }, + { + "filename": "mystic_ticket", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 19 + }, + "frame": { + "x": 264, + "y": 42, + "w": 23, + "h": 19 + } + }, + { + "filename": "tm_fairy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, "y": 5, - "w": 20, + "w": 22, "h": 22 }, "frame": { - "x": 155, - "y": 318, - "w": 20, + "x": 263, + "y": 61, + "w": 22, "h": 22 } }, @@ -5232,8 +5274,8 @@ "h": 22 }, "frame": { - "x": 155, - "y": 340, + "x": 263, + "y": 83, "w": 22, "h": 22 } @@ -5253,75 +5295,12 @@ "h": 22 }, "frame": { - "x": 157, - "y": 362, + "x": 263, + "y": 105, "w": 22, "h": 22 } }, - { - "filename": "relic_gold", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 9, - "y": 11, - "w": 15, - "h": 11 - }, - "frame": { - "x": 173, - "y": 230, - "w": 15, - "h": 11 - } - }, - { - "filename": "metronome", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 5, - "w": 17, - "h": 22 - }, - "frame": { - "x": 173, - "y": 241, - "w": 17, - "h": 22 - } - }, - { - "filename": "soothe_bell", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 5, - "w": 17, - "h": 22 - }, - "frame": { - "x": 173, - "y": 263, - "w": 17, - "h": 22 - } - }, { "filename": "tm_flying", "rotated": false, @@ -5337,14 +5316,14 @@ "h": 22 }, "frame": { - "x": 173, - "y": 285, + "x": 263, + "y": 127, "w": 22, "h": 22 } }, { - "filename": "dawn_stone", + "filename": "pair_of_tickets", "rotated": false, "trimmed": true, "sourceSize": { @@ -5352,121 +5331,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 21 + "x": 4, + "y": 7, + "w": 23, + "h": 19 }, "frame": { - "x": 190, - "y": 233, - "w": 20, - "h": 21 - } - }, - { - "filename": "sweet_apple", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 210, - "y": 235, - "w": 22, - "h": 21 - } - }, - { - "filename": "syrupy_apple", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 232, - "y": 236, - "w": 22, - "h": 21 - } - }, - { - "filename": "gb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 190, - "y": 254, - "w": 20, - "h": 20 - } - }, - { - "filename": "tart_apple", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 21 - }, - "frame": { - "x": 254, - "y": 237, - "w": 22, - "h": 21 - } - }, - { - "filename": "tera_orb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 22, - "h": 20 - }, - "frame": { - "x": 210, - "y": 256, - "w": 22, - "h": 20 + "x": 287, + "y": 42, + "w": 23, + "h": 19 } }, { @@ -5484,8 +5358,8 @@ "h": 22 }, "frame": { - "x": 232, - "y": 257, + "x": 285, + "y": 61, "w": 22, "h": 22 } @@ -5505,8 +5379,8 @@ "h": 22 }, "frame": { - "x": 254, - "y": 258, + "x": 285, + "y": 83, "w": 22, "h": 22 } @@ -5526,8 +5400,8 @@ "h": 22 }, "frame": { - "x": 175, - "y": 307, + "x": 285, + "y": 105, "w": 22, "h": 22 } @@ -5547,12 +5421,96 @@ "h": 22 }, "frame": { - "x": 177, - "y": 329, + "x": 285, + "y": 127, "w": 22, "h": 22 } }, + { + "filename": "shell_bell", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 23, + "h": 20 + }, + "frame": { + "x": 310, + "y": 42, + "w": 23, + "h": 20 + } + }, + { + "filename": "sweet_apple", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 333, + "y": 42, + "w": 22, + "h": 21 + } + }, + { + "filename": "syrupy_apple", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 355, + "y": 42, + "w": 22, + "h": 21 + } + }, + { + "filename": "tart_apple", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 21 + }, + "frame": { + "x": 377, + "y": 42, + "w": 22, + "h": 21 + } + }, { "filename": "tm_normal", "rotated": false, @@ -5568,8 +5526,8 @@ "h": 22 }, "frame": { - "x": 179, - "y": 351, + "x": 399, + "y": 42, "w": 22, "h": 22 } @@ -5589,8 +5547,8 @@ "h": 22 }, "frame": { - "x": 179, - "y": 373, + "x": 307, + "y": 62, "w": 22, "h": 22 } @@ -5610,138 +5568,12 @@ "h": 22 }, "frame": { - "x": 157, - "y": 384, + "x": 307, + "y": 84, "w": 22, "h": 22 } }, - { - "filename": "upgrade", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 22, - "h": 19 - }, - "frame": { - "x": 109, - "y": 406, - "w": 22, - "h": 19 - } - }, - { - "filename": "metal_alloy", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 21, - "h": 19 - }, - "frame": { - "x": 131, - "y": 406, - "w": 21, - "h": 19 - } - }, - { - "filename": "lum_berry", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 152, - "y": 406, - "w": 20, - "h": 19 - } - }, - { - "filename": "power_herb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 172, - "y": 406, - "w": 20, - "h": 19 - } - }, - { - "filename": "absolite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 116, - "y": 390, - "w": 16, - "h": 16 - } - }, - { - "filename": "aerodactylite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 132, - "y": 390, - "w": 16, - "h": 16 - } - }, { "filename": "tm_rock", "rotated": false, @@ -5757,8 +5589,8 @@ "h": 22 }, "frame": { - "x": 388, - "y": 198, + "x": 307, + "y": 106, "w": 22, "h": 22 } @@ -5778,8 +5610,8 @@ "h": 22 }, "frame": { - "x": 277, - "y": 211, + "x": 307, + "y": 128, "w": 22, "h": 22 } @@ -5799,8 +5631,8 @@ "h": 22 }, "frame": { - "x": 276, - "y": 233, + "x": 329, + "y": 63, "w": 22, "h": 22 } @@ -5820,8 +5652,8 @@ "h": 22 }, "frame": { - "x": 276, - "y": 255, + "x": 329, + "y": 85, "w": 22, "h": 22 } @@ -5841,8 +5673,8 @@ "h": 22 }, "frame": { - "x": 299, - "y": 219, + "x": 351, + "y": 63, "w": 22, "h": 22 } @@ -5862,8 +5694,8 @@ "h": 22 }, "frame": { - "x": 321, - "y": 219, + "x": 329, + "y": 107, "w": 22, "h": 22 } @@ -5883,8 +5715,8 @@ "h": 22 }, "frame": { - "x": 343, - "y": 219, + "x": 351, + "y": 85, "w": 22, "h": 22 } @@ -5904,8 +5736,8 @@ "h": 22 }, "frame": { - "x": 365, - "y": 219, + "x": 373, + "y": 63, "w": 22, "h": 22 } @@ -5925,8 +5757,8 @@ "h": 22 }, "frame": { - "x": 298, - "y": 241, + "x": 351, + "y": 107, "w": 22, "h": 22 } @@ -5946,8 +5778,8 @@ "h": 22 }, "frame": { - "x": 320, - "y": 241, + "x": 373, + "y": 85, "w": 22, "h": 22 } @@ -5967,33 +5799,12 @@ "h": 22 }, "frame": { - "x": 342, - "y": 241, + "x": 373, + "y": 107, "w": 22, "h": 22 } }, - { - "filename": "dusk_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 364, - "y": 241, - "w": 21, - "h": 21 - } - }, { "filename": "poison_barb", "rotated": false, @@ -6009,33 +5820,12 @@ "h": 21 }, "frame": { - "x": 387, - "y": 220, + "x": 329, + "y": 129, "w": 21, "h": 21 } }, - { - "filename": "golden_egg", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 - }, - "frame": { - "x": 408, - "y": 220, - "w": 17, - "h": 20 - } - }, { "filename": "shiny_stone", "rotated": false, @@ -6051,14 +5841,77 @@ "h": 21 }, "frame": { - "x": 385, - "y": 241, + "x": 350, + "y": 129, "w": 21, "h": 21 } }, { - "filename": "quick_claw", + "filename": "tera_orb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 22, + "h": 20 + }, + "frame": { + "x": 371, + "y": 129, + "w": 22, + "h": 20 + } + }, + { + "filename": "sitrus_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 5, + "w": 20, + "h": 22 + }, + "frame": { + "x": 395, + "y": 64, + "w": 20, + "h": 22 + } + }, + { + "filename": "zoom_lens", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 6, + "w": 21, + "h": 21 + }, + "frame": { + "x": 395, + "y": 86, + "w": 21, + "h": 21 + } + }, + { + "filename": "gb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6068,14 +5921,140 @@ "spriteSourceSize": { "x": 6, "y": 6, - "w": 19, - "h": 21 + "w": 20, + "h": 20 }, "frame": { - "x": 406, - "y": 241, - "w": 19, - "h": 21 + "x": 395, + "y": 107, + "w": 20, + "h": 20 + } + }, + { + "filename": "relic_crown", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 7, + "w": 23, + "h": 18 + }, + "frame": { + "x": 270, + "y": 149, + "w": 23, + "h": 18 + } + }, + { + "filename": "metronome", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 5, + "w": 17, + "h": 22 + }, + "frame": { + "x": 275, + "y": 167, + "w": 17, + "h": 22 + } + }, + { + "filename": "shock_drive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 293, + "y": 150, + "w": 23, + "h": 17 + } + }, + { + "filename": "upgrade", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 22, + "h": 19 + }, + "frame": { + "x": 292, + "y": 167, + "w": 22, + "h": 19 + } + }, + { + "filename": "wise_glasses", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 4, + "y": 8, + "w": 23, + "h": 17 + }, + "frame": { + "x": 316, + "y": 150, + "w": 23, + "h": 17 + } + }, + { + "filename": "metal_alloy", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 21, + "h": 19 + }, + "frame": { + "x": 314, + "y": 167, + "w": 21, + "h": 19 } }, { @@ -6093,12 +6072,54 @@ "h": 18 }, "frame": { - "x": 298, - "y": 263, + "x": 339, + "y": 150, "w": 21, "h": 18 } }, + { + "filename": "old_gateau", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 21, + "h": 18 + }, + "frame": { + "x": 335, + "y": 168, + "w": 21, + "h": 18 + } + }, + { + "filename": "baton", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 7, + "w": 18, + "h": 18 + }, + "frame": { + "x": 360, + "y": 150, + "w": 18, + "h": 18 + } + }, { "filename": "sharp_meteorite", "rotated": false, @@ -6114,117 +6135,12 @@ "h": 18 }, "frame": { - "x": 319, - "y": 263, + "x": 356, + "y": 168, "w": 21, "h": 18 } }, - { - "filename": "unremarkable_teacup", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 7, - "w": 21, - "h": 18 - }, - "frame": { - "x": 340, - "y": 263, - "w": 21, - "h": 18 - } - }, - { - "filename": "zoom_lens", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 5, - "y": 6, - "w": 21, - "h": 21 - }, - "frame": { - "x": 276, - "y": 277, - "w": 21, - "h": 21 - } - }, - { - "filename": "everstone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 17 - }, - "frame": { - "x": 297, - "y": 281, - "w": 20, - "h": 17 - } - }, - { - "filename": "magnet", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 317, - "y": 281, - "w": 20, - "h": 20 - } - }, - { - "filename": "mb", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 20, - "h": 20 - }, - "frame": { - "x": 337, - "y": 281, - "w": 20, - "h": 20 - } - }, { "filename": "candy_jar", "rotated": false, @@ -6240,14 +6156,77 @@ "h": 20 }, "frame": { - "x": 357, - "y": 281, + "x": 378, + "y": 149, "w": 19, "h": 20 } }, { - "filename": "baton", + "filename": "everstone", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 17 + }, + "frame": { + "x": 377, + "y": 169, + "w": 20, + "h": 17 + } + }, + { + "filename": "golden_egg", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 17, + "h": 20 + }, + "frame": { + "x": 393, + "y": 129, + "w": 17, + "h": 20 + } + }, + { + "filename": "razor_fang", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 18, + "h": 20 + }, + "frame": { + "x": 410, + "y": 127, + "w": 18, + "h": 20 + } + }, + { + "filename": "oval_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6258,15 +6237,99 @@ "x": 7, "y": 7, "w": 18, + "h": 19 + }, + "frame": { + "x": 410, + "y": 147, + "w": 18, + "h": 19 + } + }, + { + "filename": "magnet", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 397, + "y": 166, + "w": 20, + "h": 20 + } + }, + { + "filename": "unremarkable_teacup", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 5, + "y": 7, + "w": 21, "h": 18 }, "frame": { - "x": 361, - "y": 263, - "w": 18, + "x": 292, + "y": 186, + "w": 21, "h": 18 } }, + { + "filename": "lum_berry", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 20, + "h": 19 + }, + "frame": { + "x": 313, + "y": 186, + "w": 20, + "h": 19 + } + }, + { + "filename": "mb", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 333, + "y": 186, + "w": 20, + "h": 20 + } + }, { "filename": "pb", "rotated": false, @@ -6282,8 +6345,8 @@ "h": 20 }, "frame": { - "x": 379, - "y": 262, + "x": 353, + "y": 186, "w": 20, "h": 20 } @@ -6303,33 +6366,12 @@ "h": 20 }, "frame": { - "x": 399, - "y": 262, + "x": 373, + "y": 186, "w": 20, "h": 20 } }, - { - "filename": "razor_claw", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 7, - "w": 20, - "h": 19 - }, - "frame": { - "x": 376, - "y": 282, - "w": 20, - "h": 19 - } - }, { "filename": "rb", "rotated": false, @@ -6345,8 +6387,71 @@ "h": 20 }, "frame": { - "x": 396, - "y": 282, + "x": 393, + "y": 186, + "w": 20, + "h": 20 + } + }, + { + "filename": "eviolite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 15, + "h": 15 + }, + "frame": { + "x": 413, + "y": 186, + "w": 15, + "h": 15 + } + }, + { + "filename": "prism_scale", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 15, + "h": 15 + }, + "frame": { + "x": 413, + "y": 201, + "w": 15, + "h": 15 + } + }, + { + "filename": "smooth_meteorite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 20, + "h": 20 + }, + "frame": { + "x": 289, + "y": 204, "w": 20, "h": 20 } @@ -6366,14 +6471,14 @@ "h": 21 }, "frame": { - "x": 192, - "y": 395, + "x": 289, + "y": 224, "w": 19, "h": 21 } }, { - "filename": "smooth_meteorite", + "filename": "power_herb", "rotated": false, "trimmed": true, "sourceSize": { @@ -6381,16 +6486,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 7, - "y": 6, + "x": 6, + "y": 7, "w": 20, - "h": 20 + "h": 19 }, "frame": { - "x": 211, - "y": 276, + "x": 309, + "y": 205, "w": 20, - "h": 20 + "h": 19 } }, { @@ -6408,12 +6513,33 @@ "h": 20 }, "frame": { - "x": 231, - "y": 279, + "x": 308, + "y": 224, "w": 20, "h": 20 } }, + { + "filename": "razor_claw", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 7, + "w": 20, + "h": 19 + }, + "frame": { + "x": 329, + "y": 206, + "w": 20, + "h": 19 + } + }, { "filename": "ub", "rotated": false, @@ -6429,14 +6555,14 @@ "h": 20 }, "frame": { - "x": 251, - "y": 280, + "x": 349, + "y": 206, "w": 20, "h": 20 } }, { - "filename": "mystery_egg", + "filename": "hard_stone", "rotated": false, "trimmed": true, "sourceSize": { @@ -6444,16 +6570,16 @@ "h": 32 }, "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 18 + "x": 6, + "y": 6, + "w": 19, + "h": 20 }, "frame": { - "x": 195, - "y": 276, - "w": 16, - "h": 18 + "x": 369, + "y": 206, + "w": 19, + "h": 20 } }, { @@ -6471,8 +6597,8 @@ "h": 19 }, "frame": { - "x": 211, - "y": 296, + "x": 388, + "y": 206, "w": 20, "h": 19 } @@ -6492,54 +6618,12 @@ "h": 18 }, "frame": { - "x": 231, - "y": 299, + "x": 408, + "y": 216, "w": 20, "h": 18 } }, - { - "filename": "wl_antidote", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 251, - "y": 300, - "w": 20, - "h": 18 - } - }, - { - "filename": "hard_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 6, - "w": 19, - "h": 20 - }, - "frame": { - "x": 271, - "y": 298, - "w": 19, - "h": 20 - } - }, { "filename": "miracle_seed", "rotated": false, @@ -6555,12 +6639,33 @@ "h": 19 }, "frame": { - "x": 290, - "y": 298, + "x": 328, + "y": 225, "w": 19, "h": 19 } }, + { + "filename": "wl_antidote", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 347, + "y": 226, + "w": 20, + "h": 18 + } + }, { "filename": "wl_awakening", "rotated": false, @@ -6576,8 +6681,8 @@ "h": 18 }, "frame": { - "x": 309, - "y": 301, + "x": 367, + "y": 226, "w": 20, "h": 18 } @@ -6597,8 +6702,8 @@ "h": 18 }, "frame": { - "x": 329, - "y": 301, + "x": 388, + "y": 225, "w": 20, "h": 18 } @@ -6618,12 +6723,33 @@ "h": 18 }, "frame": { - "x": 349, - "y": 301, + "x": 408, + "y": 234, "w": 20, "h": 18 } }, + { + "filename": "soothe_bell", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 5, + "w": 17, + "h": 22 + }, + "frame": { + "x": 291, + "y": 245, + "w": 17, + "h": 22 + } + }, { "filename": "wl_custom_thief", "rotated": false, @@ -6639,8 +6765,8 @@ "h": 18 }, "frame": { - "x": 369, - "y": 301, + "x": 308, + "y": 244, "w": 20, "h": 18 } @@ -6660,54 +6786,12 @@ "h": 18 }, "frame": { - "x": 389, - "y": 302, + "x": 328, + "y": 244, "w": 20, "h": 18 } }, - { - "filename": "aggronite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 409, - "y": 302, - "w": 16, - "h": 16 - } - }, - { - "filename": "alakazite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 409, - "y": 318, - "w": 16, - "h": 16 - } - }, { "filename": "wl_ether", "rotated": false, @@ -6723,12 +6807,33 @@ "h": 18 }, "frame": { - "x": 211, - "y": 315, + "x": 348, + "y": 244, "w": 20, "h": 18 } }, + { + "filename": "lucky_egg", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 7, + "y": 6, + "w": 17, + "h": 20 + }, + "frame": { + "x": 291, + "y": 267, + "w": 17, + "h": 20 + } + }, { "filename": "wl_full_heal", "rotated": false, @@ -6744,8 +6849,8 @@ "h": 18 }, "frame": { - "x": 231, - "y": 317, + "x": 308, + "y": 262, "w": 20, "h": 18 } @@ -6765,8 +6870,8 @@ "h": 18 }, "frame": { - "x": 251, - "y": 318, + "x": 328, + "y": 262, "w": 20, "h": 18 } @@ -6786,33 +6891,12 @@ "h": 18 }, "frame": { - "x": 271, - "y": 318, + "x": 348, + "y": 262, "w": 20, "h": 18 } }, - { - "filename": "oval_stone", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 7, - "w": 18, - "h": 19 - }, - "frame": { - "x": 291, - "y": 317, - "w": 18, - "h": 19 - } - }, { "filename": "wl_hyper_potion", "rotated": false, @@ -6828,8 +6912,8 @@ "h": 18 }, "frame": { - "x": 309, - "y": 319, + "x": 368, + "y": 244, "w": 20, "h": 18 } @@ -6849,8 +6933,8 @@ "h": 18 }, "frame": { - "x": 329, - "y": 319, + "x": 388, + "y": 243, "w": 20, "h": 18 } @@ -6870,8 +6954,8 @@ "h": 18 }, "frame": { - "x": 349, - "y": 319, + "x": 408, + "y": 252, "w": 20, "h": 18 } @@ -6891,8 +6975,8 @@ "h": 18 }, "frame": { - "x": 369, - "y": 319, + "x": 368, + "y": 262, "w": 20, "h": 18 } @@ -6912,33 +6996,12 @@ "h": 18 }, "frame": { - "x": 389, - "y": 320, + "x": 388, + "y": 261, "w": 20, "h": 18 } }, - { - "filename": "altarianite", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 8, - "y": 8, - "w": 16, - "h": 16 - }, - "frame": { - "x": 409, - "y": 334, - "w": 16, - "h": 16 - } - }, { "filename": "wl_max_ether", "rotated": false, @@ -6954,197 +7017,8 @@ "h": 18 }, "frame": { - "x": 199, - "y": 333, - "w": 20, - "h": 18 - } - }, - { - "filename": "razor_fang", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 18, - "h": 20 - }, - "frame": { - "x": 201, - "y": 351, - "w": 18, - "h": 20 - } - }, - { - "filename": "lucky_egg", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 7, - "y": 6, - "w": 17, - "h": 20 - }, - "frame": { - "x": 201, - "y": 371, - "w": 17, - "h": 20 - } - }, - { - "filename": "wl_max_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 219, - "y": 335, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_max_revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 219, - "y": 353, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_paralyze_heal", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 218, - "y": 371, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 239, - "y": 336, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_reset_urge", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 259, - "y": 336, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_revive", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 279, - "y": 336, - "w": 20, - "h": 18 - } - }, - { - "filename": "wl_super_potion", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 6, - "y": 8, - "w": 20, - "h": 18 - }, - "frame": { - "x": 239, - "y": 354, + "x": 408, + "y": 270, "w": 20, "h": 18 } @@ -7164,8 +7038,8 @@ "h": 18 }, "frame": { - "x": 259, - "y": 354, + "x": 291, + "y": 287, "w": 18, "h": 18 } @@ -7185,12 +7059,159 @@ "h": 18 }, "frame": { - "x": 277, - "y": 354, + "x": 291, + "y": 305, "w": 18, "h": 18 } }, + { + "filename": "wl_max_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 290, + "y": 323, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_max_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 290, + "y": 341, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_paralyze_heal", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 290, + "y": 359, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 291, + "y": 377, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_reset_urge", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 291, + "y": 395, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_revive", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 309, + "y": 280, + "w": 20, + "h": 18 + } + }, + { + "filename": "wl_super_potion", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 6, + "y": 8, + "w": 20, + "h": 18 + }, + "frame": { + "x": 309, + "y": 298, + "w": 20, + "h": 18 + } + }, { "filename": "flame_orb", "rotated": false, @@ -7206,8 +7227,8 @@ "h": 18 }, "frame": { - "x": 238, - "y": 372, + "x": 329, + "y": 280, "w": 18, "h": 18 } @@ -7227,8 +7248,8 @@ "h": 18 }, "frame": { - "x": 256, - "y": 372, + "x": 329, + "y": 298, "w": 18, "h": 18 } @@ -7248,8 +7269,8 @@ "h": 18 }, "frame": { - "x": 274, - "y": 372, + "x": 347, + "y": 280, "w": 18, "h": 18 } @@ -7269,12 +7290,75 @@ "h": 18 }, "frame": { - "x": 299, - "y": 337, + "x": 347, + "y": 298, "w": 18, "h": 18 } }, + { + "filename": "mystery_egg", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 18 + }, + "frame": { + "x": 365, + "y": 280, + "w": 16, + "h": 18 + } + }, + { + "filename": "alakazite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 365, + "y": 298, + "w": 16, + "h": 16 + } + }, + { + "filename": "altarianite", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 16, + "h": 16 + }, + "frame": { + "x": 310, + "y": 316, + "w": 16, + "h": 16 + } + }, { "filename": "ampharosite", "rotated": false, @@ -7290,8 +7374,8 @@ "h": 16 }, "frame": { - "x": 317, - "y": 337, + "x": 310, + "y": 332, "w": 16, "h": 16 } @@ -7311,8 +7395,8 @@ "h": 16 }, "frame": { - "x": 333, - "y": 337, + "x": 326, + "y": 316, "w": 16, "h": 16 } @@ -7332,8 +7416,8 @@ "h": 16 }, "frame": { - "x": 349, - "y": 337, + "x": 310, + "y": 348, "w": 16, "h": 16 } @@ -7353,8 +7437,8 @@ "h": 16 }, "frame": { - "x": 365, - "y": 337, + "x": 326, + "y": 332, "w": 16, "h": 16 } @@ -7374,8 +7458,8 @@ "h": 16 }, "frame": { - "x": 295, - "y": 355, + "x": 342, + "y": 316, "w": 16, "h": 16 } @@ -7395,8 +7479,8 @@ "h": 16 }, "frame": { - "x": 381, - "y": 338, + "x": 326, + "y": 348, "w": 16, "h": 16 } @@ -7416,8 +7500,8 @@ "h": 16 }, "frame": { - "x": 311, - "y": 355, + "x": 342, + "y": 332, "w": 16, "h": 16 } @@ -7437,8 +7521,8 @@ "h": 16 }, "frame": { - "x": 327, - "y": 353, + "x": 342, + "y": 348, "w": 16, "h": 16 } @@ -7458,8 +7542,8 @@ "h": 16 }, "frame": { - "x": 343, - "y": 353, + "x": 381, + "y": 280, "w": 16, "h": 16 } @@ -7479,8 +7563,8 @@ "h": 16 }, "frame": { - "x": 359, - "y": 353, + "x": 381, + "y": 296, "w": 16, "h": 16 } @@ -7500,8 +7584,8 @@ "h": 16 }, "frame": { - "x": 375, - "y": 354, + "x": 358, + "y": 316, "w": 16, "h": 16 } @@ -7521,8 +7605,8 @@ "h": 16 }, "frame": { - "x": 391, - "y": 354, + "x": 358, + "y": 332, "w": 16, "h": 16 } @@ -7542,8 +7626,8 @@ "h": 16 }, "frame": { - "x": 407, - "y": 350, + "x": 358, + "y": 348, "w": 16, "h": 16 } @@ -7563,8 +7647,8 @@ "h": 16 }, "frame": { - "x": 407, - "y": 366, + "x": 397, + "y": 288, "w": 16, "h": 16 } @@ -7584,8 +7668,8 @@ "h": 16 }, "frame": { - "x": 211, - "y": 391, + "x": 397, + "y": 304, "w": 16, "h": 16 } @@ -7605,8 +7689,8 @@ "h": 16 }, "frame": { - "x": 211, - "y": 407, + "x": 381, + "y": 312, "w": 16, "h": 16 } @@ -7626,8 +7710,8 @@ "h": 16 }, "frame": { - "x": 227, - "y": 390, + "x": 374, + "y": 328, "w": 16, "h": 16 } @@ -7647,8 +7731,8 @@ "h": 16 }, "frame": { - "x": 227, - "y": 406, + "x": 374, + "y": 344, "w": 16, "h": 16 } @@ -7668,8 +7752,8 @@ "h": 16 }, "frame": { - "x": 243, - "y": 390, + "x": 397, + "y": 320, "w": 16, "h": 16 } @@ -7689,8 +7773,8 @@ "h": 16 }, "frame": { - "x": 243, - "y": 406, + "x": 390, + "y": 336, "w": 16, "h": 16 } @@ -7710,8 +7794,8 @@ "h": 16 }, "frame": { - "x": 259, - "y": 390, + "x": 390, + "y": 352, "w": 16, "h": 16 } @@ -7731,8 +7815,8 @@ "h": 16 }, "frame": { - "x": 259, - "y": 406, + "x": 374, + "y": 360, "w": 16, "h": 16 } @@ -7752,8 +7836,8 @@ "h": 16 }, "frame": { - "x": 275, - "y": 390, + "x": 390, + "y": 368, "w": 16, "h": 16 } @@ -7773,8 +7857,8 @@ "h": 16 }, "frame": { - "x": 275, - "y": 406, + "x": 406, + "y": 336, "w": 16, "h": 16 } @@ -7794,8 +7878,8 @@ "h": 16 }, "frame": { - "x": 292, - "y": 372, + "x": 406, + "y": 352, "w": 16, "h": 16 } @@ -7815,8 +7899,8 @@ "h": 16 }, "frame": { - "x": 308, - "y": 371, + "x": 406, + "y": 368, "w": 16, "h": 16 } @@ -7836,8 +7920,8 @@ "h": 16 }, "frame": { - "x": 324, - "y": 371, + "x": 311, + "y": 364, "w": 16, "h": 16 } @@ -7857,8 +7941,8 @@ "h": 16 }, "frame": { - "x": 340, - "y": 369, + "x": 327, + "y": 364, "w": 16, "h": 16 } @@ -7878,8 +7962,8 @@ "h": 16 }, "frame": { - "x": 356, - "y": 369, + "x": 311, + "y": 380, "w": 16, "h": 16 } @@ -7899,8 +7983,8 @@ "h": 16 }, "frame": { - "x": 372, - "y": 370, + "x": 343, + "y": 364, "w": 16, "h": 16 } @@ -7920,8 +8004,8 @@ "h": 16 }, "frame": { - "x": 388, - "y": 370, + "x": 311, + "y": 396, "w": 16, "h": 16 } @@ -7941,8 +8025,8 @@ "h": 16 }, "frame": { - "x": 292, - "y": 388, + "x": 311, + "y": 412, "w": 16, "h": 16 } @@ -7962,8 +8046,8 @@ "h": 16 }, "frame": { - "x": 308, - "y": 387, + "x": 327, + "y": 380, "w": 16, "h": 16 } @@ -7983,8 +8067,8 @@ "h": 16 }, "frame": { - "x": 324, - "y": 387, + "x": 327, + "y": 396, "w": 16, "h": 16 } @@ -8004,8 +8088,8 @@ "h": 16 }, "frame": { - "x": 340, - "y": 385, + "x": 327, + "y": 412, "w": 16, "h": 16 } @@ -8025,8 +8109,8 @@ "h": 16 }, "frame": { - "x": 356, - "y": 385, + "x": 343, + "y": 380, "w": 16, "h": 16 } @@ -8046,8 +8130,8 @@ "h": 16 }, "frame": { - "x": 291, - "y": 404, + "x": 343, + "y": 396, "w": 16, "h": 16 } @@ -8067,8 +8151,8 @@ "h": 16 }, "frame": { - "x": 372, - "y": 386, + "x": 343, + "y": 412, "w": 16, "h": 16 } @@ -8088,8 +8172,8 @@ "h": 16 }, "frame": { - "x": 388, - "y": 386, + "x": 359, + "y": 376, "w": 16, "h": 16 } @@ -8109,8 +8193,8 @@ "h": 16 }, "frame": { - "x": 404, - "y": 382, + "x": 359, + "y": 392, "w": 16, "h": 16 } @@ -8130,8 +8214,8 @@ "h": 16 }, "frame": { - "x": 404, - "y": 398, + "x": 359, + "y": 408, "w": 16, "h": 16 } @@ -8151,8 +8235,8 @@ "h": 16 }, "frame": { - "x": 340, - "y": 401, + "x": 375, + "y": 384, "w": 16, "h": 16 } @@ -8172,8 +8256,8 @@ "h": 16 }, "frame": { - "x": 356, - "y": 401, + "x": 375, + "y": 400, "w": 16, "h": 16 } @@ -8193,8 +8277,8 @@ "h": 16 }, "frame": { - "x": 372, - "y": 402, + "x": 391, + "y": 384, "w": 16, "h": 16 } @@ -8214,8 +8298,8 @@ "h": 16 }, "frame": { - "x": 388, - "y": 402, + "x": 391, + "y": 400, "w": 16, "h": 16 } @@ -8226,6 +8310,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:4669e332ee400e355936594c14e7221c:1a1f5a801c94e8eb8589e13bc50105a1:110e074689c9edd2c54833ce2e4d9270$" + "smartupdate": "$TexturePacker:SmartUpdate:ae06b70c6800597c7ac11beefc1d8aad:b9f30512e12737247c8cb2691252baac:110e074689c9edd2c54833ce2e4d9270$" } } diff --git a/public/images/items.png b/public/images/items.png index 9de02d9e0e9..a031b00ceb0 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 00000000000..c0986b804f9 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 00000000000..39684a40310 Binary files /dev/null and b/public/images/items/black_sludge.png differ diff --git a/public/images/items/macho_brace.png b/public/images/items/macho_brace.png new file mode 100644 index 00000000000..2085829e1ce Binary files /dev/null and b/public/images/items/macho_brace.png differ diff --git a/public/images/items/old_gateau.png b/public/images/items/old_gateau.png new file mode 100644 index 00000000000..c910e90f101 Binary files /dev/null and b/public/images/items/old_gateau.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 00000000000..e143086e157 --- /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 00000000000..9dcc1281c9e 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 00000000000..8f07c7d44e2 --- /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 00000000000..967d82973e6 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 00000000000..ae9ee38ee13 --- /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 00000000000..7de9169d187 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 00000000000..397538d8af2 --- /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 00000000000..e9be20b4863 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 00000000000..ba5d9567fe5 --- /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 00000000000..fb957ac29f0 Binary files /dev/null and b/public/images/mystery-encounters/buoy.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 00000000000..9a386802e03 --- /dev/null +++ b/public/images/mystery-encounters/chest_blue.json @@ -0,0 +1,209 @@ +{ + "textures": [ + { + "image": "chest_blue.png", + "format": "RGBA8888", + "size": { + "w": 58, + "h": 528 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 14, + "y": 30, + "w": 48, + "h": 41 + }, + "frame": { + "x": 1, + "y": 1, + "w": 48, + "h": 41 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 14, + "y": 34, + "w": 49, + "h": 37 + }, + "frame": { + "x": 1, + "y": 44, + "w": 49, + "h": 37 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 14, + "y": 30, + "w": 48, + "h": 41 + }, + "frame": { + "x": 1, + "y": 83, + "w": 48, + "h": 41 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 14, + "y": 23, + "w": 48, + "h": 48 + }, + "frame": { + "x": 1, + "y": 126, + "w": 48, + "h": 48 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 13, + "y": 4, + "w": 55, + "h": 67 + }, + "frame": { + "x": 1, + "y": 176, + "w": 55, + "h": 67 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 15, + "y": 2, + "w": 56, + "h": 69 + }, + "frame": { + "x": 1, + "y": 245, + "w": 56, + "h": 69 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 15, + "y": 2, + "w": 56, + "h": 69 + }, + "frame": { + "x": 1, + "y": 316, + "w": 56, + "h": 69 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 56, + "h": 69 + }, + "frame": { + "x": 1, + "y": 387, + "w": 56, + "h": 69 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 75, + "h": 75 + }, + "spriteSourceSize": { + "x": 13, + "y": 2, + "w": 56, + "h": 69 + }, + "frame": { + "x": 1, + "y": 458, + "w": 56, + "h": 69 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:5f36000f6160ee6f397afe5a6fd60b73:cf6f4b08e23400447813583c322eb6c7:f4f3c064e6c93b8d1290f93bee927f60$" + } +} diff --git a/public/images/mystery-encounters/chest_blue.png b/public/images/mystery-encounters/chest_blue.png new file mode 100644 index 00000000000..ac1039544e3 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 00000000000..d808652a162 --- /dev/null +++ b/public/images/mystery-encounters/chest_red.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "chest_red.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/chest_red.png b/public/images/mystery-encounters/chest_red.png new file mode 100644 index 00000000000..e9ccfa2cfc6 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 00000000000..5a48d95c18d --- /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 00000000000..168999fb0f4 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 00000000000..deb9426c269 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 00000000000..a7727f4da2e Binary files /dev/null and b/public/images/mystery-encounters/exclaim.png differ diff --git a/public/images/mystery-encounters/girawitch.json b/public/images/mystery-encounters/girawitch.json new file mode 100644 index 00000000000..98830a00a72 --- /dev/null +++ b/public/images/mystery-encounters/girawitch.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "girawitch.png", + "format": "RGBA8888", + "size": { + "w": 46, + "h": 76 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 46, + "h": 76 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 46, + "h": 76 + }, + "frame": { + "x": 0, + "y": 0, + "w": 46, + "h": 76 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e68bbc186f511d505c53b2beec3c3741:7108795fc29d953a1d3729ad93d70936:1661aeeeb2f0e4561c644aff254770b3$" + } +} diff --git a/public/images/mystery-encounters/girawitch.png b/public/images/mystery-encounters/girawitch.png new file mode 100644 index 00000000000..a1cc7e8448d Binary files /dev/null and b/public/images/mystery-encounters/girawitch.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 00000000000..10aa3d6f42a --- /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 00000000000..453cb767ec1 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 00000000000..505a6fadd27 --- /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 00000000000..2ba7cb00047 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 00000000000..23d9df44f2b --- /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 00000000000..1251dd8eda7 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 00000000000..fe81d1b9f53 --- /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 00000000000..375d66ebbe9 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 00000000000..457d440a010 --- /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 00000000000..b4332bc0032 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 00000000000..e267c9a3dde --- /dev/null +++ b/public/images/mystery-encounters/teleporter.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "teleporter.png", + "format": "RGBA8888", + "size": { + "w": 64, + "h": 78 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 64, + "h": 78 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 64, + "h": 78 + }, + "frame": { + "x": 0, + "y": 0, + "w": 64, + "h": 78 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:a8e006630c2838130468b0d5c9aeb8a6:684c1813cb6c86e395c18027a593ed28:ce1615396ce7b0a146766d50b319bb81$" + } +} diff --git a/public/images/mystery-encounters/teleporter.png b/public/images/mystery-encounters/teleporter.png new file mode 100644 index 00000000000..e71170ff184 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 00000000000..fb8f4ec9c8e --- /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 00000000000..42c3a9bb7d4 Binary files /dev/null and b/public/images/mystery-encounters/training_gear.png differ diff --git a/public/images/trainer/buck.json b/public/images/trainer/buck.json new file mode 100644 index 00000000000..d2d215f716a --- /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 00000000000..2384fb42a33 Binary files /dev/null and b/public/images/trainer/buck.png differ diff --git a/public/images/trainer/cheryl.json b/public/images/trainer/cheryl.json new file mode 100644 index 00000000000..4cac665a588 --- /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 00000000000..c46505f6b25 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 00000000000..92d9f1449e5 --- /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 00000000000..8e78e11e8ad 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 00000000000..7bd29f53475 --- /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 00000000000..5c1afe5d241 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 00000000000..f0f84a909db --- /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 00000000000..a9f0e3b53a9 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 00000000000..c19cf11622d --- /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 00000000000..3e2d6c13696 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 00000000000..5afa9704567 --- /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 00000000000..3ffddea24bb 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 00000000000..7917113621a --- /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 00000000000..e2874f266ad 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 00000000000..61dcf7af0ef --- /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 00000000000..a7c6c0444f4 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 00000000000..b36ebcd7c0c --- /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 00000000000..cd97e676cfb 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 922a145780b..50fcad4f417 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2,7 +2,7 @@ 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 { PokeballType } from "./data/pokeball"; @@ -11,7 +11,7 @@ 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 AbilityBar from "./ui/ability-bar"; @@ -22,14 +22,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"; @@ -84,6 +84,13 @@ 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, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import HeldModifierConfig from "#app/interfaces/held-modifier-config"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -236,6 +243,8 @@ export default class BattleScene extends SceneBase { public money: integer; public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; + public mysteryEncounterData: MysteryEncounterData = new MysteryEncounterData(null); + public lastMysteryEncounter: MysteryEncounter; /** Combined Biome and Wave count text */ private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; @@ -389,6 +398,7 @@ export default class BattleScene extends SceneBase { this.fieldUI = fieldUI; + // @ts-ignore const transition = this.make.rexTransitionImagePack({ x: 0, y: 0, @@ -874,6 +884,20 @@ export default class BattleScene extends SceneBase { return pokemon; } + 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`); @@ -1065,7 +1089,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, mysteryEncounter?: MysteryEncounter): Battle | null { const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave; const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1); let newDouble: boolean | undefined; @@ -1113,6 +1137,40 @@ export default class BattleScene extends SceneBase { newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT); this.field.add(newTrainer); } + + // TODO: remove these once ME spawn rates are finalized + // let testStartingWeight = 0; + // while (testStartingWeight < 3) { + // calculateMEAggregateStats(this, testStartingWeight); + // testStartingWeight += 2; + // } + // calculateRareSpawnAggregateStats(this, 14); + + // Check for mystery encounter + // Can only occur in place of a standard wild battle, waves 10-180 + if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < 180 && newWaveIndex > 10) { + const roll = Utils.randSeedInt(256); + + // Base spawn weight is 1/256, and increases by 5/256 for each missed attempt at spawning an encounter on a valid floor + const sessionEncounterRate = !isNullOrUndefined(this.mysteryEncounterData?.encounterSpawnChance) ? this.mysteryEncounterData.encounterSpawnChance : BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + + // If total number of encounters is lower than expected for the run, slightly favor a new encounter spawn + // Do the reverse as well + // Reduces occurrence of runs with very few (<6) and a ton (>10) of encounters + const expectedEncountersByFloor = AVERAGE_ENCOUNTERS_PER_RUN_TARGET / (180 - 10) * newWaveIndex; + const currentRunDiffFromAvg = expectedEncountersByFloor - (this.mysteryEncounterData?.encounteredEvents?.length || 0); + const favoredEncounterRate = sessionEncounterRate + currentRunDiffFromAvg * 5; + + const successRate = isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) ? favoredEncounterRate : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE!; + + if (roll < successRate) { + newBattleType = BattleType.MYSTERY_ENCOUNTER; + // Reset base spawn weight + this.mysteryEncounterData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + } else { + this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS; + } + } } if (double === undefined && newWaveIndex > 1) { @@ -1145,12 +1203,21 @@ export default class BattleScene extends SceneBase { const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; + this.lastMysteryEncounter = lastBattle?.mysteryEncounter ?? null; 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(mysteryEncounter); + }, 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) { @@ -1167,14 +1234,16 @@ export default class BattleScene extends SceneBase { } if (resetArenaState) { this.arena.resetArenaEffects(); - playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p))); + if (lastBattle?.mysteryEncounter?.encounterMode !== MysteryEncounterMode.NO_BATTLE) { + playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p))); - for (const pokemon of this.getParty()) { - pokemon.resetBattleData(); - applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + for (const pokemon of this.getParty()) { + pokemon.resetBattleData(); + applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon); + } + + this.pushPhase(new ShowTrainerPhase(this)); } - - this.pushPhase(new ShowTrainerPhase(this)); } for (const pokemon of this.getParty()) { @@ -2418,7 +2487,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(); @@ -2440,29 +2509,42 @@ 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 => { + const stackCount = mt.stackCount ?? 1; + // const isTransferable = mt.isTransferable ?? true; + const modifier = mt.modifierType.newModifier(enemyPokemon); + modifier.stackCount = stackCount; + // TODO: set isTransferable + // modifier.setIsTransferable(isTransferable); + 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()); }); @@ -2731,4 +2813,114 @@ export default class BattleScene extends SceneBase { this.shiftPhase(); } + + /** + * Loads or generates a mystery encounter + * @param override - used to load session encounter when restarting game, etc. + * @returns + */ + getMysteryEncounter(override: MysteryEncounter | undefined): 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 = override?.encounterType && override.encounterType >= 0 ? allMysteryEncounters[override.encounterType] : null; + } + + // Check for queued encounters first + if (!encounter && this.mysteryEncounterData?.nextEncounterQueue && this.mysteryEncounterData.nextEncounterQueue.length > 0) { + let i = 0; + while (i < this.mysteryEncounterData.nextEncounterQueue.length && !!encounter) { + const candidate = this.mysteryEncounterData.nextEncounterQueue[i]; + const forcedChance = candidate[1]; + if (Utils.randSeedInt(100) < forcedChance) { + encounter = allMysteryEncounters[candidate[0]]; + } + + 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/uncommons in run + this.mysteryEncounterData.encounteredEvents.forEach(val => { + const tier = val[1]; + if (tier === MysteryEncounterTier.COMMON) { + tierWeights[0] = tierWeights[0] - 6; + } else if (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 uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1]; + const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; + let tier: MysteryEncounterTier | null = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > uncommonThreshold ? MysteryEncounterTier.GREAT : tierValue > rareThreshold ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; + + if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { + tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE!; + } + + let availableEncounters: MysteryEncounter[] = []; + // New encounter will never be the same as the most recent encounter + const previousEncounter = this.mysteryEncounterData.encounteredEvents?.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1][0] : 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; + } + if (!encounterCandidate.meetsRequirements!(this)) { // Meets encounter requirements + return false; + } + if (!isNullOrUndefined(previousEncounter) && encounterType === previousEncounter) { // Previous encounter was not this one + return false; + } + if (this.mysteryEncounterData.encounteredEvents?.length > 0 && // Encounter has not exceeded max allowed encounters + (encounterCandidate.maxAllowedEncounters && encounterCandidate.maxAllowedEncounters > 0) + && this.mysteryEncounterData.encounteredEvents.filter(e => e[0] === 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) { + 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 88288ac8118..035b051326e 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,32 +14,35 @@ 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?: integer; - move?: QueuedMove; - targets?: BattlerIndex[]; - skip?: boolean; - args?: any[]; + command: Command; + cursor?: integer; + move?: QueuedMove; + targets?: BattlerIndex[]; + skip?: boolean; + args?: any[]; } interface TurnCommands { - [key: integer]: TurnCommand | null + [key: integer]: TurnCommand | null } export default class Battle { @@ -67,6 +70,7 @@ export default class Battle { public lastUsedPokeball: PokeballType | null; public playerFaints: number; // The amount of times pokemon on the players side have fainted public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted + public mysteryEncounter: MysteryEncounter; private rngCounter: integer = 0; @@ -105,7 +109,7 @@ export default class Battle { this.battleSpec = spec; } - private getLevelForWave(): integer { + public getLevelForWave(): integer { const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex); const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2); const bossMultiplier = 1.2; @@ -203,7 +207,7 @@ 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.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/battle-anims.ts b/src/data/battle-anims.ts index a2f6e41f4ae..4f646fa9532 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -6,6 +6,8 @@ import * as Utils from "../utils"; import { BattlerIndex } from "../battle"; import { Element } from "json-stable-stringify"; import { Moves } from "#enums/moves"; +import { isNullOrUndefined } from "../utils"; +import Phaser from "phaser"; //import fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { @@ -102,6 +104,18 @@ export enum CommonAnim { LOCK_ON = 2120 } +/** + * 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 +} + export class AnimConfig { public id: integer; public graphic: string; @@ -303,7 +317,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; } @@ -321,7 +335,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 { @@ -383,7 +397,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; @@ -413,7 +427,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(); } @@ -425,7 +439,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); } @@ -445,6 +461,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 => { @@ -515,6 +532,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)) { @@ -569,6 +606,16 @@ export function loadCommonAnimAssets(scene: BattleScene, startLoad?: boolean): P }); } +/** + * Loads encounter animation assets to scene + * MUST be called after [initEncounterAnims()](./battle-anims.ts) 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(); @@ -678,14 +725,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; @@ -757,9 +806,9 @@ export abstract class BattleAnim { play(scene: BattleScene, 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(); } @@ -983,13 +1032,181 @@ 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 spritePriorities: integer[] = []; + + 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 r = anim!.frames.length; + let f = 0; + + const existingFieldSprites = [...scene.field.getAll()]; + + scene.tweens.addCounter({ + duration: Utils.getFrameMs(3) * frameTimeMult, + repeat: anim!.frames.length, + onRepeat: () => { + const spriteFrames = anim!.frames[f]; + const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[f], targetInitialX, targetInitialY); + const u = 0; + const t = 0; + let g = 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 (g === sprites.length) { + const newSprite: Phaser.GameObjects.Sprite = scene.addFieldSprite(0, 0, anim!.graphic, 1); + sprites.push(newSprite); + scene.field.add(newSprite); + spritePriorities.push(1); + } + + const graphicIndex = g++; + const moveSprite = sprites[graphicIndex]; + spritePriorities[graphicIndex] = frame.priority; + if (!isNullOrUndefined(frame.priority)) { + const setSpritePriority = (priority: integer) => { + if (existingFieldSprites.length > priority) { + // Move to specified priority index + scene.field.moveTo(moveSprite, scene.field.getIndex(existingFieldSprites[priority])); + } 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(f)) { + for (const event of anim.frameTimedEvents.get(f)!) { + r = Math.max((anim.frames.length - f) + event.execute(scene, this, frameTimedEventPriority), r); + } + } + const targets = Utils.getEnumValues(AnimFrameTarget); + for (const i of targets) { + const count = i === AnimFrameTarget.GRAPHIC ? g : i === AnimFrameTarget.USER ? u : t; + if (count < spriteCache[i].length) { + const spritesToRemove = spriteCache[i].slice(count, spriteCache[i].length); + for (const rs of spritesToRemove) { + if (!rs.getData("locked") as boolean) { + const spriteCacheIndex = spriteCache[i].indexOf(rs); + spriteCache[i].splice(spriteCacheIndex, 1); + if (i === AnimFrameTarget.GRAPHIC) { + spritePriorities.splice(spriteCacheIndex, 1); + } + rs.destroy(); + } + } + } + } + f++; + r--; + }, + onComplete: () => { + for (const ms of Object.values(spriteCache).flat()) { + if (ms && !ms.getData("locked")) { + ms.destroy(); + } + } + if (r) { + scene.tweens.addCounter({ + duration: Utils.getFrameMs(r), + 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; } @@ -1051,6 +1268,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 8c05d296e76..806be6c2947 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -6,7 +6,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 { BattleStat } from "./battle-stat"; @@ -1827,6 +1827,37 @@ export class ExposedTag extends BattlerTag { } } +export class MysteryEncounterPostSummonTag extends BattlerTag { + constructor(sourceMove: Moves) { + super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1, sourceMove); + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + const ret = super.lapse(pokemon, lapseType); + + if (lapseType === BattlerTagLapseType.CUSTOM) { + // Give pokemon +1 stats for battle + const cancelled = new Utils.BooleanHolder(false); + applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + if (!cancelled.value) { + const mysteryEncounterBattleEffects = pokemon.mysteryEncounterBattleEffects; + if (mysteryEncounterBattleEffects) { + mysteryEncounterBattleEffects(pokemon); + } + } + } + + return ret; + } + + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} export function getBattlerTag(tagType: BattlerTagType, turnCount: number, sourceMove: Moves, sourceId: number): BattlerTag { switch (tagType) { @@ -1962,6 +1993,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.GULP_MISSILE_ARROKUDA: case BattlerTagType.GULP_MISSILE_PIKACHU: return new GulpMissileTag(tagType, sourceMove); + case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON: + return new MysteryEncounterPostSummonTag(sourceMove); 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 3e48d81ed12..6c178f10e1e 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -909,6 +909,126 @@ 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" + ], + defeat: [ + "dialogue:stat_trainer_buck.defeat.1" + ] + } + ], + [TrainerType.CHERYL]: [ + { + encounter: [ + "dialogue:stat_trainer_cheryl.encounter.1", + "dialogue:stat_trainer_cheryl.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_cheryl.victory.1" + ], + defeat: [ + "dialogue:stat_trainer_cheryl.defeat.1" + ] + } + ], + [TrainerType.MARLEY]: [ + { + encounter: [ + "dialogue:stat_trainer_marley.encounter.1", + "dialogue:stat_trainer_marley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_marley.victory.1" + ], + defeat: [ + "dialogue:stat_trainer_marley.defeat.1" + ] + } + ], + [TrainerType.MIRA]: [ + { + encounter: [ + "dialogue:stat_trainer_mira.encounter.1", + "dialogue:stat_trainer_mira.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_mira.victory.1" + ], + defeat: [ + "dialogue:stat_trainer_mira.defeat.1" + ] + } + ], + [TrainerType.RILEY]: [ + { + encounter: [ + "dialogue:stat_trainer_riley.encounter.1", + "dialogue:stat_trainer_riley.encounter.2" + ], + victory: [ + "dialogue:stat_trainer_riley.victory.1" + ], + defeat: [ + "dialogue:stat_trainer_riley.defeat.1" + ] + } + ], + [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 3e872d364f3..1406ff9d869 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, + + /** If Egg is of {@link EggSourceType.EVENT}, can customize the message displayed for where the egg was obtained */ + eventEggTypeDescriptor?: string; } export class Egg { @@ -83,6 +86,8 @@ export class Egg { private _overrideHiddenAbility: boolean; + private _eventEggTypeDescriptor?: string; + //// // #endregion //// @@ -180,6 +185,8 @@ export class Egg { this.increasePullStatistic(eggOptions.scene!); // TODO: is this bang correct? this.addEggToGameData(eggOptions.scene!); // TODO: is this bang correct? } + + this._eventEggTypeDescriptor = eggOptions?.eventEggTypeDescriptor; } //// @@ -279,6 +286,8 @@ export class Egg { return i18next.t("egg:gachaTypeShiny"); case EggSourceType.GACHA_MOVE: return i18next.t("egg:gachaTypeMove"); + case EggSourceType.EVENT: + return this._eventEggTypeDescriptor ?? i18next.t("egg:eventType"); default: console.warn("getEggTypeDescriptor case not defined. Returning default empty string"); return ""; 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 00000000000..003dcf0cde1 --- /dev/null +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -0,0 +1,184 @@ +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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:aTrainersTest"; + +/** + * A Trainer's Test encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/115 | GitHub Issue #115} + * @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].copy(); + 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; + // Spawn standard trainer battle with memory mushroom reward + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + await transitionMysteryEncounterIntroVisuals(scene); + + const eggOptions: IEggOptions = { + scene, + pulled: false, + sourceType: EggSourceType.EVENT, + eventEggTypeDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.ULTRA + }; + encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.epic`)); + setEncounterRewards(scene, { 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, + eventEggTypeDescriptor: encounter.misc.trainerEggDescription, + tier: EggTier.GREAT + }; + encounter.setDialogueToken("eggType", i18next.t(`${namespace}.eggTypes.rare`)); + setEncounterRewards(scene, { fillRemaining: false, rerollMultiplier: 0 }, [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 00000000000..29e84355ace --- /dev/null +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -0,0 +1,514 @@ +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 { BattleStat } from "#app/data/battle-stat"; +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 { StatChangePhase } from "#app/phases/stat-change-phase"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:absoluteAvarice"; + +/** + * Absolute Avarice encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/58 | GitHub Issue #58} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AbsoluteAvariceEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.ABSOLUTE_AVARICE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .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"); + 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({ 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 StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + 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 => { + const seedModifier = revSeed.newModifier(p); + 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("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("PRSFX- Bug Bite"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("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: integer) { + 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 00000000000..19bc4d98513 --- /dev/null +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -0,0 +1,162 @@ +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"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:offerYouCantRefuse"; + +/** + * An Offer You Can't Refuse encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/72 | GitHub Issue #72} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const AnOfferYouCantRefuseEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .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); + } + } + + 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 00000000000..7a464a5fd55 --- /dev/null +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -0,0 +1,267 @@ +import { BattleStat } from "#app/data/battle-stat"; +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 { Stat } from "#enums/stat"; +import { StatChangePhase } from "#app/phases/stat-change-phase"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:berriesAbound"; + +/** + * Berries Abound encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/24 | GitHub Issue #24} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BerriesAboundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BERRIES_ABOUND) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .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, 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 + shopOptions.push(generateModifierTypeOption(scene, modifierTypes.BERRY)); + } + + 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 + shopOptions.push(generateModifierTypeOption(scene, modifierTypes.BERRY)); + } + + 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 StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.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); + break; + } + } +} 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 00000000000..e8a7a238743 --- /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 { EncounterAnim, EncounterBattleAnim } from "#app/data/battle-anims"; +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/69 | GitHub Issue #69} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ClowningAroundEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(80, 180) + .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].copy(); + 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), + mysteryEncounterData: new MysteryEncounterPokemonData(undefined, ability, undefined, [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]); + + 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 === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { + numUltra += m.stackCount; + scene.removeModifier(m); + } else if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { + numRogue += 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); + }) + .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) => { + // Swap player's types on all party pokemon + // If a Pokemon had a single type prior, they will still have a single type after + 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); + } + + let newTypes; + if (!originalTypes || originalTypes.length < 1) { + newTypes = priorityTypes?.length > 0 ? [priorityTypes.pop()] : [(randSeedInt(18) as Type)]; + } else { + newTypes = originalTypes.map(m => { + if (priorityTypes?.length > 0) { + const ret = priorityTypes.pop(); + randSeedShuffle(priorityTypes); + return ret; + } + + return randSeedInt(18) as Type; + }); + } + + if (!pokemon.mysteryEncounterData) { + pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes); + } else { + pokemon.mysteryEncounterData.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); + }) + .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`, 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 + if (!pokemon.mysteryEncounterData) { + pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, Abilities.AERILATE); + } + pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability; + scene.currentBattle.mysteryEncounter.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: integer, 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 00000000000..d5515ce43cb --- /dev/null +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -0,0 +1,295 @@ +import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { 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 { EncounterAnim, 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 { BattleStat } from "#app/data/battle-stat"; +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 { StatChangePhase } from "#app/phases/stat-change-phase"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/130 | GitHub Issue #130} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DancingLessonsEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DANCING_LESSONS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .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.getPlayerPokemon()); + 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 enemyPokemon = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], 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); + + // Adds a real Pokemon sprite to the field (required for the animation) + scene.currentBattle.enemyParty[0] = enemyPokemon; + scene.field.add(enemyPokemon); + + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [{ + species: species, + dataSource: oricorioData, + isBoss: true, + // Gets +1 to all stats on battle start + tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], + mysteryEncounterBattleEffects: (pokemon: Pokemon) => { + queueEncounterMessage(pokemon.scene, `${namespace}.option.1.boss_enraged`); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1)); + } + }], + }; + encounter.enemyPartyConfigs = [config]; + encounter.misc = { + oricorioData + }; + + 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; + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.REVELATION_DANCE), + ignorePp: true + }); + + 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 + 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); + } + } + + transitionMysteryEncounterIntroVisuals(scene, true, true, 500); + await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); 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 00000000000..4136100b6b8 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -0,0 +1,187 @@ +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"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/61 | GitHub Issue #61} + * @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, 180) // waves 30 to 180 + .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); + scene.removePokemonFromPlayerParty(removedPokemon); + + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); + + // Store removed pokemon types + scene.currentBattle.mysteryEncounter.misc = [ + removedPokemon.species.type1, + ]; + if (removedPokemon.species.type2) { + scene.currentBattle.mysteryEncounter.misc.push(removedPokemon.species.type2); + } + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player 5 Rogue Balls + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); + + // Start encounter with random legendary (7-10 starter strength) that has level additive + const bossTypes = scene.currentBattle.mysteryEncounter.misc as Type[]; + // 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, + }; + 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 00000000000..e6ed3641982 --- /dev/null +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -0,0 +1,302 @@ +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"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/57 | GitHub Issue #57} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DelibirdyEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // 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`, + } + ]) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 2.75) // 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 }), 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 }), 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 }), 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 }), 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 00000000000..09c991ee4a3 --- /dev/null +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -0,0 +1,163 @@ +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"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:departmentStoreSale"; + +/** + * Department Store Sale encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/33 | GitHub Issue #33} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DepartmentStoreSaleEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 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_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 00000000000..ca6f7424dc8 --- /dev/null +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -0,0 +1,320 @@ +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 { TempBattleStat } from "#app/data/temp-battle-stat"; +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"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fieldTrip"; + +/** + * Field Trip encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/17 | GitHub Issue #17} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieldTripEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIELD_TRIP) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .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`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .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 + const correctMove = move.getMove().category === MoveCategory.PHYSICAL; + encounter.setDialogueToken("moveCategory", "Physical"); + if (!correctMove) { + encounter.options[0].dialogue!.selected = [ + { + text: `${namespace}.option.incorrect`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.option.lesson_learned`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_bad`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + } else { + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); + encounter.setDialogueToken("move", move.getName()); + encounter.options[0].dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_good`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); + } + encounter.misc = { + correctMove: correctMove, + }; + 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_BOOSTER, [TempBattleStat.ATK]), + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.DEF]), + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT), + ]; + + 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`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .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 + const correctMove = move.getMove().category === MoveCategory.SPECIAL; + encounter.setDialogueToken("moveCategory", "Special"); + if (!correctMove) { + encounter.options[1].dialogue!.selected = [ + { + text: `${namespace}.option.incorrect`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.option.lesson_learned`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_bad`, + speaker: `${namespace}.speaker`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_bad`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + } else { + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); + encounter.setDialogueToken("move", move.getName()); + encounter.options[1].dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_good`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); + } + encounter.misc = { + correctMove: correctMove, + }; + 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_BOOSTER, [TempBattleStat.SPATK]), + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPDEF]), + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT), + ]; + + 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`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }) + .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 + const correctMove = move.getMove().category === MoveCategory.STATUS; + encounter.setDialogueToken("moveCategory", "Status"); + if (!correctMove) { + encounter.options[2].dialogue!.selected = [ + { + text: `${namespace}.option.incorrect`, + speaker: `${namespace}.speaker`, + }, + { + text: `${namespace}.option.lesson_learned`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_bad`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); + } else { + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); + encounter.setDialogueToken("move", move.getName()); + encounter.options[2].dialogue!.selected = [ + { + text: `${namespace}.option.selected`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}.outro_good`, + speaker: `${namespace}.speaker`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); + } + encounter.misc = { + correctMove: correctMove, + }; + 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_BOOSTER, [TempBattleStat.ACC]), + generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), + generateModifierTypeOption(scene, modifierTypes.GREAT_BALL), + generateModifierTypeOption(scene, modifierTypes.IV_SCANNER), + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .build(); 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 00000000000..fe5cf320401 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -0,0 +1,251 @@ +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 { EncounterAnim, 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"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/88 | GitHub Issue #88} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FieryFalloutEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIERY_FALLOUT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(40, 180) + .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); + + 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 00000000000..a7aeefe2db5 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -0,0 +1,174 @@ +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 { 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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fightOrFlight"; + +/** + * Fight or Flight encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/24 | GitHub Issue #24} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FightOrFlightEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .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); + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [{ + level: level, + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate item + // 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 excluded from possible rewards as they're too swingy in value for a singular item reward + while (!item || item.type.id.includes("TM_")) { + 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 + 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/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts new file mode 100644 index 00000000000..db8168e7bd7 --- /dev/null +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -0,0 +1,142 @@ +import { getPokemonSpecies } from "#app/data/pokemon-species.js"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species.js"; +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"; + +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/AsdarDevelops/PokeRogue-Events/issues/9 | GitHub Issue #9} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(11, 179) + .withIntroSpriteConfigs([ + { + spriteKey: "buoy", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 20, + y: 3, + }, + ]) + .withIntroDialogue([{ text: `${namespace}.intro` }]) + .withOnInit((scene: BattleScene) => { + const { mysteryEncounter } = scene.currentBattle; + + mysteryEncounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE)); + mysteryEncounter.setDialogueToken("option1RequiredMove", Moves[OPTION_1_REQUIRED_MOVE]); + mysteryEncounter.setDialogueToken("option2RequiredMove", Moves[OPTION_2_REQUIRED_MOVE]); + + 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 + * @param guidePokemon pokemon choosen as a guide + */ +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 00000000000..1695466c1cd --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -0,0 +1,212 @@ +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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:mysteriousChallengers"; + +/** + * Mysterious Challengers encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/41 | GitHub Issue #41} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChallengersEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .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].copy(); + 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].copy(); + 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: 0.5, + 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].copy(); + 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, + 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 with ULTRA/GREAT reward (can improve with luck) + 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 with ROGUE/ULTRA/GREAT reward (can improve with luck) + 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 00000000000..303beb57aae --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -0,0 +1,141 @@ +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { leaveEncounterWithoutBattle, setEncounterRewards } 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.js"; +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"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:mysteriousChest"; + +/** + * Mysterious Chest encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/32 | GitHub Issue #32} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const MysteriousChestEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 10, + yShadow: 3, + disableAnimation: true, // Re-enabled after option select + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + } + ]) + .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) => { + // Play animation + const introVisuals = + scene.currentBattle.mysteryEncounter.introVisuals!; + introVisuals.spriteConfigs[0].disableAnimation = false; + introVisuals.playAnim(); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Open the chest + const roll = randSeedInt(100); + if (roll > 60) { + // Choose between 2 COMMON / 2 GREAT tier items (40%) + 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 > 40) { + // Choose between 3 ULTRA tier items (20%) + 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 > 36) { + // Choose between 2 ROGUE tier items (4%) + 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 > 35) { + // Choose 1 MASTER tier item (1%) + 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 Pok�mon gets OHKO. Progress with no rewards (35%) + const highestLevelPokemon = getHighestLevelPlayerPokemon( + scene, + true + ); + koPlayerPokemon(scene, highestLevelPokemon); + + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); + // Show which Pokemon was KOed, then leave encounter with no rewards + // Does this synchronously so that game over doesn't happen over result message + await showEncounterText(scene, `${namespace}.option.1.bad`); + leaveEncounterWithoutBattle(scene); + } + }) + .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 00000000000..a17a47f23fe --- /dev/null +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -0,0 +1,339 @@ +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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:partTimer"; + +/** + * Part Timer encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/82 | GitHub Issue #82} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const PartTimerEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.PART_TIMER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .withIntroSpriteConfigs([ + { + spriteKey: "worker_f", + fileRoot: "trainer", + hasShadow: true, + x: -20 + }, + { + spriteKey: "training_gear", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 6, + x: 20, + yShadow: -2 + } + ]) + .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("mysteryEncounter: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("mysteryEncounter: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("mysteryEncounter: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("PRSFX- Horn Drill1"); + scene.playSound("PRSFX- Horn Drill1"); + + scene.time.delayedCall(1000, () => { + scene.playSound("PRSFX- Guillotine2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("PRSFX- Heavy Slam2"); + }); + + scene.time.delayedCall(2500, () => { + scene.playSound("PRSFX- Guillotine2"); + }); +} + +function doDeliverySfx(scene: BattleScene) { + scene.playSound("PRSFX- Accelerock1"); + + scene.time.delayedCall(1500, () => { + scene.playSound("PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("PRSFX- Extremespeed1"); + }); + + scene.time.delayedCall(2250, () => { + scene.playSound("PRSFX- Agility"); + }); +} + +function doSalesSfx(scene: BattleScene) { + scene.playSound("PRSFX- Captivate"); + + scene.time.delayedCall(1500, () => { + scene.playSound("PRSFX- Attract2"); + }); + + scene.time.delayedCall(2000, () => { + scene.playSound("PRSFX- Aurora Veil2"); + }); + + scene.time.delayedCall(3000, () => { + scene.playSound("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 00000000000..a7ac57fff7d --- /dev/null +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -0,0 +1,501 @@ +import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, 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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:safariZone"; + +const TRAINER_THROW_ANIMATION_TIMES = [512, 184, 768]; + +/** + * Safari Zone encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/39 | GitHub Issue #39} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SafariZoneEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive + .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, 2.75)) // 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"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims"); + scene.loadSe("PRSFX- Taunt2", "battle_anims"); + scene.loadAtlas("bait", "mystery-encounters"); + scene.loadAtlas("mud", "mystery-encounters"); + 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`) ?? "", 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.eating`) ?? "", 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`) ?? "", 1000, false ); + } else { + await showEncounterText(scene, getEncounterText(scene, `${namespace}.safari.angry`) ?? "", 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])); + enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(scene.currentBattle.waveIndex, true, false, scene.gameMode)); + scene.currentBattle.enemyParty = []; + pokemon = scene.addEnemyPokemon(enemySpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, false); + + // Roll shiny twice + if (!pokemon.shiny) { + pokemon.trySetShiny(); + } + + // 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[0] = 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") ?? "", 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("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("PRSFX- Bug Bite"); + bait.setFrame("0002.png"); + }, + onLoop: () => { + if (index % 2 === 0) { + scene.playSound("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("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("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("PRSFX- Taunt2"); + }, + onLoop: () => { + scene.playSound("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) { + 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 00000000000..2876ce64c8f --- /dev/null +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -0,0 +1,242 @@ +import { generateModifierType, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { StatusEffect } from "#app/data/status-effect"; +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"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:shadyVitaminDealer"; + +/** + * Shady Vitamin Dealer encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/34 | GitHub Issue #34} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ShadyVitaminDealerEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .withPrimaryPokemonStatusEffectRequirement([StatusEffect.NONE]) // Pokemon must not have status + .withPrimaryPokemonHealthRatioRequirement([0.34, 1]) // Pokemon must have above 1/3rd 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, 2) // Wave scaling money multiplier of 2 + .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 1/3rd 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; + + // Pokemon takes 1/3 max HP damage + applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 3)); + + // Roll for poison (80%) + if (randSeedInt(10) < 8) { + if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) { + // Toxic applied + queueEncounterMessage(scene, `${namespace}.bad_poison`); + } else { + // Pokemon immune or something else prevents status + queueEncounterMessage(scene, `${namespace}.damage_only`); + } + } else { + queueEncounterMessage(scene, `${namespace}.damage_only`); + } + + setEncounterExp(scene, [chosenPokemon.id], 100); + + chosenPokemon.updateInfo(); + }) + .build() + ) + .withOption( + MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 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 above 1/3rd 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 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; + + // Roll for poison (20%) + if (randSeedInt(10) < 2) { + if (chosenPokemon.trySetStatus(StatusEffect.POISON)) { + // Poison applied + queueEncounterMessage(scene, `${namespace}.poison`); + } else { + // Pokemon immune or something else prevents status + queueEncounterMessage(scene, `${namespace}.no_bad_effects`); + } + } else { + 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 00000000000..f819e6cb7e5 --- /dev/null +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -0,0 +1,148 @@ +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"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:slumberingSnorlax"; + +/** + * Sleeping Snorlax encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/103 | GitHub Issue #103} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const SlumberingSnorlaxEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SLUMBERING_SNORLAX) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .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]); + + 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 00000000000..63e7674b05a --- /dev/null +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -0,0 +1,235 @@ +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 { StatChangePhase } from "#app/phases/stat-change-phase"; +import { BattleStat } from "#app/data/battle-stat"; +import { getPokemonNameWithAffix } from "#app/messages"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/119 | GitHub Issue #119} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TeleportingHijinksEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TELEPORTING_HIJINKS) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .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, + y: 4 + } + ]) + .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 }); + setEncounterExp(scene, encounter.selectedOption!.primaryPokemon!.id, 100); + 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 StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.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 00000000000..9604783d3ff --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -0,0 +1,157 @@ +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"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:pokemonSalesman"; + +const MAX_POKEMON_PRICE_MULTIPLIER = 6; + +/** + * Pokemon Salesman encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/36 | GitHub Issue #36} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ThePokemonSalesmanEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_POKEMON_SALESMAN) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) + .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); + } + + 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 00000000000..0b4e60a16e4 --- /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, { PlayerPokemon, 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 { BattleStat } from "#app/data/battle-stat"; +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 { StatChangePhase } from "#app/phases/stat-change-phase"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theStrongStuff"; + +/** + * The Strong Stuff encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/54 | GitHub Issue #54} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheStrongStuffEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withHideWildIntroMessage(true) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "berry_juice", + fileRoot: "items", + hasShadow: true, + isItem: true, + scale: 1.5, + x: -15, + y: 3, + disableAnimation: true + }, + { + spriteKey: Species.SHUCKLE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + scale: 1.5, + 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, + mysteryEncounterData: new MysteryEncounterPokemonData(1.5), + nature: Nature.BOLD, + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierConfigs: [ + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType + }, + { + modifierType: 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 StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF, BattleStat.SPDEF], 2)); + } + } + ], + }; + + encounter.enemyPartyConfigs = [config]; + + loadCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]); + + 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); + }); + + // -20 to all base stats of highest BST, +10 to all base stats of rest of party + // Get highest BST mon + const party = scene.getParty(); + let highestBst: PlayerPokemon | null = null; + let statTotal = 0; + for (const pokemon of party) { + if (!highestBst) { + highestBst = pokemon; + statTotal = pokemon.getSpeciesForm().getBaseStatTotal(); + continue; + } + + const total = pokemon.getSpeciesForm().getBaseStatTotal(); + if (total > statTotal) { + highestBst = pokemon; + statTotal = total; + } + } + + if (!highestBst) { + highestBst = party[0]; + } + + modifyPlayerPokemonBST(highestBst, -20); + for (const pokemon of party) { + if (highestBst.id === pokemon.id) { + continue; + } + + modifyPlayerPokemonBST(pokemon, 10); + } + + encounter.setDialogueToken("highBstPokemon", highestBst.getNameToRender()); + await showEncounterText(scene, `${namespace}.option.1.selected_2`, 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 00000000000..08940818b9b --- /dev/null +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -0,0 +1,497 @@ +import { EnemyPartyConfig, generateModifierType, 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 } 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"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:theWinstrateChallenge"; + +/** + * The Winstrate Challenge encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/136 | GitHub Issue #136} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TheWinstrateChallengeEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_WINSTRATE_CHALLENGE) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withSceneWaveRangeRequirement(80, 180) + .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: "trainerNames:victor", + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Spawn 5 trainer battles back to back with Macho Brace in rewards + // scene.currentBattle.mysteryEncounter.continuousEncounter = true; + 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`); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MYSTERY_ENCOUNTER_MACHO_BRACE], fillRemaining: false }); + encounter.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, false, MysteryEncounterMode.TRAINER_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: [ + { + modifierType: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.FLAME_ORB) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.PSYCHIC]) as PokemonHeldItemModifierType, + stackCount: 1, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.HP]) as PokemonHeldItemModifierType, + stackCount: 4, + isTransferable: false + }, + { + modifierType: 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: [ + { + modifierType: 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: [ + { + modifierType: 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: [ + { + modifierType: 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: [ + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.GANLON]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.STARF]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SALAC]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LUM]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LANSAT]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.LIECHI]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.PETAYA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType, + stackCount: 2, + }, + { + modifierType: 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: [ + { + modifierType: 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: [ + { + modifierType: 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: [ + { + modifierType: 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 00000000000..19e8fc50136 --- /dev/null +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -0,0 +1,415 @@ +import { Ability, allAbilities } from "#app/data/ability"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, 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 { getStatName } from "#app/data/pokemon-stat"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { AbilityAttr } from "#app/system/game-data"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +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 } 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"; + +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:trainingSession"; + +/** + * Training Session encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/43 | GitHub Issue #43} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrainingSessionEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRAINING_SESSION) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .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", + getStatName(ivToChange.index) ?? "" + ); + } else { + encounter.setDialogueToken( + "stat2", + getStatName(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 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() + ) + .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 = new Array(abilityCount) + .fill(null) + .map((val, i) => allAbilities[speciesForm.getAbility(i)]); + return abilities.map((ability: Ability, index) => { + 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: () => { + scene.ui.showText(ability.description); + }, + }; + 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 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() + ) + .build(); + +function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig { + playerPokemon.resetSummonData(); + + // Passes modifiers by reference + modifiers.value = scene.findModifiers((m) => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === playerPokemon.id) as PokemonHeldItemModifier[]; + const modifierConfigs = modifiers.value.map((mod) => { + return { + modifierType: mod.type + }; + }) 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 00000000000..d14f3fe6441 --- /dev/null +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -0,0 +1,220 @@ +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"; + +/** 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/AsdarDevelops/PokeRogue-Events/issues/74 | GitHub Issue #74} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const TrashToTreasureEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRASH_TO_TREASURE) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) + .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 }), 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 }), undefined, true); +} + +async function doGarbageDig(scene: BattleScene) { + scene.playSound("PRSFX- Dig2"); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => { + scene.playSound("PRSFX- Dig2"); + scene.playSound("PRSFX- Venom Drench", { volume: 2 }); + }); + scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME * 2, () => { + scene.playSound("PRSFX- Dig2"); + }); +} 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 00000000000..d4ec3ab3c04 --- /dev/null +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -0,0 +1,541 @@ +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 Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils"; +import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species"; +import { HiddenAbilityRateBoosterModifier, PokemonBaseStatTotalModifier, 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 { Stat } from "#app/data/pokemon-stat"; +import i18next from "#app/plugins/i18n"; +import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; +import { getLevelTotalExp } from "#app/data/exp"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:weirdDream"; + +/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, Urshifu, the Poison Chain trio, Ogerpon */ +const excludedPokemon = [ + 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, +]; + +/** + * Weird Dream encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/137 | GitHub Issue #137} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const WeirdDreamEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withIntroSpriteConfigs([ + { + spriteKey: "girawitch", + fileRoot: "mystery-encounters", + hasShadow: false, + y: 4 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withSceneWaveRangeRequirement(10, 180) + .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 hasPokemonBstHigherThan600 = false; + // Only 1 other Pokemon can be transformed into BST between 570-600 + let hasPokemonBstBetween570And600 = 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 = getOriginalBst(scene, removed); + let newBstRange; + if (i < 2) { + newBstRange = [90, 110]; + } else { + newBstRange = [40, 50]; + } + + const newSpecies = getTransformedSpecies(bst, newBstRange, hasPokemonBstHigherThan600, hasPokemonBstBetween570And600, alreadyUsedSpecies); + + const newSpeciesBst = newSpecies.getBaseStatTotal(); + if (newSpeciesBst > 600) { + hasPokemonBstHigherThan600 = true; + } + if (newSpeciesBst <= 600 && newSpeciesBst >= 570) { + hasPokemonBstBetween570And600 = 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() <= 570 || 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.mysteryEncounterData) { + newPokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, undefined, undefined, newTypes); + } else { + newPokemon.mysteryEncounterData.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, lowest of Atk/SpAtk, and lowest of Def/SpDef + if (newPokemon.getSpeciesForm().getBaseStatTotal() <= 450) { + 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 getOriginalBst(scene: BattleScene, pokemon: Pokemon) { + const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); + scene.applyModifiers(PokemonBaseStatTotalModifier, true, pokemon, baseStats); + if (pokemon.fusionSpecies) { + const fusionBaseStats = pokemon.getFusionSpeciesForm().baseStats; + for (let s = 0; s < pokemon.stats.length; s++) { + baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); + } + } else if (scene.gameMode.isSplicedOnly) { + for (let s = 0; s < pokemon.stats.length; s++) { + baseStats[s] = Math.ceil(baseStats[s] / 2); + } + } + return baseStats.reduce((a, b) => a + b, 0); +} + +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 < 570 || speciesBst > 600)) && + (!hasPokemonBstHigherThan600 || speciesBst <= 600); + return bstInRange && validBst && !excludedPokemon.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-data.ts b/src/data/mystery-encounters/mystery-encounter-data.ts new file mode 100644 index 00000000000..dc96be2581f --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-data.ts @@ -0,0 +1,16 @@ +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 MysteryEncounterData { + encounteredEvents: [MysteryEncounterType, MysteryEncounterTier][] = []; + encounterSpawnChance: number = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; + nextEncounterQueue: [MysteryEncounterType, integer][] = []; + + constructor(flags: MysteryEncounterData | null) { + if (!isNullOrUndefined(flags)) { + Object.assign(this, flags); + } + } +} 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 00000000000..34f5f4eb169 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -0,0 +1,74 @@ +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?: [...OptionTextDisplay[]]; // Options array with minimum 2 options +} + +/** + * 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 00000000000..086706075e7 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -0,0 +1,260 @@ +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 * as Utils from "#app/utils"; +import { Type } from "../type"; +import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; +import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; +import { isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; + + +export type OptionPhaseCallback = (scene: BattleScene) => Promise; + +/** + * Used by {@link MysteryEncounterOptionBuilder} class to define required/optional properties on the {@link MysteryEncounterOption} class when building. + * + * Should ONLY contain properties that are necessary for {@link MysteryEncounterOption} construction. + * Post-construct and flag data properties are defined in the {@link 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 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 ?? []; + } + + hasRequirements() { + return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0; + } + + meetsRequirements(scene: BattleScene) { + return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) && + this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) && + this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); + } + + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon) { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene) { + if (!this.primaryPokemonRequirements) { + return true; + } + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.primaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + if (req instanceof EncounterPokemonRequirement) { + 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) { + 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 supporting 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 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; + } + } + + meetsSupportingRequirementAndSupportingPokemonSelected(scene: BattleScene) { + if (!this.secondaryPokemonRequirements) { + this.secondaryPokemon = []; + return true; + } + + let qualified: PlayerPokemon[] = scene.getParty(); + for (const req of this.secondaryPokemonRequirements) { + if (req.meetsRequirement(scene)) { + if (req instanceof EncounterPokemonRequirement) { + 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 }); + } + + 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)); + } + + withPreOptionPhase(onPreOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPreOptionPhase: onPreOptionPhase }); + } + + withOptionPhase(onOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onOptionPhase: onOptionPhase }); + } + + withPostOptionPhase(onPostOptionPhase: OptionPhaseCallback): this & Required> { + return Object.assign(this, { onPostOptionPhase: onPostOptionPhase }); + } + + 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)); + } + + 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 }); + } + + /** + * Se 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 00000000000..4e933304c5e --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts @@ -0,0 +1,16 @@ +import { Abilities } from "#enums/abilities"; +import { Type } from "#app/data/type"; + +export class MysteryEncounterPokemonData { + public spriteScale: number | undefined; + public ability: Abilities | undefined; + public passive: Abilities | undefined; + public types: Type[]; + + constructor(spriteScale?: number, ability?: Abilities, passive?: Abilities, types?: Type[]) { + this.spriteScale = spriteScale; + this.ability = ability; + this.passive = passive; + this.types = 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 00000000000..042f967a23d --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -0,0 +1,993 @@ +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"; + +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 { + abstract meetsRequirement(scene: BattleScene): boolean; + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationSceneRequirement extends EncounterSceneRequirement { + orRequirements: EncounterSceneRequirement[]; + + constructor(... orRequirements: EncounterSceneRequirement[]) { + super(); + this.orRequirements = orRequirements; + } + + meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + 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; + + 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[]; + + 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; + } + + meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + + queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + for (const req of this.orRequirements) { + const result = req.queryParty(partyPokemon); + if (result?.length > 0) { + return result; + } + } + + return []; + } + + 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; + } + + meetsRequirement(scene: BattleScene): boolean { + return scene.mysteryEncounterData.encounteredEvents.some(e => e[0] === this.previousEncounterRequirement); + } + + getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["previousEncounter", scene.mysteryEncounterData.encounteredEvents.find(e => e[0] === 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; + } + + 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; + } + + 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 - number[], the allowed modulus results + * @param modulusValue - number, 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; + } + + meetsRequirement(scene: BattleScene): boolean { + return this.waveModuli.includes(scene.currentBattle.waveIndex % this.modulusValue); + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const timeOfDay = scene.arena?.getTimeOfDay(); + if (!isNullOrUndefined(timeOfDay) && this?.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) { + return false; + } + + return true; + } + + 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]; + } + + 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; + } + + 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 - [min, max] + * @param excludeFainted + */ + constructor(partySizeRange: [number, number], excludeFainted: boolean) { + super(); + this.partySizeRange = partySizeRange; + this.excludeFainted = excludeFainted; + } + + 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; + } + + 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]; + } + + 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; + } + + 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; + } + + 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); + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredSpecies?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredNature?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + let partyPokemon = scene.getParty(); + + if (isNullOrUndefined(partyPokemon) || this?.requiredType?.length < 0) { + return false; + } + + if (!this.excludeFainted) { + partyPokemon = partyPokemon.filter((pokemon) => !pokemon.isFainted()); + } + + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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 EvolutionTargetSpeciesRequirement extends EncounterPokemonRequirement { + requiredEvolutionTargetSpecies: Species[]; + minNumberOfPokemon:number; + invertQuery:boolean; + + constructor(evolutionTargetSpecies: Species | Species[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + super(); + this.minNumberOfPokemon = minNumberOfPokemon; + this.invertQuery = invertQuery; + this.requiredEvolutionTargetSpecies = Array.isArray(evolutionTargetSpecies) ? evolutionTargetSpecies : [evolutionTargetSpecies]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionTargetSpecies?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + if (!this.invertQuery) { + return partyPokemon.filter((pokemon) => this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution()?.speciesId === evolutionTargetSpecies).length > 0); + } else { + // for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionTargetSpeciess + return partyPokemon.filter((pokemon) => this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution()?.speciesId === evolutionTargetSpecies).length === 0); + } + } + + getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] { + const evos = this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution().speciesId === evolutionTargetSpecies); + if (evos.length > 0) { + return ["evolution", Species[evos[0]]]; + } + return ["evolution", ""]; + } + +}*/ + +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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredAbilities?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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]; + } + + 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; + } + + 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; + } + }); + }); + } + } + + 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]; + } + + 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; + } + } + + 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); + } + } + + 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]; + } + + 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; + } + + 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); + } + } + + 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]; + } + + meetsRequirement(scene: BattleScene): boolean { + const partyPokemon = scene.getParty(); + if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) { + return false; + } + return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; + } + + 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); + } + } + + 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 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; + } + + 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; + } + + 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]); + } + } + + 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; + } + + 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; + } + + 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]); + } + } + + 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; + } + + meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required level 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; + } + + 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]); + } + } + + 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; + } + + meetsRequirement(scene: BattleScene): boolean { + // Party Pokemon inside required friendship 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; + } + + 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]); + } + } + + getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { + return ["weight", pokemon?.getWeight().toString() ?? ""]; + } +} + + diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts new file mode 100644 index 00000000000..3db97bf1f98 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -0,0 +1,863 @@ +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 { EncounterAnim } from "#app/data/battle-anims"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; + +export interface EncounterStartOfBattleEffect { + sourcePokemon?: Pokemon; + sourceBattlerIndex?: BattlerIndex; + targets: BattlerIndex[]; + move: PokemonMove; + ignorePp: boolean; + followUp?: boolean; +} + +/** + * Used by {@link MysteryEncounterBuilder} class to define required/optional properties on the {@link MysteryEncounter} class when building. + * + * Should ONLY contain properties that are necessary for {@link MysteryEncounter} construction. + * Post-construct and flag data properties are defined in the {@link MysteryEncounter} class itself. + */ +export interface IMysteryEncounter { + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + encounterTier: MysteryEncounterTier; + encounterAnimations?: EncounterAnim[]; + hideBattleIntroMessage: boolean; + autoHideIntroVisuals: boolean; + enterIntroVisualsFromRight: boolean; + catchAllowed: boolean; + continuousEncounter: boolean; + maxAllowedEncounters: number; + + 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 { + /** + * Required params + */ + encounterType: MysteryEncounterType; + options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]]; + spriteConfigs: MysteryEncounterSpriteConfig[]; + /** + * 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 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; + + + /** + * 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; + /** 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[]; + + /** + * 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; + + /** + * 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; + /** + * 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; + } + + /** + * Checks if the current scene state meets the requirements for the 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) { + 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; + } + + pokemonMeetsPrimaryRequirements(scene: BattleScene, pokemon: Pokemon) { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + + meetsPrimaryRequirementAndPrimaryPokemonSelected(scene: BattleScene): boolean { + if (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) { + 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; + } + } + + meetsSecondaryRequirementAndSecondaryPokemonSelected(scene: BattleScene): boolean { + if (!this.secondaryPokemonRequirements) { + 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) { + 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) { + 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]); + } + } + } + } + } + } + + setDialogueToken(key: string, value: string): void { + this.dialogueTokens[key] = value; + } + + /** + * If an encounter uses {@link MysteryEncounterMode.continuousEncounter}, + * should rely on this value for seed offset instead of wave index. + * + * This offset is incremented for each new {@link 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 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; + 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 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 hasDexProgress - + * @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 }); + } + + /** + * 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 }); + } + + /** + * 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 00000000000..79a3b6ed635 --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -0,0 +1,325 @@ +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"; + +// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 +export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; +export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 5; +export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 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 +]; + +/** + * 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 +]; + +/** + * 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; + + // 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 00000000000..9a0447d816d --- /dev/null +++ b/src/data/mystery-encounters/requirements/can-learn-move-requirement.ts @@ -0,0 +1,93 @@ +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]; + + const { excludeLevelMoves, excludeTmMoves, excludeEggMoves, includeFainted, minNumberOfPokemon, invertQuery } = options; + + this.excludeLevelMoves = excludeLevelMoves ?? false; + this.excludeTmMoves = excludeTmMoves ?? false; + this.excludeEggMoves = excludeEggMoves ?? false; + this.includeFainted = includeFainted ?? false; + this.minNumberOfPokemon = minNumberOfPokemon ?? 1; + this.invertQuery = 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 00000000000..235c9910ef0 --- /dev/null +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -0,0 +1,103 @@ +import { Moves } from "#enums/moves"; +import { Abilities } from "#enums/abilities"; + +export const STEALING_MOVES = [ + Moves.PLUCK, + Moves.COVET, + Moves.KNOCK_OFF, + Moves.THIEF, + Moves.TRICK, + Moves.SWITCHEROO +]; + +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.KNOCK_OFF +]; + +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 +]; + +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 +]; + +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, +]; + +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 00000000000..2ed2696182b --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -0,0 +1,77 @@ +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"; + +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; +} + +function getTextWithDialogueTokens(scene: BattleScene, keyOrString?: string): string | null { + if (isNullOrUndefined(keyOrString)) { + return null; + } + + const tokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; + // @ts-ignore + if (i18next.exists(keyOrString, tokens)) { + const stringArray = [`${keyOrString}`] as any; + stringArray.raw = [`${keyOrString}`]; + // @ts-ignore + return i18next.t(stringArray, 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 prompt + * @param callbackDelay + */ +export function showEncounterText(scene: BattleScene, contentKey: string, callbackDelay: number = 0, prompt: boolean = true): Promise { + return new Promise(resolve => { + const text: string | null = getEncounterText(scene, contentKey); + scene.ui.showText(text ?? "", null, () => resolve(), callbackDelay, prompt); + }); +} + +/** + * Will display a dialogue (with speaker title) in UI with injected encounter data tokens + * @param scene + * @param textContentKey + * @param speakerContentKey + * @param callbackDelay + */ +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, 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 ?? "", null, () => 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 00000000000..f2ffcb964a0 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -0,0 +1,1012 @@ +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 { 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 { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; +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 Overrides from "#app/overrides"; +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 { ExpPhase } from "#app/phases/exp-phase"; +import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-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("GEN8- Exclaim", { volume: 0.7 }); +} + +export interface EnemyPokemonConfig { + species: PokemonSpecies; + isBoss: boolean; + bossSegments?: number; + bossSegmentModifier?: number; // Additive to the determined segment number + mysteryEncounterData?: MysteryEncounterPokemonData; + formIndex?: number; + abilityIndex?: number; + level?: number; + gender?: Gender; + passive?: boolean; + moveSet?: Moves[]; + nature?: Nature; + ivs?: [integer, integer, integer, integer, integer, integer]; + 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 { + levelAdditiveMultiplier?: number; // Formula for enemy: level += waveIndex / 10 * levelAdditive + doubleBattle?: boolean; + trainerType?: TrainerType; // Generates trainer battle solely off trainer type + trainerConfig?: TrainerConfig; // More customizable option for configuring trainer battle + pokemonConfigs?: EnemyPokemonConfig[]; + female?: boolean; // True for female trainer, false for male + disableSwitch?: boolean; // True will prevent player from switching +} + +/** + * 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 => enemyPokemon.destroy()); + 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.mysteryEncounterData)) { + enemyPokemon.mysteryEncounterData = config.mysteryEncounterData!; + } + + // 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!); + if (customModifierTypes) { + 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 - Battle 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("buy"); + } + if (showMessage) { + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounter: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 { + const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier)!; + let result: ModifierType = modifierTypes[modifierId]?.(); + + // Populates item id and tier (order matters) + result = result + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); + + const generatedResult = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; + return generatedResult ?? 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 { + const result = generateModifierType(scene, modifier, pregenArgs); + return new ModifierTypeOption(result, 0); +} + +/** + * 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 to train + scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, 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(Mode.MYSTERY_ENCOUNTER); + resolve(false); + return true; + }, + onHover: () => { + scene.ui.showText(i18next.t("mysteryEncounter:cancel_option")); + } + }); + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + supportHover: true + }; + 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); + }); +} + +/** + * 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 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: integer | integer[], baseExpValue: number, useWaveIndex: boolean = true) { + const participantIds = Array.isArray(participantId) ? participantId : [participantId]; + + scene.currentBattle.mysteryEncounter.doEncounterExp = (scene: BattleScene) => { + const party = scene.getParty(); + const expShareModifier = scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; + const expBalanceModifier = scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; + const multipleParticipantExpBonusModifier = scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; + const nonFaintedPartyMembers = party.filter(p => p.hp); + const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < scene.getMaxExpLevel()); + const partyMemberExp: number[] = []; + // EXP value calculation is based off Pokemon.getExpValue + let expValue = Math.floor(baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1) / 5 + 1); + + if (participantIds?.length > 0) { + if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + expValue = Math.floor(expValue * 1.5); + } + for (const partyMember of nonFaintedPartyMembers) { + const pId = partyMember.id; + const participated = participantIds.includes(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.length); + if (participantIds.length > 1 && multipleParticipantExpBonusModifier) { + expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + } + } else if (expShareModifier) { + expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.length; + } + if (partyMember.pokerus) { + expMultiplier *= 1.5; + } + if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { + expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; + } + const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); + 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]); + scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(scene, partyMemberIndex, exp)); + } + } + } + + 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 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.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; + } +} + +/** + * 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 targetEncountersPerRun = 15; // AVERAGE_ENCOUNTERS_PER_RUN_TARGET + 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]; + 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, integer])[]) + .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 = targetEncountersPerRun / (180 - 10) * i; + const currentRunDiffFromAvg = expectedEncountersByFloor - numEncounters.reduce((a, b) => a + b); + const favoredEncounterRate = encounterRate + currentRunDiffFromAvg * 5; + + if (roll < favoredEncounterRate) { + encounterRate = baseSpawnWeight; + + // Calculate encounter rarity + // Common / Uncommon / Rare / Super Rare (base is out of 128) + const tierWeights = [64, 40, 21, 3]; + + // Adjust tier weights by currently encountered events (pity system that lowers odds of multiple common/uncommons) + 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 00000000000..7ee670b4828 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -0,0 +1,637 @@ +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 { BattlerIndex } from "#app/battle"; +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 { Stat } from "#enums/stat"; +import { VictoryPhase } from "#app/phases/victory-phase"; + +export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { 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 }; +} + +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 - 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: Stat, 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: handle special cases like wonder-guard/ninjask + * @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 pokemong 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("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("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("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("pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("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)); + } + }); + } + }); + }); + }); +} + +function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("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, BattlerIndex.ENEMY, 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: integer, _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(); + } + }); +} + +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(); + } + }); + } +} + +export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + await new Promise(resolve => { + scene.playSound("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() }), 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +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() }), 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} 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 00000000000..9da36ee6846 --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -0,0 +1,332 @@ +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(success => { + 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); + } + }); + }); + } + }); + }); + }); + }); + }); + } + }); + } + }); + }); +} + +function doSpiralUpward(scene: BattleScene, transformationBaseBg, transformationContainer, 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++; + } + } + }); +} + +function doArcDownward(scene: BattleScene, transformationBaseBg, transformationContainer, 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++; + } + } + }); +} + +function doCycle(scene: BattleScene, l: number, lastCycle: integer, pokemonTintSprite, pokemonEvoTintSprite): 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); + } + } + }); + }); +} + +function doCircleInward(scene: BattleScene, transformationBaseBg, transformationContainer, 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++; + } + }); +} + +function doSpiralUpwardParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, 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(); +} + +function doArcDownParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, 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(); +} + +function doCircleInwardParticle(scene: BattleScene, trigIndex: integer, speed: integer, transformationBaseBg, transformationContainer, 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-species.ts b/src/data/pokemon-species.ts index dc12ca402cd..db4f0a555ac 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -238,6 +238,14 @@ export abstract class PokemonSpeciesForm { return false; } + /** + * Gets the BST for the species + * @returns The species' BST. + */ + getBaseStatTotal(): integer { + return this.baseStats.reduce((i, n) => n + i); + } + /** * Gets the species' base stat amount for the given stat. * @param stat The desired stat. diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 5f47ce42a62..507b7bd5139 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"; @@ -628,6 +628,44 @@ 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.setTitle(title); + 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. @@ -925,6 +963,63 @@ export class TrainerConfig { } }); } + + copy(): TrainerConfig { + let copy = new TrainerConfig(this.trainerType); + copy = this.trainerTypeDouble ? copy.setDoubleTrainerType(this.trainerTypeDouble) : copy; + copy = this.name ? copy.setName(this.name) : copy; + copy = this.hasGenders ? copy.setHasGenders(this.nameFemale, this.femaleEncounterBgm) : copy; + copy = this.hasDouble ? copy.setHasDouble(this.nameDouble, this.doubleEncounterBgm) : copy; + copy = this.title ? copy.setTitle(this.title) : copy; + copy = this.titleDouble ? copy.setDoubleTitle(this.titleDouble) : copy; + copy = this.hasCharSprite ? copy.setHasCharSprite() : copy; + copy = this.doubleOnly ? copy.setDoubleOnly() : copy; + copy = this.moneyMultiplier ? copy.setMoneyMultiplier(this.moneyMultiplier) : copy; + copy = this.isBoss ? copy.setBoss() : copy; + copy = this.hasStaticParty ? copy.setStaticParty() : copy; + copy = this.useSameSeedForAllMembers ? copy.setUseSameSeedForAllMembers() : copy; + copy = this.battleBgm ? copy.setBattleBgm(this.battleBgm) : copy; + copy = this.encounterBgm ? copy.setEncounterBgm(this.encounterBgm) : copy; + copy = this.victoryBgm ? copy.setVictoryBgm(this.victoryBgm) : copy; + copy = this.genModifiersFunc ? copy.setGenModifiersFunc(this.genModifiersFunc) : copy; + + if (this.modifierRewardFuncs) { + // Clones array instead of passing ref + copy.modifierRewardFuncs = this.modifierRewardFuncs.slice(0); + } + + if (this.partyTemplates) { + copy.partyTemplates = this.partyTemplates.slice(0); + } + + copy = this.partyTemplateFunc ? copy.setPartyTemplateFunc(this.partyTemplateFunc) : copy; + + if (this.partyMemberFuncs) { + Object.keys(this.partyMemberFuncs).forEach((index) => { + copy = copy.setPartyMemberFunc(parseInt(index, 10), this.partyMemberFuncs[index]); + }); + } + + copy = this.speciesPools ? copy.setSpeciesPools(this.speciesPools) : copy; + copy = this.speciesFilter ? copy.setSpeciesFilter(this.speciesFilter) : copy; + if (this.specialtyTypes) { + copy.specialtyTypes = this.specialtyTypes.slice(0); + } + + copy.encounterMessages = this.encounterMessages?.slice(0); + copy.victoryMessages = this.victoryMessages?.slice(0); + copy.defeatMessages = this.defeatMessages?.slice(0); + + copy.femaleEncounterMessages = this.femaleEncounterMessages?.slice(0); + copy.femaleVictoryMessages = this.femaleVictoryMessages?.slice(0); + copy.femaleDefeatMessages = this.femaleDefeatMessages?.slice(0); + + copy.doubleEncounterMessages = this.doubleEncounterMessages?.slice(0); + copy.doubleVictoryMessages = this.doubleVictoryMessages?.slice(0); + copy.doubleDefeatMessages = this.doubleDefeatMessages?.slice(0); + + return copy; + } } let t = 0; @@ -1862,4 +1957,152 @@ export const trainerConfigs: TrainerConfigs = { p.generateAndPopulateMoveset(); p.pokeball = PokeballType.MASTER_BALL; })), + // TODO: use signature species? + [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).setName("Victor").setTitle("The Winstrates") + .setMoneyMultiplier(1) // The Winstrate trainers have total money multiplier of 6 + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VICTORIA]: new TrainerConfig(++t).setName("Victoria").setTitle("The Winstrates") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG_ONE_STRONG), + [TrainerType.VIVI]: new TrainerConfig(++t).setName("Vivi").setTitle("The Winstrates") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.TWO_AVG_ONE_STRONG), + [TrainerType.VICKY]: new TrainerConfig(++t).setName("Vicky").setTitle("The Winstrates") + .setMoneyMultiplier(1) + .setPartyTemplates(trainerPartyTemplates.ONE_AVG), + [TrainerType.VITO]: new TrainerConfig(++t).setName("Vito").setTitle("The Winstrates") + .setMoneyMultiplier(2) + .setPartyTemplates(new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE), new TrainerPartyTemplate(2, PartyMemberStrength.STRONG))) }; diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index b133b442801..cbe12de852b 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -69,5 +69,6 @@ export enum BattlerTagType { GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA", GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU", BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING", - SHELL_TRAP = "SHELL_TRAP" + SHELL_TRAP = "SHELL_TRAP", + MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", } diff --git a/src/enums/mystery-encounter-mode.ts b/src/enums/mystery-encounter-mode.ts new file mode 100644 index 00000000000..3acab7b4797 --- /dev/null +++ b/src/enums/mystery-encounter-mode.ts @@ -0,0 +1,8 @@ +export enum MysteryEncounterMode { + DEFAULT, + TRAINER_BATTLE, + WILD_BATTLE, + /** Enables wild boss music during encounter */ + BOSS_BATTLE, + 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 00000000000..a994c30581b --- /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 00000000000..b3924b2ff9d --- /dev/null +++ b/src/enums/mystery-encounter-tier.ts @@ -0,0 +1,10 @@ +/** + * Enum values are base spawn weights of each tier + */ +export enum MysteryEncounterTier { + COMMON = 64, + GREAT = 40, + ULTRA = 21, + 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 00000000000..b36a2c4ce41 --- /dev/null +++ b/src/enums/mystery-encounter-type.ts @@ -0,0 +1,28 @@ +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, // Might be generalized later on + 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 +} diff --git a/src/enums/trainer-type.ts b/src/enums/trainer-type.ts index 1d4d9579ee3..37c5bb5f20b 100644 --- a/src/enums/trainer-type.ts +++ b/src/enums/trainer-type.ts @@ -1,223 +1,233 @@ 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, - ROCKET_BOSS_GIOVANNI_1, - ROCKET_BOSS_GIOVANNI_2, - MAXIE, - MAXIE_2, - ARCHIE, - ARCHIE_2, - CYRUS, - CYRUS_2, - GHETSIS, - GHETSIS_2, - LYSANDRE, - LYSANDRE_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, + ROCKET_BOSS_GIOVANNI_1, + ROCKET_BOSS_GIOVANNI_2, + MAXIE, + MAXIE_2, + ARCHIE, + ARCHIE_2, + CYRUS, + CYRUS_2, + GHETSIS, + GHETSIS_2, + LYSANDRE, + LYSANDRE_2, + BUCK, + CHERYL, + MARLEY, + MIRA, + RILEY, + VICTOR, + VICTORIA, + VIVI, + VICKY, + VITO, - 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 7622b9a014f..d64b59c8212 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, 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 00000000000..2adf26bb2b3 --- /dev/null +++ b/src/field/mystery-encounter-intro.ts @@ -0,0 +1,411 @@ +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); + }); + } + + 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(); + } + }); + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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); + } + } + + tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void { + const tintSprites = this.getTintSprites(); + tintSprites.map(tintSprite => { + this.tint(tintSprite, color, alpha, duration, ease); + }); + } + + 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); + } + } + + 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); + } +} + +export default interface MysteryEncounterIntroVisuals { + scene: BattleScene +} diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 756ee2a44cd..0bcb4725f4c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -10,7 +10,7 @@ import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; import { Stat } from "../data/pokemon-stat"; -import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; +import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier, PokemonBaseStatTotalModifier, PokemonIncrementingStatModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; @@ -59,6 +59,7 @@ import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase. import { StatChangePhase } from "#app/phases/stat-change-phase.js"; import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js"; import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export enum FieldPosition { CENTER, @@ -112,6 +113,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleData: PokemonBattleData; public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + public mysteryEncounterData: 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; @@ -195,6 +200,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.fusionVariant = dataSource.fusionVariant || 0; this.fusionGender = dataSource.fusionGender; this.fusionLuck = dataSource.fusionLuck; + this.mysteryEncounterData = dataSource.mysteryEncounterData; } else { this.id = Utils.randSeedInt(4294967296); this.ivs = ivs || Utils.getIvsFromId(this.id); @@ -242,6 +248,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.fusionLuck = this.luck; + this.mysteryEncounterData = new MysteryEncounterPokemonData(); } this.generateName(); @@ -314,6 +321,15 @@ 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; @@ -558,6 +574,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?.mysteryEncounterData?.spriteScale) { + return this.mysteryEncounterData.spriteScale; } return 1; } @@ -761,6 +779,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.stats = [ 0, 0, 0, 0, 0, 0 ]; } const baseStats = this.getSpeciesForm().baseStats.slice(0); + this.scene.applyModifiers(PokemonBaseStatTotalModifier, this.isPlayer(), this, baseStats); if (this.fusionSpecies) { const fusionBaseStats = this.getFusionSpeciesForm().baseStats; for (let s = 0; s < this.stats.length; s++) { @@ -800,6 +819,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.stats[s] = value; } + this.scene.applyModifier(PokemonIncrementingStatModifier, this.isPlayer(), this, this.stats); } getNature(): Nature { @@ -954,7 +974,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.mysteryEncounterData?.types && this.mysteryEncounterData.types.length > 0) { + // "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters + this.mysteryEncounterData.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); @@ -1015,6 +1038,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } + if (this.mysteryEncounterData?.ability) { + return allAbilities[this.mysteryEncounterData.ability]; + } if (this.isFusion()) { return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } @@ -1039,6 +1065,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 (this.mysteryEncounterData?.passive) { + return allAbilities[this.mysteryEncounterData.passive]; + } let starterSpeciesId = this.species.speciesId; while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { @@ -1486,6 +1515,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; @@ -1821,7 +1859,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}`, @@ -4282,6 +4320,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: integer[]; diff --git a/src/field/trainer.ts b/src/field/trainer.ts index 68ebabbbe23..52bbd063ba4 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 f5dadad6f1b..ac2132d5395 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -29,6 +29,7 @@ interface GameModeConfig { hasRandomBosses?: boolean; isSplicedOnly?: boolean; isChallenge?: boolean; + hasMysteryEncounters?: boolean; } export class GameMode implements GameModeConfig { @@ -45,6 +46,7 @@ export class GameMode implements GameModeConfig { public isChallenge: boolean; public challenges: Challenge[]; public battleConfig: FixedBattleConfigs; + public hasMysteryEncounters: boolean; constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) { this.modeId = modeId; @@ -336,7 +338,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 +346,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 00000000000..304de72f01b --- /dev/null +++ b/src/interfaces/held-modifier-config.ts @@ -0,0 +1,7 @@ +import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; + +export default interface HeldModifierConfig { + modifierType: PokemonHeldItemModifierType; + stackCount?: number; + isTransferable?: boolean; +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index f4aa12c56c6..f915d01d845 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"; @@ -278,6 +279,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"); @@ -354,6 +358,7 @@ export class LoadingScene extends SceneBase { initMoves(); initAbilities(); initChallenges(); + initMysteryEncounters(); } loadLoadingScreen() { diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index e5ca8f77bb1..80116faf5e7 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", @@ -48,6 +52,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}}!", @@ -94,5 +99,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/config.ts b/src/locales/en/config.ts index f1827b5152d..0752a393711 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -58,6 +58,7 @@ 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 { mysteryEncounter } from "#app/locales/en/mystery-encounter"; export const enConfig = { ability, @@ -119,5 +120,6 @@ export const enConfig = { partyUiHandler, modifierSelectUiHandler, moveTriggers, - runHistory + runHistory, + mysteryEncounter: mysteryEncounter }; diff --git a/src/locales/en/dialogue-female.json b/src/locales/en/dialogue-female.json index 6be1c7586b6..2268b7ccebd 100644 --- a/src/locales/en/dialogue-female.json +++ b/src/locales/en/dialogue-female.json @@ -787,6 +787,106 @@ "1": "Fools with no vision will continue to befoul this beautiful world." } }, + "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!" + }, + "defeat": { + "1": "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." + }, + "defeat": { + "1": "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." + }, + "defeat": { + "1": "... 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." + }, + "defeat": { + "1": "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...\n $It's great how Trainers can interact." + }, + "defeat": { + "1": "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?\n $You must be quite the trainer to beat my husband, though.\n $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!\n $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!\n $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!\n $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/dialogue-male.json b/src/locales/en/dialogue-male.json index bf0612539d3..f10fc11fa7c 100644 --- a/src/locales/en/dialogue-male.json +++ b/src/locales/en/dialogue-male.json @@ -787,6 +787,106 @@ "1": "Fools with no vision will continue to befoul this beautiful world." } }, + "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!" + }, + "defeat": { + "1": "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." + }, + "defeat": { + "1": "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." + }, + "defeat": { + "1": "... 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." + }, + "defeat": { + "1": "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...\n $It's great how Trainers can interact." + }, + "defeat": { + "1": "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?\n $You must be quite the trainer to beat my husband, though.\n $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!\n $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!\n $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!\n $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 8a5e061d883..d6b352fca1e 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-type.json b/src/locales/en/modifier-type.json index ed1ef900878..886b93a19f9 100644 --- a/src/locales/en/modifier-type.json +++ b/src/locales/en/modifier-type.json @@ -64,6 +64,20 @@ "PokemonBaseStatBoosterModifierType": { "description": "Increases the holder's base {{statName}} 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." }, @@ -242,7 +256,12 @@ "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 healing items are no longer available to purchase in shops." }, + "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}}." } }, "SpeciesBoosterItem": { "LIGHT_BALL": { "name": "Light Ball", "description": "It's a mysterious orb that boosts Pikachu's Attack and Sp. Atk stats." }, diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts new file mode 100644 index 00000000000..7328c2d5b1b --- /dev/null +++ b/src/locales/en/mystery-encounter.ts @@ -0,0 +1,79 @@ +import lostAtSeaDialogue from "./mystery-encounters/lost-at-sea-dialogue.json"; +import mysteriousChestDialogue from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json"; +import mysteriousChallengersDialogue from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json"; +import darkDealDialogue from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json"; +import departmentStoreSaleDialogue from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json"; +import fieldTripDialogue from "#app/locales/en/mystery-encounters/field-trip-dialogue.json"; +import fieryFalloutDialogue from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json"; +import fightOrFlightDialogue from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json"; +import safariZoneDialogue from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json"; +import shadyVitaminDealerDialogue from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json"; +import slumberingSnorlaxDialogue from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json"; +import trainingSessionDialogue from "#app/locales/en/mystery-encounters/training-session-dialogue.json"; +import theStrongStuffDialogue from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json"; +import thePokemonSalesmanDialogue from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json"; +import anOfferYouCantRefuseDialogue from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json"; +import delibirdyDialogue from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json"; +import absoluteAvariceDialogue from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json"; +import aTrainersTestDialogue from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json"; +import trashToTreasureDialogue from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json"; +import berriesAboundDialogue from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json"; +import clowningAroundDialogue from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json"; +import partTimerDialogue from "#app/locales/en/mystery-encounters/part-timer-dialogue.json"; +import dancingLessonsDialogue from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json"; +import weirdDreamDialogue from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json"; +import theWinstrateChallengeDialogue from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json"; +import teleportingHijinksDialogue from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json"; + +/** + * 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. + * - `{{}}` 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. + * - `@[]{}` will auto-color the given text to a specified {@link TextStyle} (e.g. `TextStyle.SUMMARY_GREEN`). + * + * For Option tooltips ({@link OptionTextDisplay.buttonTooltip}): + * - Any tooltip that starts with `(+)` or `(-)` at the beginning of a newline will auto-color to green/blue respectively. + * - Note, this only occurs for option tooltips, nowhere else. + * - Other types of `(...)` tooltips will have to specify the text color manually by using the `@[SUMMARY_GREEN]{}` pattern. + */ +export const mysteryEncounter = { + // DO NOT REMOVE + "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + + // General use content + "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.", + + mysteriousChallengers: mysteriousChallengersDialogue, + mysteriousChest: mysteriousChestDialogue, + darkDeal: darkDealDialogue, + fightOrFlight: fightOrFlightDialogue, + slumberingSnorlax: slumberingSnorlaxDialogue, + trainingSession: trainingSessionDialogue, + departmentStoreSale: departmentStoreSaleDialogue, + shadyVitaminDealer: shadyVitaminDealerDialogue, + fieldTrip: fieldTripDialogue, + safariZone: safariZoneDialogue, + lostAtSea: lostAtSeaDialogue, + fieryFallout: fieryFalloutDialogue, + theStrongStuff: theStrongStuffDialogue, + pokemonSalesman: thePokemonSalesmanDialogue, + offerYouCantRefuse: anOfferYouCantRefuseDialogue, + delibirdy: delibirdyDialogue, + absoluteAvarice: absoluteAvariceDialogue, + aTrainersTest: aTrainersTestDialogue, + trashToTreasure: trashToTreasureDialogue, + berriesAbound: berriesAboundDialogue, + clowningAround: clowningAroundDialogue, + partTimer: partTimerDialogue, + dancingLessons: dancingLessonsDialogue, + weirdDream: weirdDreamDialogue, + theWinstrateChallenge: theWinstrateChallengeDialogue, + teleportingHijinks: teleportingHijinksDialogue +} as const; 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 00000000000..a28d31841a3 --- /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.\n $I have a super awesome proposal\nfor a strong trainer such as yourself!\n $I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.\n $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.\n $Here, let me help with that." + }, + "cheryl": { + "intro_dialogue": "Hello, my name's Cheryl.\n $I have a particularly interesting request,\nfor a strong trainer such as yourself.\n $I'm carrying two rare Pokémon Eggs with me,\nbut I'd like someone else to care for one.\n $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.\n $Here, let me help with that." + }, + "marley": { + "intro_dialogue": "...@d{64} I'm Marley.\n $I have an offer for you...\n $I'm carrying two Pokémon Eggs with me,\nbut I'd like someone else to care for one.\n $If you're stronger than me,\nI'll give you the rarer Egg.", + "accept": "... I see.", + "decline": "... I see.\n $Your Pokémon look hurt...\nLet me help." + }, + "mira": { + "intro_dialogue": "Hi! I'm Mira!\n $Mira has a request\nfor a strong trainer like you!\n $Mira has two rare Pokémon Eggs,\nbut Mira wants someone else to take one!\n $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!\n $Here, Mira will heal your team!" + }, + "riley": { + "intro_dialogue": "I'm Riley.\n $I have an odd proposal\nfor a strong trainer such as yourself.\n $I'm carrying two rare Pokémon Eggs with me,\nbut I'd like to give one to another trainer.\n $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.\n $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 00000000000..9e58b3ec8ac --- /dev/null +++ b/src/locales/en/mystery-encounters/absolute-avarice-dialogue.json @@ -0,0 +1,25 @@ +{ + "intro": "A Greedent ambushes you\nand steals your party's berries!", + "title": "Absolute Avarice", + "description": "The Greedent has caught you totally off guard now all your berries are gone!\n\nThe Greedent 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 Greedent stuffs its cheeks\nand prepares for battle!", + "boss_enraged": "Greedent's fierce love for food has it incensed!", + "food_stash": "It looks like the Greedent was guarding an enormous stash of food!\n $@s{item_fanfare}Each Pokémon in your party gains 1x Reviver Seed!" + }, + "2": { + "label": "Reason with It", + "tooltip": "(+) Regain Some Lost Berries", + "selected": "Your pleading strikes a chord with the Greedent.\n $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 Greedent Will Like You", + "selected": "The Greedent devours the entire\nstash of berries in a flash!\n $Patting its stomach,\nit looks at you appreciatively.\n $Perhaps you could feed it\nmore berries on your adventure...\n $@s{level_up_fanfare}The Greedent 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 00000000000..30665fdb5d1 --- /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.\n $I can't help but notice that your\n{{strongestPokemon}} looks positively divine!\n $I've always wanted to have a pet like that!\n $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}}!\n $It's time to show you off to everyone at the yacht club!\n $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, Liepard!\n $You'll be hearing from my lawyers for this!" + }, + "3": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "What a rotten day...\n $Ah, well. Let's return to the yacht club then, Liepard." + } + } +} \ 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 00000000000..1a6ed26e661 --- /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 this Pokémon looks strong. Maybe a fast Pokémon would be able to grab some 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!\n $It manages to nab {{numBerries}} before the {{enemyPokemon}} can react!\n $You quickly retreat with your newfound prize.", + "selected_bad": "Your {{fastestPokemon}} races for the berry bush!\n $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/clowning-around-dialogue.json b/src/locales/en/mystery-encounters/clowning-around-dialogue.json new file mode 100644 index 00000000000..8aa8a8b74d9 --- /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 Blacephalon 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 Blacephalon 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 Blacephalon 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 00000000000..bd75ab13f24 --- /dev/null +++ b/src/locales/en/mystery-encounters/dancing-lessons-dialogue.json @@ -0,0 +1,27 @@ +{ + "intro": "An Oricorio dances sadly alone, without a partner.", + "title": "Dancing Lessons", + "description": "The Oricorio 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 Oricorio is distraught and moves to defend itself!", + "boss_enraged": "The Oricorio's fear boosted its stats!" + }, + "2": { + "label": "Learn Its Dance", + "tooltip": "(+) Teach a Pokémon Revelation Dance", + "selected": "You watch the Oricorio closely as it performs its dance...\n $@s{level_up_fanfare}Your {{selectedPokemon}} wants to learn Revelation Dance!" + }, + "3": { + "label": "Show It a Dance", + "tooltip": "(-) Teach the Oricorio a Dance Move\n(+) The Oricorio 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 Oricorio watches in fascination as\n{{selectedPokemon}} shows off {{selectedMove}}!\n $It loves the display!\n $@s{level_up_fanfare}The Oricorio 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 00000000000..d3a94a312d3 --- /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!\n $I've been working on a new device\nto bring out a Pokémon's latent power!\n $It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.\n $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!\n $Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...", + "selected_message": "The man hands you 5 Rogue Balls.\n ${{pokeName}} hops into the strange machine...\n $Flashing lights and weird noises\nstart coming from the machine!\n $...@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 00000000000..d5bdf608663 --- /dev/null +++ b/src/locales/en/mystery-encounters/delibirdy-dialogue.json @@ -0,0 +1,29 @@ + + +{ + "intro": "A pack of Delibird have appeared!", + "title": "Delibir-dy", + "description": "The Delibirds 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 Delibirds {{money, money}}\n(+) Receive a Gift Item", + "selected": "You toss the money to the Delibirds,\nwho chatter amongst themselves excitedly.\n $They turn back to you and happily give you a present!" + }, + "2": { + "label": "Give Food", + "tooltip": "(-) Give the Delibirds a Berry or Reviver Seed\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the Delibirds,\nwho chatter amongst themselves excitedly.\n $They turn back to you and happily give you a present!" + }, + "3": { + "label": "Give an Item", + "tooltip": "(-) Give the Delibirds a Held Item\n(+) Receive a Gift Item", + "select_prompt": "Select an item to give.", + "selected": "You toss the {{chosenItem}} to the Delibirds,\nwho chatter amongst themselves excitedly.\n $They turn back to you and happily give you a present!" + } + }, + "outro": "The Delibird pack happily waddles off into the distance.\n $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 00000000000..d7ef07f7462 --- /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?\n $There's a special coupon that you can\nredeem for a free item during the sale!\n $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 00000000000..1bb58be363b --- /dev/null +++ b/src/locales/en/mystery-encounters/field-trip-dialogue.json @@ -0,0 +1,28 @@ +{ + "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?\n $I'm teaching them about Pokémon moves\nand would love to show them a demonstration.\n $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}}!", + "incorrect": "...\n $That isn't a {{moveCategory}} move!\n $I'm sorry, but I can't give you anything.", + "lesson_learned": "Looks like you learned a valuable lesson?\n $Your Pokémon also gained some knowledge." + }, + "second_option_prompt": "Choose a move for your Pokémon to use.", + "outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!", + "outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere." +} \ 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 00000000000..5c36f3a8b99 --- /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 Volcarona in the middle of a mating dance!\n $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!\n $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 Volcarona are in the middle of a mating dance!\n $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.\n $@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 00000000000..3ba99b7ba17 --- /dev/null +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json @@ -0,0 +1,24 @@ +{ + "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 this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.", + "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." + }, + "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}\n $Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!\n $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/lost-at-sea-dialogue.json b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.json new file mode 100644 index 00000000000..8e10a39b479 --- /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.\n ${{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.\n ${{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.\n $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 00000000000..01f4e6092eb --- /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 00000000000..cbe5df1cda8 --- /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 trapped!\n $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 00000000000..918b1fb61b8 --- /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!\n $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!\n $Here's your check for the day.", + "job_complete_bad": "Your {{selectedPokemon}} helped us out a bit!\n $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 00000000000..8869f2055e5 --- /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 00000000000..94904fbcb12 --- /dev/null +++ b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json @@ -0,0 +1,29 @@ +{ + "intro": "A man in a dark coat approaches you.", + "speaker": "Shady Salesman", + "intro_dialogue": ".@d{16}.@d{16}.@d{16}\n $I've got the goods if you've got the money.\n $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 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(-) Side Effects?\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.\n ${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!" + }, + "damage_only": "But the medicine had some side effects!\n $Your {{selectedPokemon}} takes some damage...", + "bad_poison": "But the medicine had some side effects!\n $Your {{selectedPokemon}} takes some damage\nand becomes badly poisoned...", + "poison": "But the medicine had some side effects!\n $Your {{selectedPokemon}} becomes poisoned...", + "no_bad_effects": "Looks like there were no side-effects this time." +} \ 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 00000000000..82e202d9367 --- /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.\n $You get closer to see a Snorlax sleeping peacefully.\nIt seems like there's no way around it.", + "title": "Slumbering Snorlax", + "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 Snorlax\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}\n $You wait for a time, but the Snorlax's yawns make your party sleepy...", + "rest_result": "When you all awaken, the Snorlax 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}}!\n $@s{item_fanfare}It steals Leftovers off the sleeping\nSnorlax 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 00000000000..a2665686d88 --- /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!\n $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...\n $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!\n $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!\n $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 00000000000..88e93782062 --- /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!\n $I can see you've a keen eye for business.\n $Oh, yeah...@d{64} Returns not accepted, got that?" + }, + "2": { + "label": "Refuse", + "tooltip": "(-) No Rewards", + "selected": "No?@d{32} You say no?\n $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 00000000000..b0174486b29 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "It's a massive Shuckle and what appears\nto be an equally large stash of... juice?", + "title": "The Strong Stuff", + "description": "The Shuckle that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe Shuckle extends its feelers in your direction. It seems like it wants to touch you, but is that really a good idea?", + "query": "What will you do?", + "option": { + "1": { + "label": "Let It Touch You", + "tooltip": "(?) Something awful or amazing might happen", + "selected": "You black out.", + "selected_2": "@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained.\n $Your {{highBstPokemon}} feels a\nterrible lethargy come over it!\n $Its base stats were reduced by 20 in each stat!\n $Your remaining Pokémon feel an incredible vigor, though!\n $Their base stats are increased by 10 in each stat!" + }, + "2": { + "label": "Battle the Shuckle", + "tooltip": "(-) Hard Battle\n(+) Special Rewards", + "selected": "Enraged, the Shuckle drinks some of its juice and attacks!", + "stat_boost": "The Shuckle'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 00000000000..9f50b6abae1 --- /dev/null +++ b/src/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json @@ -0,0 +1,21 @@ +{ + "intro": "It's a family standing outside their house!", + "speaker": "The Winstrates", + "intro_dialogue": "We're the Winstrates!\n $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": "That's the spirit! I like you!" + }, + "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!\n $Our family uses this Macho Brace to strengthen our Pokémon more effectively during their training.\n $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 00000000000..62e89cd1dae --- /dev/null +++ b/src/locales/en/mystery-encounters/training-session-dialogue.json @@ -0,0 +1,28 @@ +{ + "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!\n $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!\n $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!\n $Its ability was changed to {{ability}}!" + }, + "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 00000000000..1ec4a7102d7 --- /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": "(-) Lose Healing Items in Shops\n(+) Gain Amazing Items", + "selected": "You wade through the garbage pile, becoming mired in filth.\n $There's no way any respectable shopkeepers\nwill sell you anything in your grimy state!\n $You'll just have to make do without shop healing items.\n $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/weird-dream-dialogue.json b/src/locales/en/mystery-encounters/weird-dream-dialogue.json new file mode 100644 index 00000000000..254c161d92d --- /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...\n $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.\n $Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.\n $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...\n $.@d{32}.@d{32}.@d{32}\n $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.\n $When you finally stop to collect yourself, you check the Pokémon in your team.\n $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 9c2b3f30e5e..338bdfaec80 100644 --- a/src/locales/en/party-ui-handler.json +++ b/src/locales/en/party-ui-handler.json @@ -15,6 +15,7 @@ "UNPAUSE_EVOLUTION": "Unpause 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!", diff --git a/src/locales/en/trainer-classes.json b/src/locales/en/trainer-classes.json index 1b827281a6a..f3b81b06acb 100644 --- a/src/locales/en/trainer-classes.json +++ b/src/locales/en/trainer-classes.json @@ -117,5 +117,15 @@ "plasma_grunts": "Plasma Grunts", "flare_grunt": "Flare Grunt", "flare_grunt_female": "Flare Grunt", - "flare_grunts": "Flare Grunts" + "flare_grunts": "Flare Grunts", + "buck": "Buck", + "cheryl": "Cheryl", + "marley": "Marley", + "mira": "Mira", + "riley": "Riley", + "victor": "Victor", + "victoria": "Victoria", + "vivi": "Vivi", + "vicky": "Vicky", + "vito": "Vito" } \ No newline at end of file diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 365fc433d2f..6a70b4c7737 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1,27 +1,27 @@ 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 { Stat, getStatName } from "../data/pokemon-stat"; +import { getStatName, Stat } from "../data/pokemon-stat"; import { tmPoolTiers, tmSpecies } from "../data/tms"; import { Type } from "../data/type"; import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler"; import * as Utils from "../utils"; -import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from "../data/temp-battle-stat"; +import { getTempBattleStatBoosterItemName, getTempBattleStatName, TempBattleStat } from "../data/temp-battle-stat"; 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"; @@ -110,28 +110,35 @@ 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; + const modifierPool = getModifierPoolForType(poolType); + + for (const weightedModifiers of Object.values(modifierPool)) { + for (const mod of weightedModifiers) { + if (mod.modifierType.id === this.id) { + this.tier = mod.modifierType.tier; + return this; } } - if (this.tier) { - break; - } } + + // Fallback to COMMON tier if no tier found + this.tier = ModifierTier.COMMON; return this; } @@ -164,7 +171,7 @@ export interface GeneratedPersistentModifierType { getPregenArgs(): any[]; } -class AddPokeballModifierType extends ModifierType { +export class AddPokeballModifierType extends ModifierType { private pokeballType: PokeballType; private count: integer; @@ -652,6 +659,49 @@ export class PokemonBaseStatBoosterModifierType extends PokemonHeldItemModifierT } } +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; + } + + 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, + }); + } + + getPregenArgs(): any[] { + return [ this.statModifier ]; + } +} + +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; + } + + getDescription(scene: BattleScene): string { + return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", { + stats: this.stats.map(stat => getStatName(stat)).join("/"), + statValue: this.statModifier, + }); + } + + getPregenArgs(): any[] { + return [ this.statModifier, this.stats ]; + } +} + class AllPokemonFullHpRestoreModifierType extends ModifierType { private descriptionKey: string; @@ -1473,6 +1523,21 @@ 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.RemoveHealShopModifier(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)), }; interface ModifierPool { @@ -1941,29 +2006,98 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod } } +export interface CustomModifierSettings { + guaranteedModifierTiers?: ModifierTier[]; + guaranteedModifierTypeOptions?: ModifierTypeOption[]; + guaranteedModifierTypeFuncs?: ModifierTypeFunc[]; + fillRemaining?: boolean; + 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 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 0, 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!); } - if (candidate) { - options.push(candidate); + + // 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)); + }); + } + + // Fill remaining + if (options.length < count && customModifierSettings.fillRemaining) { + while (options.length < count) { + options.push(getModifierTypeOptionWithRetry(options, retryCount, party, undefined)); + } + } + } overridePlayerModifierTypeOptions(options, party); return options; } +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} @@ -2089,7 +2223,7 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): Modifiers.P return ret; } -function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0): ModifierTypeOption | null { +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; @@ -2115,7 +2249,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; @@ -2148,7 +2282,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 99f4540f493..a2ce1eacc3f 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -20,7 +20,7 @@ import { VoucherType } from "../system/voucher"; import { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { Nature } from "#app/data/nature"; import Overrides from "#app/overrides"; -import { ModifierType, modifierTypes } from "./modifier-type"; +import { GeneratorModifierOverride, ModifierType, ModifierTypeGenerator, modifierTypes } from "./modifier-type"; import { Command } from "#app/ui/command-ui-handler"; import { Species } from "#enums/species"; import i18next from "i18next"; @@ -706,6 +706,147 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier { } } +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; + } + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatTotalModifier; + } + + clone(): PersistentModifier { + return new PokemonBaseStatTotalModifier(this.type as ModifierTypes.PokemonBaseStatTotalModifierType, this.pokemonId, this.statModifier, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.statModifier); + } + + shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + apply(args: any[]): boolean { + // Modifies the passed in baseStats[] array + args[1].forEach((v, i) => { + const newVal = Math.floor(v + this.statModifier); + args[1][i] = Math.min(Math.max(newVal, 1), 999999); + }); + + return true; + } + + getScoreMultiplier(): number { + return 1.2; + } + + getMaxHeldItemCount(pokemon: Pokemon): integer { + return 2; + } +} + +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; + } + + matchType(modifier: Modifier): boolean { + return modifier instanceof PokemonBaseStatFlatModifier; + } + + clone(): PersistentModifier { + return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount); + } + + getArgs(): any[] { + return super.getArgs().concat(this.statModifier, this.stats); + } + + shouldApply(args: any[]): boolean { + return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array; + } + + 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; + } + + getScoreMultiplier(): number { + return 1.1; + } + + getMaxHeldItemCount(pokemon: Pokemon): integer { + return 1; + } +} + +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. @@ -2225,6 +2366,28 @@ export class LockModifierTiersModifier extends PersistentModifier { } } +export class RemoveHealShopModifier extends PersistentModifier { + constructor(type: ModifierType, stackCount?: integer) { + super(type, stackCount); + } + + match(modifier: Modifier): boolean { + return modifier instanceof RemoveHealShopModifier; + } + + clone(): RemoveHealShopModifier { + return new RemoveHealShopModifier(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); @@ -2759,9 +2922,19 @@ export function overrideModifiers(scene: BattleScene, isPlayer: boolean = true): } modifiersOverride.forEach(item => { - const modifierFunc = modifierTypes[item.name]; - const modifier = modifierFunc().withIdFromFunc(modifierFunc).newModifier() as PersistentModifier; - modifier.stackCount = item.count || 1; + const modifierName = item.name; + const qty = item.count || 1; + if (!modifierTypes.hasOwnProperty(modifierName)) { + return; + } // if the modifier does not exist, we skip it + let modifierType: ModifierType = modifierTypes[modifierName](); + if (modifierType instanceof ModifierTypeGenerator) { + const itemType = [(item as GeneratorModifierOverride)?.type] ?? null; + modifierType = (modifierType as ModifierTypeGenerator).generateType(scene.getParty(), itemType)!; + } + modifierType = modifierType.withIdFromFunc(modifierTypes[modifierName]); + const modifier: PersistentModifier = modifierType.newModifier(scene.getParty()[0]) as PersistentModifier; + modifier.stackCount = qty; if (isPlayer) { scene.addModifier(modifier, true, false, false, true); diff --git a/src/overrides.ts b/src/overrides.ts index 8b3d628e05e..1ec9df4b442 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 @@ -126,6 +127,14 @@ 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 68ede826d95..b81164be4f1 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -18,6 +18,7 @@ import i18next from "i18next"; import * as Utils from "#app/utils.js"; 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; @@ -137,6 +138,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) { @@ -176,7 +184,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 d3663abe3b6..ec680158498 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, () => { + const target = this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon(); + new CommonBattleAnim(this.anim, this.getPokemon(), target, this.playOnEmptyField).play(this.scene, () => { this.end(); }); } diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index dfa198c8339..de0262bd70a 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -26,6 +26,11 @@ import { ScanIvsPhase } from "./scan-ivs-phase"; import { ShinySparklePhase } from "./shiny-sparkle-phase"; import { SummonPhase } from "./summon-phase"; import { ToggleDoublePositionPhase } from "./toggle-double-position-phase"; +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"; export class EncounterPhase extends BattlePhase { private loaded: boolean; @@ -54,6 +59,29 @@ 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) => { @@ -78,7 +106,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) { @@ -111,6 +139,21 @@ 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) { + const newEncounter = this.scene.getMysteryEncounter(mysteryEncounter); + battle.mysteryEncounter = newEncounter; + } + loadEnemyAssets.push(battle.mysteryEncounter.introVisuals!.loadAssets().then(() => battle.mysteryEncounter.introVisuals!.initSprite())); + // 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 { // This block only applies for double battles to init the boss segments (idk why it's split up like this) if (battle.enemyParty.filter(p => p.isBoss()).length > 1) { @@ -138,6 +181,8 @@ export class EncounterPhase extends BattlePhase { } else if (battle.battleType === BattleType.TRAINER) { enemyPokemon.setVisible(false); this.scene.currentBattle.trainer?.tint(0, 0.5); + } else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) { + // TODO: this may not be necessary, but leaving as placeholder } if (battle.double) { enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT); @@ -199,6 +244,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 { @@ -285,6 +343,62 @@ export class EncounterPhase extends BattlePhase { showDialogueAndSummon(); } } + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals!; + introVisuals.playAnim(); + + if (this.scene.currentBattle.mysteryEncounter.onVisualsStart) { + this.scene.currentBattle.mysteryEncounter.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 = this.scene.currentBattle.mysteryEncounter.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())); + }); + } } } @@ -297,7 +411,7 @@ export class EncounterPhase extends BattlePhase { } }); - if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { + if (this.scene.currentBattle.battleType !== BattleType.TRAINER && this.scene.currentBattle.battleType !== BattleType.MYSTERY_ENCOUNTER) { 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/faint-phase.ts b/src/phases/faint-phase.ts index 66946d268cb..f3e6dd3169d 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -104,7 +104,7 @@ export class FaintPhase extends PokemonPhase { } } else { this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex)); - if (this.scene.currentBattle.battleType === BattleType.TRAINER) { + if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { 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)); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts new file mode 100644 index 00000000000..52b4f0163db --- /dev/null +++ b/src/phases/mystery-encounter-phases.ts @@ -0,0 +1,505 @@ +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 { Tutorial, handleTutorial } from "../tutorial"; +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"; + +/** + * 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 onPreOptionPhase() logic if it exists for the selected option + * - Display any OptionTextDisplay.selected type dialogue that is set in the MysteryEncounterDialogue dialogue tree for selected option + * - Queuing of the MysteryEncounterOptionSelectedPhase + */ +export class MysteryEncounterPhase extends Phase { + private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300; + optionSelectSettings?: OptionSelectSettings; + + /** + * + * @param scene + * @param optionSelectSettings - allows overriding the typical options of an encounter with new ones + * Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time + */ + constructor(scene: BattleScene, optionSelectSettings?: OptionSelectSettings) { + super(scene); + this.optionSelectSettings = optionSelectSettings; + } + + start() { + super.start(); + + // Clears out queued phases that are part of standard battle + this.scene.clearPhaseQueue(); + this.scene.clearPhaseQueueSplice(); + + this.scene.currentBattle.mysteryEncounter.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, etc. + this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]); + } + + // Initiates encounter dialogue window and option select + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings); + } + + handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { + // Set option selected flag + this.scene.currentBattle.mysteryEncounter.selectedOption = option; + + 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; + } + + 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(); + } + } + + cancel() { + this.end(); + } + + end() { + this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end()); + } +} + +/** + * Will handle (in order): + * - Execute 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; + } + + 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()); + }); + } else { + this.scene.executeWithSeedOffset(() => { + this.onOptionSelect(this.scene).finally(() => { + this.end(); + }); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); + } + } +} + +/** + * Runs at the beginning of an Encounter's battle + * Will cleanup any residual flinches, Endure, etc. that are left over from startOfBattleEffects + * See [TurnEndPhase](../phases.ts) for more details + */ +export class MysteryEncounterBattleStartCleanupPhase extends Phase { + constructor(scene: BattleScene) { + super(scene); + } + + 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); + } + + super.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 SummonPhases, PostSummonPhases, 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; + } + + start() { + super.start(); + + this.doMysteryEncounterBattle(this.scene); + } + + 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 }); + } + + 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(); + } + } + } + } + + 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)); + } + } + } + + // TODO: remove? + handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end()); + } + + 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(); + } + }); + } + + 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 MysteryEncounter doEncounterExp + * - Any encounter reward logic that is set within MysteryEncounter doEncounterRewards + * - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true + * - Queuing of the PostMysteryEncounterPhase + */ +export class MysteryEncounterRewardsPhase extends Phase { + addHealPhase: boolean; + + constructor(scene: BattleScene, addHealPhase: boolean = false) { + super(scene); + this.addHealPhase = addHealPhase; + } + + start() { + super.start(); + const encounter = this.scene.currentBattle.mysteryEncounter; + + if (encounter.doContinueEncounter) { + encounter.doContinueEncounter(this.scene).then(() => { + this.end(); + }); + } else { + this.scene.executeWithSeedOffset(() => { + if (this.scene.currentBattle.mysteryEncounter.doEncounterExp) { + this.scene.currentBattle.mysteryEncounter.doEncounterExp(this.scene); + } + + if (this.scene.currentBattle.mysteryEncounter.doEncounterRewards) { + this.scene.currentBattle.mysteryEncounter.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: 0 })); + } + // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) + }, this.scene.currentBattle.waveIndex * 1000); + + this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene)); + this.end(); + } + } +} + +/** + * Will handle (in order): + * - 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; + } + + 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()); + } else { + this.continueEncounter(); + } + } + + 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 c447e78f7b1..f2ead9bf58c 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 89987534fc0..547b1b19651 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/post-summon-phase.ts b/src/phases/post-summon-phase.ts index e671bf30ed1..4b4af25741e 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -4,6 +4,8 @@ import { applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability.js"; import { ArenaTrapTag } from "#app/data/arena-tag.js"; import { StatusEffect } from "#app/enums/status-effect.js"; import { PokemonPhase } from "./pokemon-phase"; +import { MysteryEncounterPostSummonTag } from "#app/data/battler-tags"; +import { BattlerTagType } from "#enums/battler-tag-type"; export class PostSummonPhase extends PokemonPhase { constructor(scene: BattleScene, battlerIndex: BattlerIndex) { @@ -19,6 +21,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 (pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag)) { + 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 1c96d278d69..ea722026cfb 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.js"; 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 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.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,8 @@ 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); + const multiplier = !isNullOrUndefined(this.customModifierSettings?.rerollMultiplier) ? this.customModifierSettings!.rerollMultiplier! : 1; + 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 +250,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/summon-phase.ts b/src/phases/summon-phase.ts index f65a2063d4c..53f2ceb6ca3 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,13 +80,16 @@ 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(); } } @@ -167,6 +171,58 @@ export class SummonPhase extends PartyMemberPokemonPhase { }); } + 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 +232,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 || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index a999d57ca0f..a8d1b16bc2f 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 } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -46,6 +47,8 @@ export class TurnInitPhase extends FieldPhase { //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); + handleMysteryEncounterBattleStartEffects(this.scene); + 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 b7587de4dbb..a4d8c096c33 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -15,10 +15,17 @@ 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 { PokemonIncrementingStatModifier } from "#app/modifier/modifier"; +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, isExpOnly: boolean = false) { super(scene, battlerIndex); + + this.isExpOnly = isExpOnly; } start() { @@ -39,12 +46,20 @@ export class VictoryPhase extends PokemonPhase { let expValue = this.getPokemon().getExpValue(); if (this.scene.currentBattle.battleType === BattleType.TRAINER) { expValue = Math.floor(expValue * 1.5); + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + expValue = Math.floor(expValue * this.scene.currentBattle.mysteryEncounter.expMultiplier); } for (const partyMember of nonFaintedPartyMembers) { const pId = partyMember.id; const participated = participantIds.has(pId); if (participated) { partyMember.addFriendship(2); + const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier); + if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this.scene)) { + machoBraceModifier.stackCount++; + this.scene.updateModifiers(true, true); + partyMember.updateInfo(); + } } if (!expPartyMembers.includes(partyMember)) { continue; @@ -107,7 +122,13 @@ export class VictoryPhase extends PokemonPhase { } } - if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) { + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + handleMysteryEncounterVictory(this.scene, false, this.isExpOnly); + this.end(); + return; + } + + 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 c9a76dc50a4..88d6ce2d387 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 3d24458a06c..bede8522627 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -167,6 +167,14 @@ 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); + return `@[MONEY]{₽${numberFormattedString}}`; + }); + await initFonts(localStorage.getItem("prLang") ?? undefined); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index e7bc85d9037..fc5a32e7c7e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -45,6 +45,8 @@ import { TerrainType } from "#app/data/terrain.js"; import { OutdatedPhase } from "#app/phases/outdated-phase.js"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js"; import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; +import { MysteryEncounterData } from "../data/mystery-encounters/mystery-encounter-data"; +import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -89,7 +91,7 @@ export function encrypt(data: string, bypassLogin: boolean): string { export function decrypt(data: string, bypassLogin: boolean): string { return (bypassLogin - ? (data: string) => atob(data) + ? (data: string) => decodeURIComponent(atob(data)) : (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data); } @@ -129,6 +131,8 @@ export interface SessionSaveData { gameVersion: string; timestamp: integer; challenges: ChallengeData[]; + mysteryEncounter: MysteryEncounter; + mysteryEncounterData: MysteryEncounterData; } interface Unlocks { @@ -962,7 +966,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)), + mysteryEncounter: scene.currentBattle.mysteryEncounter, + mysteryEncounterData: scene.mysteryEncounterData } as SessionSaveData; } @@ -1053,11 +1059,14 @@ export class GameData { scene.score = sessionData.score; scene.updateScoreText(); + scene.mysteryEncounterData = sessionData?.mysteryEncounterData ? sessionData?.mysteryEncounterData : new MysteryEncounterData(null); + 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 mysteryEncounterConfig = sessionData?.mysteryEncounter; + const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterConfig)!; // TODO: is this bang correct? battle.enemyLevels = sessionData.enemyParty.map(p => p.level); scene.arena.init(); @@ -1271,6 +1280,14 @@ export class GameData { return ret; } + if (k === "mysteryEncounter") { + return new MysteryEncounter(v); + } + + if (k === "mysteryEncounterData") { + return new MysteryEncounterData(v); + } + return v; }) as SessionSaveData; } @@ -1555,12 +1572,20 @@ export class GameData { } } - setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false): Promise { - return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg); + /** + * + * @param pokemon + * @param incrementCount + * @param fromEgg + * @param showNewStarterMessage + * @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, showNewStarterMessage: boolean = true): Promise { + return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg, showNewStarterMessage); } - setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false): Promise { - return new Promise(resolve => { + setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showNewStarterMessage: boolean = true): Promise { + return new Promise(resolve => { const dexEntry = this.dexData[species.speciesId]; const caughtAttr = dexEntry.caughtAttr; const formIndex = pokemon.formIndex; @@ -1615,20 +1640,20 @@ export class GameData { } } - const checkPrevolution = () => { + const checkPrevolution = (newStarter: boolean) => { if (hasPrevolution) { const prevolutionSpecies = pokemonPrevolutions[species.speciesId]; - return this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg).then(() => resolve()); + return this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showNewStarterMessage).then(result => resolve(result)); } else { - resolve(); + resolve(newStarter); } }; - if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { + if (newCatch && speciesStarters.hasOwnProperty(species.speciesId) && showNewStarterMessage) { 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); } }); } @@ -1670,7 +1695,7 @@ export class GameData { this.starterData[species.speciesId].candyCount += count; } - setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer): Promise { + setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, prependSpeciesToMessage: boolean = false): Promise { return new Promise(resolve => { const speciesId = species.speciesId; if (!speciesEggMoves.hasOwnProperty(speciesId) || !speciesEggMoves[speciesId][eggMoveIndex]) { @@ -1694,7 +1719,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 8f094379434..ddd1063ce93 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; @@ -55,6 +56,7 @@ export default class PokemonData { public bossSegments?: integer; public summonData: PokemonSummonData; + public mysteryEncounterData: MysteryEncounterPokemonData; constructor(source: Pokemon | any, forHistory: boolean = false) { const sourcePokemon = source instanceof Pokemon ? source : null; @@ -110,6 +112,7 @@ export default class PokemonData { this.status = sourcePokemon.status; if (this.player) { this.summonData = sourcePokemon.summonData; + this.mysteryEncounterData = sourcePokemon.mysteryEncounterData; } } } else { @@ -139,6 +142,14 @@ export default class PokemonData { this.summonData.tags = []; } } + + this.mysteryEncounterData = new MysteryEncounterPokemonData(); + if (!forHistory && source.mysteryEncounterData) { + this.mysteryEncounterData.spriteScale = source.mysteryEncounterData.spriteScale; + this.mysteryEncounterData.ability = source.mysteryEncounterData.ability; + this.mysteryEncounterData.passive = source.mysteryEncounterData.passive; + this.mysteryEncounterData.types = source.mysteryEncounterData.types; + } } } diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts new file mode 100644 index 00000000000..a157f87c1ba --- /dev/null +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -0,0 +1,165 @@ +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 MysteryEncounter to either the start of a battle, or to the 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("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; + } + + uiHandler.processInput(Button.ACTION); + + if (!isNullOrUndefined(secondaryOptionSelect?.pokemonNo)) { + await handleSecondaryOptionSelect(game, secondaryOptionSelect!.pokemonNo, secondaryOptionSelect!.optionNo); + } +} + +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"); + 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 MysteryEncounter that has a battle, can call this to skip battle and proceed to MysteryEncounterRewardsPhase + * @param game + */ +export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager) { + 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(); + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase, true); +} 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 00000000000..6b405b9e5b3 --- /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/encounterTestUtils"; +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 00000000000..b5999f69897 --- /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/encounterTestUtils"; +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", 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", 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 00000000000..bbc1a968b5c --- /dev/null +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -0,0 +1,256 @@ +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/encounterTestUtils"; +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"; + +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); + + 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); + + 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 00000000000..23ec94683ee --- /dev/null +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -0,0 +1,237 @@ +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/encounterTestUtils"; +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.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + 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); + }); + + it("should reward the player with X berries based on wave", { retry: 5 }, async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const numBerries = game.scene.currentBattle.mysteryEncounter.misc.numBerries; + scene.modifiers = []; + + 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.battleStats).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/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts new file mode 100644 index 00000000000..ebe0a28dd4f --- /dev/null +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -0,0 +1,379 @@ +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/encounterTestUtils"; +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), + mysteryEncounterData: expect.anything(), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }); + expect(config.pokemonConfigs?.[1].mysteryEncounterData?.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].mysteryEncounterData?.ability); + expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs?.[1].mysteryEncounterData?.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.mysteryEncounterData?.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); + }, 2000000); + + 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].mysteryEncounterData?.types; + const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterData?.types; + const thirdTypesAfter = scene.getParty()[2].mysteryEncounterData?.types; + + expect(leadTypesAfter.length).toBe(2); + expect(leadTypesAfter).not.toBe([Type.ICE, Type.WATER]); + expect(secondaryTypesAfter.length).toBe(2); + expect(secondaryTypesAfter.includes(Type.GRASS)).toBeTruthy(); + expect(secondaryTypesAfter.includes(Type.ELECTRIC)).toBeTruthy(); + expect(thirdTypesAfter.length).toBe(1); + }); + + 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 00000000000..8e31c6d822d --- /dev/null +++ b/src/test/mystery-encounter/encounters/dancing-lessons-encounter.test.ts @@ -0,0 +1,249 @@ +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/encounterTestUtils"; +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); + 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.battleStats).toEqual([1, 1, 1, 1, 1, 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); + 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); // 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); + 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); + 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 00000000000..136df416ea7 --- /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/encounterTestUtils"; +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 00000000000..2a050d2d835 --- /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/encounterTestUtils"; +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_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/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts new file mode 100644 index 00000000000..ccb4e998486 --- /dev/null +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -0,0 +1,288 @@ +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/encounterTestUtils"; +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 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; + }); + + 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 00000000000..fe23be61d26 --- /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/encounterTestUtils"; +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/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts new file mode 100644 index 00000000000..f77d9962f01 --- /dev/null +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -0,0 +1,281 @@ +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.js"; +import { Biome } from "#app/enums/biome"; +import { Moves } from "#app/enums/moves"; +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 "../encounterTestUtils"; +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"; + +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(Moves[Moves.SURF]); + expect(LostAtSeaEncounter.dialogueTokens?.option2RequiredMove).toBe(Moves[Moves.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); + + 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); + + 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 00000000000..ad596078738 --- /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/encounterTestUtils"; +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: 0.5, + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveMultiplier: 1, + 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 00000000000..4eb44af5f24 --- /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/encounterTestUtils"; +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 00000000000..f4a3b41bf1d --- /dev/null +++ b/src/test/mystery-encounter/encounters/teleporting-hijinks-encounter.test.ts @@ -0,0 +1,300 @@ +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/encounterTestUtils"; +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); + 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, 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", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + const enemyField = scene.getEnemyField(); + expect(enemyField[0].summonData.battleStats).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.battleStats).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.battleStats).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 00000000000..7dcf10669d6 --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -0,0 +1,205 @@ +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/encounterTestUtils"; +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); + expect(scene.getParty().find(p => p.name === pokemonName) instanceof PlayerPokemon).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 00000000000..41554cf10de --- /dev/null +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -0,0 +1,238 @@ +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/encounterTestUtils"; +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.COMMON); + 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, + mysteryEncounterData: new MysteryEncounterPokemonData(1.5), + 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 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); + }); + + expect(bstsAfter[0]).toEqual(bstsPrior[0] - 20 * 6); + expect(bstsAfter[1]).toEqual(bstsPrior[1] + 10 * 6); + expect(bstsAfter[2]).toEqual(bstsPrior[2] + 10 * 6); + }); + + 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.battleStats).toEqual([0, 2, 0, 2, 0, 0, 0]); + const shuckleItems = enemyField[0].getHeldItems(); + expect(shuckleItems.length).toBe(4); + expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.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 00000000000..e322c63b3c9 --- /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/encounterTestUtils"; +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: "trainerNames:victor", + 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 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 00000000000..2a60c4686e2 --- /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/encounterTestUtils"; +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, RemoveHealShopModifier, 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 RemoveHealShopModifier) as RemoveHealShopModifier; + 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/weird-dream-encounter.test.ts b/src/test/mystery-encounter/encounters/weird-dream-encounter.test.ts new file mode 100644 index 00000000000..99c4b2fdb13 --- /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/encounterTestUtils"; +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.mysteryEncounterData?.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 00000000000..baeef631e53 --- /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"); + + showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0, true); + }); + }); + + 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"); + + 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 00000000000..d2a2e7f9d92 --- /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 00000000000..5e02f6c94ab --- /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.mysteryEncounterData.encounteredEvents.length).toBeGreaterThan(0); + expect(game.scene.mysteryEncounterData.encounteredEvents[0][0]).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(game.scene.mysteryEncounterData.encounteredEvents[0][1]).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 00000000000..d946c850ae3 --- /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/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index 507161eb6d0..466bcbf8052 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 cb3c547744b..cf754dbc6e3 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -42,6 +42,11 @@ import { DailyModeHelper } from "./helpers/dailyModeHelper"; import { MoveHelper } from "./helpers/moveHelper"; import { OverridesHelper } from "./helpers/overridesHelper"; import { SettingsHelper } from "./helpers/settingsHelper"; +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"; /** * Class to manage the game state and transitions between phases. @@ -169,6 +174,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); + } + } + /** * Transitions to the start of a battle. * @param species - Optional array of species to start the battle with. diff --git a/src/test/utils/gameManagerUtils.ts b/src/test/utils/gameManagerUtils.ts index 20a3fd179fd..186f2efdc76 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, 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); + 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/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index d5eaee003db..40f0111ae78 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -8,8 +8,10 @@ import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type.js"; import Overrides from "#app/overrides"; -import { vi } from "vitest"; +import { MockInstance, 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 @@ -281,6 +283,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); + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); + return spy; + } + + /** + * Override the encounter chance for a mystery encounter. + * @returns spy instance + * @param tier + */ + mysteryEncounterTier(tier: MysteryEncounterTier): MockInstance { + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); + this.log(`Mystery encounter tier set to ${tier}!`); + return spy; + } + + /** + * Override the encounter that spawns for the scene + * @param encounterType + * @returns spy instance + */ + mysteryEncounter(encounterType: MysteryEncounterType) { + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); + this.log(`Mystery encounter override set to ${encounterType}!`); + return spy; + } + private log(...params: any[]) { console.log("Overrides:", ...params); } diff --git a/src/test/utils/mocks/mockGameObject.ts b/src/test/utils/mocks/mockGameObject.ts index 9138e0f687a..4c243ec9ca1 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 b26d03441fe..d0a1bfd7753 100644 --- a/src/test/utils/mocks/mockTextureManager.ts +++ b/src/test/utils/mocks/mockTextureManager.ts @@ -7,6 +7,7 @@ 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 MockVideo from "#test/utils/mocks/mocksContainer/mockVideo"; /** * Stub class for Phaser.Textures.TextureManager @@ -34,6 +35,7 @@ export default class MockTextureManager { text: this.text.bind(this), bitmapText: this.text.bind(this), displayList: this.displayList, + video: this.video.bind(this) }; } @@ -94,4 +96,10 @@ export default class MockTextureManager { this.list.push(polygon); return polygon; } + + video(x: number, y: number, key?: string) { + const video = new MockVideo(this, x, y, key); + this.list.push(video); + return video; + } } diff --git a/src/test/utils/mocks/mocksContainer/mockContainer.ts b/src/test/utils/mocks/mocksContainer/mockContainer.ts index 5babd9e71b2..78d84f7845b 100644 --- a/src/test/utils/mocks/mocksContainer/mockContainer.ts +++ b/src/test/utils/mocks/mocksContainer/mockContainer.ts @@ -14,7 +14,7 @@ export default class MockContainer implements MockGameObject { public frame; protected textureManager; public list: MockGameObject[] = []; - private name?: string; + name: string; constructor(textureManager: MockTextureManager, x, y) { this.x = x; @@ -36,6 +36,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. } @@ -153,6 +157,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. } @@ -208,4 +216,8 @@ 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); + } + } diff --git a/src/test/utils/mocks/mocksContainer/mockGraphics.ts b/src/test/utils/mocks/mocksContainer/mockGraphics.ts index e026b212e16..70a38c80aa0 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[] = []; + 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 26c2f74ea42..696427d10a3 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[] = []; + 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 35cd2d5faab..011f574a065 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[] = []; + 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 6b9ecf083fd..f307da99361 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -11,7 +11,7 @@ export default class MockText implements MockGameObject { public list: MockGameObject[] = []; public style; public text = ""; - private name?: string; + name: string; public color?: string; constructor(textureManager, x, y, content, styleOptions) { @@ -22,6 +22,7 @@ export default class MockText implements MockGameObject { // 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); @@ -86,6 +87,13 @@ export default class MockText implements MockGameObject { } } + showDialogue(text, name, delay, callback, callbackDelay, promptDelay) { + this.scene.messageWrapper.showDialogue(text, name, delay, callback, callbackDelay, promptDelay); + if (callback) { + callback(); + } + } + setScale(scale) { // return this.phaserText.setScale(scale); } @@ -247,6 +255,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 cb31480cc60..7f7e7570655 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; + name: string; constructor(manager, key: string, source) { this.manager = manager; diff --git a/src/test/utils/mocks/mocksContainer/mockVideo.ts b/src/test/utils/mocks/mocksContainer/mockVideo.ts new file mode 100644 index 00000000000..8f9457b70ac --- /dev/null +++ b/src/test/utils/mocks/mocksContainer/mockVideo.ts @@ -0,0 +1,26 @@ +import MockContainer from "#test/utils/mocks/mocksContainer/mockContainer"; + +export default class MockVideo extends MockContainer { + private video: HTMLVideoElement | null; + private videoTexture: Phaser.Textures.Texture | null; + private videoTextureSource: Phaser.Textures.TextureSource | null; + + constructor(textureManager, x: number, y: number, key?: string) { + super(textureManager, x, y); + this.video = null; + this.videoTexture = null; + this.videoTextureSource = null; + } + + stop(): this { + return this; + } + + play(): this { + return this; + } + + setLoop(value?: boolean): this { + return this; + } +} diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts new file mode 100644 index 00000000000..5b190a9d6a9 --- /dev/null +++ b/src/test/utils/overridesHelper.ts @@ -0,0 +1,129 @@ +import { Weather, WeatherType } from "#app/data/weather"; +import { Biome } from "#app/enums/biome"; +import { MockInstance, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import * as GameMode from "#app/game-mode"; +import { GameModes, getGameMode } from "#app/game-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import Overrides from "#app/overrides"; +import { ModifierOverride } from "#app/modifier/modifier-type"; + +/** + * Helper to handle overrides in tests + */ +export class OverridesHelper { + game: GameManager; + constructor(game: GameManager) { + this.game = game; + } + + /** + * 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); + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + this.log(`Mystery encounter chance set to ${percentage}% (=${rate})!`); + return spy; + } + + /** + * Override the encounter chance for a mystery encounter. + * @returns spy instance + * @param tier + */ + mysteryEncounterTier(tier: MysteryEncounterTier): MockInstance { + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_TIER_OVERRIDE", "get").mockReturnValue(tier); + this.log(`Mystery encounter tier set to ${tier}!`); + return spy; + } + + /** + * Override the encounter that spawns for the scene + * @param encounterType + * @returns spy instance + */ + mysteryEncounter(encounterType: MysteryEncounterType) { + const spy = vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(encounterType); + this.log(`Mystery encounter override set to ${encounterType}!`); + return spy; + } + + /** + * Override the starting biome + * @warning Any event listeners that are attached to [NewArenaEvent](events\battle-scene.ts) may need to be handled down the line + * @param biome the biome to set + */ + startingBiome(biome: Biome) { + this.game.scene.newArena(biome); + this.log(`Starting biome set to ${Biome[biome]} (=${biome})!`); + } + + /** + * Override the starting wave (index) + * @param wave the wave (index) to set. Classic: `1`-`200` + * @returns spy instance + */ + startingWave(wave: number) { + const spy = vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave); + this.log(`Starting wave set to ${wave}!`); + return spy; + } + + /** + * Override each wave to have or not have standard trainer battles + * @returns spy instance + * @param disable - true + */ + disableTrainerWaves(disable: boolean): MockInstance { + const realFn = getGameMode; + const spy = vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => { + const mode = realFn(gameMode); + mode.hasTrainers = !disable; + return mode; + }); + this.log(`Standard trainer waves are ${disable? "disabled" : "enabled"}!`); + return spy; + } + + /** + * Override the weather (type) + * @param type weather type to set + * @returns spy instance + */ + weather(type: WeatherType) { + const spy = vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); + this.log(`Weather set to ${Weather[type]} (=${type})!`); + return spy; + } + + /** + * Override the seed + * @param seed the seed to set + * @returns spy instance + */ + seed(seed: string) { + const spy = vi.spyOn(this.game.scene, "resetSeed").mockImplementation(() => { + this.game.scene.waveSeed = seed; + Phaser.Math.RND.sow([seed]); + this.game.scene.rngCounter = 0; + }); + this.game.scene.resetSeed(); + this.log(`Seed set to "${seed}"!`); + return spy; + } + + starterHeldItems(modifiers: ModifierOverride[]) { + const spy = vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); + this.log(`Starting modifiers set to ${modifiers.map(m => JSON.stringify(m)).join(", ")}!`); + return spy; + } + + private log(...params: any[]) { + console.log("Overrides:", ...params); + } +} diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index ca3d55137fa..06f714a90ec 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -37,6 +37,23 @@ import { UnavailablePhase } from "#app/phases/unavailable-phase"; import { VictoryPhase } from "#app/phases/victory-phase"; import ErrorInterceptor from "#app/test/utils/errorInterceptor"; import UI, { Mode } from "#app/ui/ui"; +import { + MysteryEncounterBattlePhase, + MysteryEncounterOptionSelectedPhase, + MysteryEncounterPhase, + MysteryEncounterRewardsPhase, + PostMysteryEncounterPhase +} from "#app/phases/mystery-encounter-phases"; +import { LearnMovePhase } from "#app/phases/learn-move-phase"; +import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase"; + +export interface PromptHandler { + phaseTarget?; + mode?; + callback?; + expireFn?; + awaitingActionInput?; +} export default class PhaseInterceptor { public scene; @@ -46,7 +63,7 @@ export default class PhaseInterceptor { private interval; private promptInterval; private intervalRun; - private prompts; + private prompts: PromptHandler[]; private phaseFrom; private inProgress; private originalSetMode; @@ -92,10 +109,17 @@ export default class PhaseInterceptor { [SwitchPhase, this.startPhase], [SwitchSummonPhase, this.startPhase], [PartyHealPhase, this.startPhase], + [MysteryEncounterPhase, this.startPhase], + [MysteryEncounterOptionSelectedPhase, this.startPhase], + [MysteryEncounterBattlePhase, this.startPhase], + [MysteryEncounterRewardsPhase, this.startPhase], + [PostMysteryEncounterPhase, this.startPhase], + [LearnMovePhase, this.startPhase], + [ModifierRewardPhase, this.startPhase] ]; private endBySetMode = [ - TitlePhase, SelectGenderPhase, CommandPhase + TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase, MysteryEncounterPhase, PostMysteryEncounterPhase ]; /** @@ -292,7 +316,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(); @@ -310,12 +334,12 @@ 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(); + this.prompts.shift()?.callback(); } } }); @@ -327,6 +351,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 eaa987c1a66..fe409321ff1 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -12,8 +12,9 @@ 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 { beforeAll, vi } from "vitest"; +import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; +import { beforeAll, beforeEach, vi } from "vitest"; +import Overrides from "#app/overrides"; /** Mock the override import to always return default values, ignoring any custom overrides. */ vi.mock("#app/overrides", async (importOriginal) => { @@ -36,6 +37,7 @@ initSpecies(); initMoves(); initAbilities(); initLoggedInUser(); +initMysteryEncounters(); global.testFailed = false; @@ -47,3 +49,8 @@ beforeAll(() => { } }); }); + +// Disables Mystery Encounters on all tests (can be overridden at test level) +beforeEach( () => { + vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(0); +}); diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index 446445fe86f..9e82e7ea809 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -32,7 +32,8 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { 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 +46,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 +107,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 +139,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 4a567c926d7..2ea053f47b4 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -4,9 +4,9 @@ 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, RemoveHealShopModifier } 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"; @@ -22,10 +22,11 @@ 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; @@ -105,6 +106,10 @@ 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); + // prepare move overlay const overlayScale = 1; this.moveInfoOverlay = new MoveInfoOverlay(this.scene, { @@ -113,7 +118,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); @@ -131,7 +136,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; } @@ -156,6 +161,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; @@ -163,7 +171,8 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.updateRerollCostText(); const typeOptions = args[1] as ModifierTypeOption[]; - const shopTypeOptions = !this.scene.gameMode.hasNoShop + const removeHealShop = this.scene.gameMode.hasNoShop || !!this.scene.findModifier(m => m instanceof RemoveHealShopModifier); + const shopTypeOptions = !removeHealShop ? getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1)) : []; const optionsYOffset = shopTypeOptions.length >= SHOP_OPTIONS_ROW_LIMIT ? -8 : -24; @@ -177,6 +186,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { this.options.push(option); } + // Add continue button + if (this.options.length === 0) { + const continueButtonText = addTextObject(this.scene, -24, optionsYOffset - 5, "Continue", TextStyle.MESSAGE); + continueButtonText.setName("text-continue-btn"); + this.continueButtonContainer.add(continueButtonText); + } + 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; @@ -240,16 +256,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); @@ -412,6 +436,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("Continue to the next wave."); + 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)); @@ -429,10 +461,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); @@ -448,7 +480,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; } @@ -533,7 +572,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 00000000000..094b8264bb1 --- /dev/null +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -0,0 +1,579 @@ +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"; + +export default class MysteryEncounterUiHandler extends UiHandler { + private cursorContainer: Phaser.GameObjects.Container; + private cursorObj?: Phaser.GameObjects.Image; + + private optionsContainer: Phaser.GameObjects.Container; + + 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); + } + + 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); + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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; + } + + 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); + } + } + } + + getCursor(): integer { + return this.cursor ? this.cursor : 0; + } + + 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; + switch (this.encounterOptions.length) { + case 2: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); + break; + case 3: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); + break; + case 4: + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, 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); + } + this.optionsContainer.add(optionText); + } + + // View Party Button + const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag("View Party", 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 + }); + } + } + + 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); + } + } + + 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(); + } + + eraseCursor(): void { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = undefined; + } + + /** + * + * @param show - if true does show, if false does hide + */ + 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("mysteryEncounter: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 66c777944d1..14dde61469c 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, @@ -208,7 +214,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); @@ -519,6 +525,10 @@ 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(); + // ui.setModeWithoutClear(Mode.SUMMARY, pokemon).then(() => this.clearOptions()); + return true; } } else if (button === Button.CANCEL) { this.clearOptions(); @@ -868,6 +878,9 @@ export default class PartyUiHandler extends MessageUiHandler { } } break; + case PartyUiMode.SELECT: + this.options.push(PartyOption.SELECT); + break; } this.options.push(PartyOption.SUMMARY); diff --git a/src/ui/text.ts b/src/ui/text.ts index 99a0436bba3..7764d07da15 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 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 250a21544dc..ae293a8dc6d 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -48,6 +48,7 @@ import BgmBar from "#app/ui/bgm-bar"; import RenameFormUiHandler from "./rename-form-ui-handler"; import RunHistoryUiHandler from "./run-history-ui-handler"; import RunInfoUiHandler from "./run-info-ui-handler"; +import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler"; export enum Mode { MESSAGE, @@ -88,6 +89,7 @@ export enum Mode { RENAME_POKEMON, RUN_HISTORY, RUN_INFO, + MYSTERY_ENCOUNTER } const transitionModes = [ @@ -188,6 +190,7 @@ export default class UI extends Phaser.GameObjects.Container { new RenameFormUiHandler(scene), new RunHistoryUiHandler(scene), new RunInfoUiHandler(scene), + new MysteryEncounterUiHandler(scene), ]; } diff --git a/src/utils.ts b/src/utils.ts index 173ea25b17c..731767dab51 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -561,6 +561,10 @@ export function isNullOrUndefined(object: any): boolean { return null === object || undefined === object; } +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.