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/images/mystery-encounters/exclaim.json b/public/images/mystery-encounters/exclaim.json new file mode 100644 index 00000000000..31231910097 --- /dev/null +++ b/public/images/mystery-encounters/exclaim.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "exclaim.png", + "format": "RGBA8888", + "size": { + "w": 32, + "h": 32 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 32 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + }, + "frame": { + "x": 0, + "y": 0, + "w": 32, + "h": 32 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + } +} 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/mud.json b/public/images/mystery-encounters/mud.json index 804eed36052..505a6fadd27 100644 --- a/public/images/mystery-encounters/mud.json +++ b/public/images/mystery-encounters/mud.json @@ -4,51 +4,72 @@ "image": "mud.png", "format": "RGBA8888", "size": { - "w": 18, - "h": 55 + "w": 14, + "h": 68 }, "scale": 1, "frames": [ { - "filename": "0002.png", + "filename": "0001.png", "rotated": false, "trimmed": true, "sourceSize": { - "w": 16, - "h": 16 + "w": 12, + "h": 20 }, "spriteSourceSize": { "x": 0, - "y": 3, - "w": 16, + "y": 0, + "w": 12, "h": 13 }, "frame": { "x": 1, "y": 1, - "w": 16, + "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": 16, - "h": 16 + "w": 12, + "h": 20 }, "spriteSourceSize": { "x": 0, - "y": 4, - "w": 16, - "h": 12 + "y": 1, + "w": 12, + "h": 16 }, "frame": { "x": 1, - "y": 16, - "w": 16, - "h": 12 + "y": 32, + "w": 12, + "h": 16 } }, { @@ -56,41 +77,20 @@ "rotated": false, "trimmed": true, "sourceSize": { - "w": 16, - "h": 16 + "w": 12, + "h": 20 }, "spriteSourceSize": { "x": 0, - "y": 7, - "w": 16, - "h": 9 - }, - "frame": { - "x": 1, - "y": 30, - "w": 16, - "h": 9 - } - }, - { - "filename": "0001.png", - "rotated": false, - "trimmed": true, - "sourceSize": { - "w": 16, - "h": 16 - }, - "spriteSourceSize": { - "x": 1, "y": 3, - "w": 14, - "h": 13 + "w": 12, + "h": 17 }, "frame": { "x": 1, - "y": 41, - "w": 14, - "h": 13 + "y": 50, + "w": 12, + "h": 17 } } ] @@ -99,6 +99,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:a9f7ae83758a2dffaacdaba2ee9dc2e2:0ebff9db47ce74a0ec049f5d74d589fa:c64f6b8befc3d5e9f836246d2b9536be$" + "smartupdate": "$TexturePacker:SmartUpdate:4f18a8effb8f01eb70f9f25b8294c1bf:ad663a73c51f780bbf45d00a52519553:c64f6b8befc3d5e9f836246d2b9536be$" } } diff --git a/public/images/mystery-encounters/mud.png b/public/images/mystery-encounters/mud.png index 89f174bef74..2ba7cb00047 100644 Binary files a/public/images/mystery-encounters/mud.png and b/public/images/mystery-encounters/mud.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 6cde5929308..4c0f31b051e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,43 +1,42 @@ import Phaser from "phaser"; import UI from "./ui/ui"; -import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases"; -import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; -import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; -import {Constructor, isNullOrUndefined} from "#app/utils"; +import { LevelCapPhase, LoginPhase, MessagePhase, MovePhase, NewBiomeEncounterPhase, NextEncounterPhase, ReturnPhase, SelectBiomePhase, ShowTrainerPhase, SwitchPhase, TitlePhase, TurnInitPhase } from "./phases"; +import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon"; +import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "./data/pokemon-species"; +import { Constructor, isNullOrUndefined } from "#app/utils"; import * as Utils from "./utils"; -import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier"; +import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, TerastallizeModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims"; import { Phase } from "./phase"; import { initGameSpeed } from "./system/game-speed"; import { Arena, ArenaBase } from "./field/arena"; import { GameData } from "./system/game-data"; -import { TextStyle, addTextObject, getTextColor } from "./ui/text"; +import { addTextObject, getTextColor, TextStyle } from "./ui/text"; import { allMoves } from "./data/move"; -import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue, PokemonHeldItemModifierType } from "./modifier/modifier-type"; +import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue, ModifierPoolType, PokemonHeldItemModifierType } from "./modifier/modifier-type"; import AbilityBar from "./ui/ability-bar"; -import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; -import { allAbilities } from "./data/ability"; +import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr } from "./data/ability"; import Battle, { BattleType, FixedBattleConfig } from "./battle"; 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"; import { addUiThemeOverrides } from "./ui/ui-theme"; import PokemonData from "./system/pokemon-data"; import { Nature } from "./data/nature"; -import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges } from "./data/pokemon-forms"; +import { pokemonFormChanges, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "./data/pokemon-forms"; import { FormChangePhase, QuietFormChangePhase } from "./form-change-phase"; import { getTypeRgb } from "./data/type"; import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler"; @@ -50,8 +49,8 @@ import CandyBar from "./ui/candy-bar"; import { Variant, variantData } from "./data/variant"; import { Localizable } from "#app/interfaces/locales"; import * as Overrides from "./overrides"; -import {InputsController} from "./inputs-controller"; -import {UiInputs} from "./ui-inputs"; +import { InputsController } from "./inputs-controller"; +import { UiInputs } from "./ui-inputs"; import { NewArenaEvent } from "./events/battle-scene"; import ArenaFlyout from "./ui/arena-flyout"; import { EaseType } from "#enums/ease-type"; @@ -68,7 +67,7 @@ import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; -import { mysteryEncountersByBiome, allMysteryEncounters, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; +import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WIGHT_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"; @@ -2651,8 +2650,8 @@ export default class BattleScene extends SceneBase { return encounter; } - // Common / Uncommon / Rare / Super Rare - const tierWeights = [64, 40, 21, 3]; + // 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 => { diff --git a/src/battle.ts b/src/battle.ts index ab47eac7993..2c033ce38bf 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -206,7 +206,7 @@ export default class Battle { getBgmOverride(scene: BattleScene): string { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { - if (!this.started && this.trainer.config.encounterBgm && this.trainer.getEncounterMessages()?.length) { + if (!this.started && this.trainer.config.encounterBgm && this.trainer.getEncounterMessages().length) { return `encounter_${this.trainer.getEncounterBgm()}`; } if (scene.musicPreference === 0) { diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index d4dbb8ec350..6f2f293d099 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -6,6 +6,7 @@ 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 fs from 'vite-plugin-fs/browser'; export enum AnimFrameTarget { @@ -102,6 +103,16 @@ 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 +} + export class AnimConfig { public id: integer; public graphic: string; @@ -302,7 +313,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; } @@ -320,7 +331,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 { @@ -382,7 +393,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; @@ -412,7 +423,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(); } @@ -424,7 +435,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); } @@ -444,6 +457,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 => { @@ -511,6 +525,28 @@ 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 encounterAnimIds = Utils.getEnumValues(EncounterAnim); + const encounterAnimFetches = []; + for (const anim of anims) { + if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { + continue; + } + const encounterAnimId = encounterAnimIds[anim]; + encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) + .then(response => response.json()) + .then(cas => encounterAnims.set(encounterAnimId, new AnimConfig(cas)))); + } + await Promise.allSettled(encounterAnimFetches); +} + export function initMoveChargeAnim(scene: BattleScene, chargeAnim: ChargeAnim): Promise { return new Promise(resolve => { if (chargeAnims.has(chargeAnim)) { @@ -565,6 +601,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(); @@ -672,14 +718,16 @@ export abstract class BattleAnim { public target: Pokemon; 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; this.target = target; this.sprites = []; + this.playOnEmptyField = playOnEmptyField; } abstract getAnim(): AnimConfig; @@ -753,7 +801,7 @@ export abstract class BattleAnim { const user = !isOppAnim ? this.user : this.target; const target = !isOppAnim ? this.target : this.user; - if (!target.isOnField()) { + if (!target.isOnField() && !this.playOnEmptyField) { if (callback) { callback(); } @@ -977,13 +1025,183 @@ 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) => { + try { + 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); + } + } catch (ignored) { + console.log("index is no longer valid"); + } + }; + setSpritePriority(frame.priority); + } + moveSprite.setFrame(frame.graphicFrame); + + const graphicFrameData = frameData.get(frame.target).get(graphicIndex); + 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.has(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; - constructor(commonAnim: CommonAnim, user: Pokemon, target?: Pokemon) { - super(user, target || user); + constructor(commonAnim: CommonAnim, user: Pokemon, target?: Pokemon, playOnEmptyField: boolean = false) { + super(user, target || user, playOnEmptyField); this.commonAnim = commonAnim; } @@ -1045,6 +1263,24 @@ export class MoveChargeAnim extends MoveAnim { } } +export class EncounterBattleAnim extends BattleAnim { + public encounterAnim: EncounterAnim; + + constructor(encounterAnim: EncounterAnim, user: Pokemon, target?: Pokemon) { + super(user, target || user); + + this.encounterAnim = encounterAnim; + } + + getAnim(): AnimConfig { + return encounterAnims.get(this.encounterAnim); + } + + isOppAnim(): boolean { + return false; + } +} + 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 e78977b56ba..060bf3efa19 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1554,7 +1554,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { const cancelled = new Utils.BooleanHolder(false); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); if (!cancelled.value) { - const mysteryEncounterBattleEffects = pokemon.summonData.mysteryEncounterBattleEffects; + const mysteryEncounterBattleEffects = pokemon.mysteryEncounterBattleEffects; if (mysteryEncounterBattleEffects) { mysteryEncounterBattleEffects(pokemon); } diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index f9a339db8c2..1aba405745a 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -4,7 +4,7 @@ import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "../../../battle-scene"; -import { AddPokeballModifierType } from "../../../modifier/modifier-type"; +import { AddPokeballModifierType } from "#app/modifier/modifier-type"; import { PokeballType } from "../../pokeball"; import { getPokemonSpecies } from "../../pokemon-species"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; @@ -13,9 +13,9 @@ import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveE import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** i18n namespace for encounter */ -const namespace = "mysteryEncounter:dark_deal"; +const namespace = "mysteryEncounter:darkDeal"; -// Exclude Ultra Beasts, Paradox, Necrozma, Eternatus, and egg-locked mythicals +/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, and egg-locked mythicals */ const excludedBosses = [ Species.NECROZMA, Species.COSMOG, @@ -68,6 +68,11 @@ const excludedBosses = [ 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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DARK_DEAL) .withEncounterTier(MysteryEncounterTier.ROGUE) @@ -86,32 +91,32 @@ export const DarkDealEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, { - speaker: `${namespace}_speaker`, - text: `${namespace}_intro_dialogue`, + 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`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - speaker: `${namespace}_speaker`, - text: `${namespace}_option_1_selected`, + speaker: `${namespace}:speaker`, + text: `${namespace}:option:1:selected_dialogue`, }, { - text: `${namespace}_option_1_selected_message`, + text: `${namespace}:option:1:selected_message`, }, ], }) @@ -121,10 +126,7 @@ export const DarkDealEncounter: IMysteryEncounter = const removedPokemon = getRandomPlayerPokemon(scene, false, true); scene.removePokemonFromPlayerParty(removedPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken( - "pokeName", - removedPokemon.name - ); + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.name); // Store removed pokemon types scene.currentBattle.mysteryEncounter.misc = [ @@ -153,7 +155,6 @@ export const DarkDealEncounter: IMysteryEncounter = pokemonConfig.formIndex = 0; } const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.75, pokemonConfigs: [pokemonConfig], }; return initBattleWithEnemyConfig(scene, config); @@ -162,12 +163,12 @@ export const DarkDealEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, selected: [ { - speaker: `${namespace}_speaker`, - text: `${namespace}_option_2_selected`, + speaker: `${namespace}:speaker`, + text: `${namespace}:option:2:selected`, }, ], }, @@ -179,7 +180,7 @@ export const DarkDealEncounter: IMysteryEncounter = ) .withOutroDialogue([ { - text: `${namespace}_outro` + text: `${namespace}:outro` } ]) .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 index f3bb422aa64..131dc68aa14 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -13,8 +13,13 @@ import IMysteryEncounter, { } from "../mystery-encounter"; /** i18n namespace for encounter */ -const namespace = "mysteryEncounter:department_store_sale"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) .withEncounterTier(MysteryEncounterTier.COMMON) @@ -36,21 +41,21 @@ export const DepartmentStoreSaleEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, { - text: `${namespace}_intro_dialogue`, - speaker: `${namespace}_speaker`, + text: `${namespace}:intro_dialogue`, + speaker: `${namespace}:speaker`, }, ]) - .withHideIntroVisuals(false) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withSimpleOption( { - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, }, async (scene: BattleScene) => { // Choose TMs @@ -75,8 +80,8 @@ export const DepartmentStoreSaleEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, }, async (scene: BattleScene) => { // Choose Vitamins @@ -99,8 +104,8 @@ export const DepartmentStoreSaleEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, }, async (scene: BattleScene) => { // Choose X Items @@ -123,8 +128,8 @@ export const DepartmentStoreSaleEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_4_label`, - buttonTooltip: `${namespace}_option_4_tooltip`, + buttonLabel: `${namespace}:option:4:label`, + buttonTooltip: `${namespace}:option:4:tooltip`, }, async (scene: BattleScene) => { // Choose Pokeballs @@ -149,4 +154,9 @@ export const DepartmentStoreSaleEncounter: IMysteryEncounter = 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 index bc369ee4c4b..21db15ac7e0 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -19,8 +19,13 @@ import IMysteryEncounter, { } from "../mystery-encounter"; /** i18n namespace for the encounter */ -const namespace = "mysteryEncounter:field_trip"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIELD_TRIP) .withEncounterTier(MysteryEncounterTier.COMMON) @@ -44,27 +49,27 @@ export const FieldTripEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, { - text: `${namespace}_intro_dialogue`, - speaker: `${namespace}_speaker`, + text: `${namespace}:intro_dialogue`, + speaker: `${namespace}:speaker`, }, ]) - .withHideIntroVisuals(false) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withAutoHideIntroVisuals(false) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, - secondOptionPrompt: `${namespace}_second_option_prompt`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + secondOptionPrompt: `${namespace}:second_option_prompt`, selected: [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ], }) @@ -82,11 +87,11 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[0].dialogue.selected = [ { - text: `${namespace}_option_incorrect`, - speaker: `${namespace}_speaker`, + text: `${namespace}:incorrect`, + speaker: `${namespace}:speaker`, }, { - text: `${namespace}_lesson_learned`, + text: `${namespace}:lesson_learned`, }, ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); @@ -95,7 +100,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.setDialogueToken("move", move.getName()); encounter.options[0].dialogue.selected = [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ]; setEncounterExp(scene, [pokemon.id], 100); @@ -133,12 +138,12 @@ export const FieldTripEncounter: IMysteryEncounter = new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, - secondOptionPrompt: `${namespace}_second_option_prompt`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + secondOptionPrompt: `${namespace}:second_option_prompt`, selected: [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ], }) @@ -156,11 +161,11 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[1].dialogue.selected = [ { - text: `${namespace}_option_incorrect`, - speaker: `${namespace}_speaker`, + text: `${namespace}:incorrect`, + speaker: `${namespace}:speaker`, }, { - text: `${namespace}_lesson_learned`, + text: `${namespace}:lesson_learned`, }, ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); @@ -169,7 +174,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.setDialogueToken("move", move.getName()); encounter.options[1].dialogue.selected = [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ]; setEncounterExp(scene, [pokemon.id], 100); @@ -207,12 +212,12 @@ export const FieldTripEncounter: IMysteryEncounter = new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, - secondOptionPrompt: `${namespace}_second_option_prompt`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + secondOptionPrompt: `${namespace}:second_option_prompt`, selected: [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ], }) @@ -230,11 +235,11 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[2].dialogue.selected = [ { - text: `${namespace}_option_incorrect`, - speaker: `${namespace}_speaker`, + text: `${namespace}:incorrect`, + speaker: `${namespace}:speaker`, }, { - text: `${namespace}_lesson_learned`, + text: `${namespace}:lesson_learned`, }, ]; setEncounterExp( @@ -247,7 +252,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.setDialogueToken("move", move.getName()); encounter.options[2].dialogue.selected = [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ]; setEncounterExp(scene, [pokemon.id], 100); 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..d61a0c1ff4e --- /dev/null +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -0,0 +1,252 @@ +import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes, } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } 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 } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; + +/** 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: IMysteryEncounter = + 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: volcaronaSpecies.getSpriteId(false), + fileRoot: "pokemon", + repeat: true, + hidden: true, + hasShadow: true, + x: -20 + }, + { + spriteKey: volcaronaSpecies.getSpriteId(true ), + fileRoot: "pokemon", + repeat: true, + hidden: true, + hasShadow: true, + x: 20 + }, + ]; + + // Load animations/sfx for Volcarona moves + initCustomMovesForEncounter(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 }, + null, + () => { + 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.name); + queueEncounterMessage(scene, `${namespace}:option:2:target_burned`); + } + } + + // No rewards + leaveEncounterWithoutBattle(scene, true); + } + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.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 }, + null, + () => { + 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 = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]); + scene.addModifier(charcoal.type.newModifier(leadPokemon), true); + scene.updateModifiers(); + scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name); + 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 index db7495f6cfa..2d142f46d4e 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -27,10 +27,16 @@ import IMysteryEncounter, { } from "../mystery-encounter"; import { MoveRequirement } from "../mystery-encounter-requirements"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; /** the i18n namespace for the encounter */ -const namespace = "mysteryEncounter:fight_or_flight"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.FIGHT_OR_FLIGHT @@ -97,23 +103,23 @@ export const FightOrFlightEncounter: IMysteryEncounter = const primaryPokemon = encounter.options[1].primaryPokemon; if (primaryPokemon) { // Use primaryPokemon to execute the thievery - encounter.options[1].dialogue.buttonTooltip = `${namespace}_option_2_steal_tooltip`; + encounter.options[1].dialogue.buttonTooltip = `${namespace}:option:2:tooltip_special`; } else { - encounter.options[1].dialogue.buttonTooltip = `${namespace}_option_2_tooltip`; + encounter.options[1].dialogue.buttonTooltip = `${namespace}:option:2:tooltip`; } return true; }) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withSimpleOption( { - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}_option_1_selected_message`, + text: `${namespace}:option:1:selected`, }, ], }, @@ -130,8 +136,8 @@ export const FightOrFlightEncounter: IMysteryEncounter = .withOptionMode(EncounterOptionMode.DEFAULT_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`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, }) .withOptionPhase(async (scene: BattleScene) => { // Pick steal @@ -143,7 +149,7 @@ export const FightOrFlightEncounter: IMysteryEncounter = const primaryPokemon = encounter.options[1].primaryPokemon; if (primaryPokemon) { // Use primaryPokemon to execute the thievery - await showEncounterText(scene, `${namespace}_option_2_steal_result`); + await showEncounterText(scene, `${namespace}:option:2:steal_result`); leaveEncounterWithoutBattle(scene); return; } @@ -154,16 +160,16 @@ export const FightOrFlightEncounter: IMysteryEncounter = const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { - pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", pokemon.name); - queueEncounterMessage(pokemon.scene, `${namespace}_boss_enraged`); + pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(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)); }; - await showEncounterText(scene, `${namespace}_option_2_bad_result`); + await showEncounterText(scene, `${namespace}:option:2:bad_result`); await initBattleWithEnemyConfig(scene, config); } else { // Steal item (37.5%) // Display result message then proceed to rewards - await showEncounterText(scene, `${namespace}_option_2_good_result`); + await showEncounterText(scene, `${namespace}:option:2:good_result`); leaveEncounterWithoutBattle(scene); } }) @@ -171,11 +177,11 @@ export const FightOrFlightEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, selected: [ { - text: `${namespace}_option_3_selected`, + text: `${namespace}:option:3:selected`, }, ], }, diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index 9e25e003e1b..7becc65a160 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -5,7 +5,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { applyDamageToPokemon, leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; +import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; const OPTION_1_REQUIRED_MOVE = Moves.SURF; const OPTION_2_REQUIRED_MOVE = Moves.FLY; @@ -16,12 +17,12 @@ const OPTION_2_REQUIRED_MOVE = Moves.FLY; */ const DAMAGE_PERCENTAGE: number = 25; /** The i18n namespace for the encounter */ -const namepsace = "mysteryEncounter:lostAtSea"; +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 [mysteryEncountersByBiome](../mystery-encounters.ts) + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} */ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.LOST_AT_SEA) .withEncounterTier(MysteryEncounterTier.COMMON) @@ -35,7 +36,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with y: 3, }, ]) - .withIntroDialogue([{ text: `${namepsace}:intro` }]) + .withIntroDialogue([{ text: `${namespace}:intro` }]) .withOnInit((scene: BattleScene) => { const { mysteryEncounter } = scene.currentBattle; @@ -45,22 +46,22 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with return true; }) - .withTitle(`${namepsace}:title`) - .withDescription(`${namepsace}:description`) - .withQuery(`${namepsace}:query`) + .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/ new MysteryEncounterOptionBuilder() .withPokemonCanLearnMoveRequirement(OPTION_1_REQUIRED_MOVE) .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) .withDialogue({ - buttonLabel: `${namepsace}:option:1:label`, - disabledButtonLabel: `${namepsace}:option:1:label_disabled`, - buttonTooltip: `${namepsace}:option:1:tooltip`, - disabledButtonTooltip: `${namepsace}:option:1:tooltip_disabled`, + buttonLabel: `${namespace}:option:1:label`, + disabledButtonLabel: `${namespace}:option:1:label_disabled`, + buttonTooltip: `${namespace}:option:1:tooltip`, + disabledButtonTooltip: `${namespace}:option:1:tooltip_disabled`, selected: [ { - text: `${namepsace}:option:1:selected`, + text: `${namespace}:option:1:selected`, }, ], }) @@ -73,13 +74,13 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withPokemonCanLearnMoveRequirement(OPTION_2_REQUIRED_MOVE) .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) .withDialogue({ - buttonLabel: `${namepsace}:option:2:label`, - disabledButtonLabel: `${namepsace}:option:2:label_disabled`, - buttonTooltip: `${namepsace}:option:2:tooltip`, - disabledButtonTooltip: `${namepsace}:option:2:tooltip_disabled`, + buttonLabel: `${namespace}:option:2:label`, + disabledButtonLabel: `${namespace}:option:2:label_disabled`, + buttonTooltip: `${namespace}:option:2:tooltip`, + disabledButtonTooltip: `${namespace}:option:2:tooltip_disabled`, selected: [ { - text: `${namepsace}:option:2:selected`, + text: `${namespace}:option:2:selected`, }, ], }) @@ -89,11 +90,11 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withSimpleOption( // Option 3: Wander aimlessly { - buttonLabel: `${namepsace}:option:3:label`, - buttonTooltip: `${namepsace}:option:3:tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, selected: [ { - text: `${namepsace}:option:3:selected`, + text: `${namespace}:option:3:selected`, }, ], }, @@ -113,7 +114,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withOutroDialogue([ { - text: `${namepsace}:outro`, + text: `${namespace}:outro`, }, ]) .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index f9fe8ab3d42..382dc1f212e 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -21,8 +21,13 @@ import IMysteryEncounter, { } from "../mystery-encounter"; /** the i18n namespace for the encounter */ -const namespace = "mysteryEncounter:mysterious_challengers"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.MYSTERIOUS_CHALLENGERS @@ -32,7 +37,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = .withIntroSpriteConfigs([]) // These are set in onInit() .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, ]) .withOnInit((scene: BattleScene) => { @@ -94,7 +99,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); encounter.enemyPartyConfigs.push({ trainerConfig: brutalConfig, - levelAdditiveMultiplier: 1.1, + levelAdditiveMultiplier: 1, female: female, }); @@ -121,16 +126,16 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = return true; }) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withSimpleOption( { - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }, @@ -151,11 +156,11 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }, @@ -176,11 +181,11 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }, @@ -204,7 +209,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = ) .withOutroDialogue([ { - text: `${namespace}_outro_win`, + 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 index 63ceea1dac9..e8db89a6961 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -1,26 +1,26 @@ 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 { 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 "../../../battle-scene"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; +import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +/** 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: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.MYSTERIOUS_CHEST - ) + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 - .withHideIntroVisuals(false) + .withAutoHideIntroVisuals(false) .withIntroSpriteConfigs([ { spriteKey: "chest_blue", @@ -34,21 +34,21 @@ export const MysteriousChestEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: "mysteryEncounter:mysterious_chest_intro_message", + text: "${namespace}:intro:message", }, ]) - .withTitle("mysteryEncounter:mysterious_chest_title") - .withDescription("mysteryEncounter:mysterious_chest_description") - .withQuery("mysteryEncounter:mysterious_chest_query") + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: "mysteryEncounter:mysterious_chest_option_1_label", - buttonTooltip: "mysteryEncounter:mysterious_chest_option_1_tooltip", + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: "mysteryEncounter:mysterious_chest_option_1_selected_message", + text: `${namespace}:option:1:selected`, }, ], }) @@ -73,7 +73,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result"); + queueEncounterMessage(scene, `${namespace}:option:1:normal`); leaveEncounterWithoutBattle(scene); } else if (roll > 40) { // Choose between 3 ULTRA tier items (20%) @@ -85,7 +85,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result"); + queueEncounterMessage(scene, `${namespace}:option:1:good`); leaveEncounterWithoutBattle(scene); } else if (roll > 36) { // Choose between 2 ROGUE tier items (4%) @@ -93,7 +93,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], }); // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result"); + queueEncounterMessage(scene, `${namespace}:option:1:great`); leaveEncounterWithoutBattle(scene); } else if (roll > 35) { // Choose 1 MASTER tier item (1%) @@ -101,7 +101,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.MASTER], }); // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result"); + queueEncounterMessage(scene, `${namespace}:option:1:amazing`); leaveEncounterWithoutBattle(scene); } else { // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) @@ -109,12 +109,12 @@ export const MysteriousChestEncounter: IMysteryEncounter = scene, true ); - koPlayerPokemon(highestLevelPokemon); + koPlayerPokemon(scene, highestLevelPokemon); scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); // 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, "mysteryEncounter:mysterious_chest_option_1_bad_result").then(() => { + await showEncounterText(scene, `${namespace}:option:1:bad`).then(() => { leaveEncounterWithoutBattle(scene); }); } @@ -123,11 +123,11 @@ export const MysteriousChestEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: "mysteryEncounter:mysterious_chest_option_2_label", - buttonTooltip: "mysteryEncounter:mysterious_chest_option_2_tooltip", + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, selected: [ { - text: "mysteryEncounter:mysterious_chest_option_2_selected_message", + text: `${namespace}:option:2:selected`, }, ], }, diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index aa7fd19391f..147ce2a5e2c 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -5,7 +5,6 @@ import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, Myste import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases"; -import i18next from "i18next"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import { EnemyPokemon } from "#app/field/pokemon"; import { PokeballType } from "#app/data/pokeball"; @@ -14,11 +13,19 @@ 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 { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; /** the i18n namespace for the encounter */ -const namespace = "mysteryEncounter:safari_zone"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) .withEncounterTier(MysteryEncounterTier.GREAT) @@ -36,32 +43,33 @@ export const SafariZoneEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, ]) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption(new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.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`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}_option_1_selected_message`, + text: `${namespace}:option:1:selected`, }, ], }) .withOptionPhase(async (scene: BattleScene) => { // Start safari encounter const encounter = scene.currentBattle.mysteryEncounter; - encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE; + encounter.encounterVariant = MysteryEncounterVariant.CONTINUOUS_ENCOUNTER; 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"); @@ -75,11 +83,11 @@ export const SafariZoneEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, selected: [ { - text: `${namespace}_option_2_selected_message`, + text: `${namespace}:option:2:selected`, }, ], }, @@ -110,11 +118,11 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_pokeball_option_label`, - buttonTooltip: `${namespace}_pokeball_option_tooltip`, + buttonLabel: `${namespace}:safari:1:label`, + buttonTooltip: `${namespace}:safari:1:tooltip`, selected: [ { - text: `${namespace}_pokeball_option_selected`, + text: `${namespace}:safari:1:selected`, } ], }) @@ -144,11 +152,11 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_bait_option_label`, - buttonTooltip: `${namespace}_bait_option_tooltip`, + buttonLabel: `${namespace}:safari:2:label`, + buttonTooltip: `${namespace}:safari:2:tooltip`, selected: [ { - text: `${namespace}_bait_option_selected`, + text: `${namespace}:safari:2:selected`, }, ], }) @@ -162,9 +170,9 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to increase flee stage +1 const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); if (!fleeChangeResult) { - await showEncounterText(scene, i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 1500, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}:safari:busy_eating`), 1000, false ); } else { - await showEncounterText(scene, i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 1500, false); + await showEncounterText(scene, getEncounterText(scene, `${namespace}:safari:eating`), 1000, false); } await doEndTurn(scene, 1); @@ -174,11 +182,11 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_mud_option_label`, - buttonTooltip: `${namespace}_mud_option_tooltip`, + buttonLabel: `${namespace}:safari:3:label`, + buttonTooltip: `${namespace}:safari:3:tooltip`, selected: [ { - text: `${namespace}_mud_option_selected`, + text: `${namespace}:safari:3:selected`, }, ], }) @@ -191,9 +199,9 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ // 80% chance to decrease catch stage -1 const catchChangeResult = tryChangeCatchStage(scene, -1, 8); if (!catchChangeResult) { - await showEncounterText(scene, i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 1500, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}:safari:beside_itself_angry`), 1000, false ); } else { - await showEncounterText(scene, i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 1500, false ); + await showEncounterText(scene, getEncounterText(scene, `${namespace}:safari:angry`), 1000, false ); } await doEndTurn(scene, 2); @@ -203,8 +211,8 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_flee_option_label`, - buttonTooltip: `${namespace}_flee_option_tooltip`, + buttonLabel: `${namespace}:safari:4:label`, + buttonTooltip: `${namespace}:safari:4:tooltip`, }) .withOptionPhase(async (scene: BattleScene) => { // Flee option @@ -226,7 +234,8 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ async function summonSafariPokemon(scene: BattleScene) { const encounter = scene.currentBattle.mysteryEncounter; // Message pokemon remaining - scene.queueMessage(i18next.t(`${namespace}_remaining_count`, { remainingCount: encounter.misc.safariPokemonRemaining}), null, true); + 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 @@ -274,7 +283,8 @@ async function summonSafariPokemon(scene: BattleScene) { scene.unshiftPhase(new SummonPhase(scene, 0, false)); - showEncounterText(scene, i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), 1500, 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) { @@ -304,16 +314,16 @@ async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); - scene.time.delayedCall(512, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("pb_throw"); + // Trainer throw frames scene.trainer.setFrame("2"); - scene.time.delayedCall(256, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { scene.trainer.setFrame("3"); - scene.time.delayedCall(768, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); }); }); @@ -366,20 +376,20 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); - scene.time.delayedCall(512, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[0], () => { + scene.playSound("pb_throw"); + // Trainer throw frames scene.trainer.setFrame("2"); - scene.time.delayedCall(256, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[1], () => { scene.trainer.setFrame("3"); - scene.time.delayedCall(768, () => { + scene.time.delayedCall(TRAINER_THROW_ANIMATION_TIMES[2], () => { scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); }); }); @@ -395,32 +405,39 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { + scene.time.delayedCall(200, () => { mud.setFrame("0003.png"); - scene.time.delayedCall(512, () => { + scene.time.delayedCall(400, () => { mud.setFrame("0004.png"); }); }); - scene.time.delayedCall(1536, () => { - 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); - } - }); + // 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); + } + }); + } }); } }); @@ -456,14 +473,15 @@ function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number return true; } -async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: string) { - const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; - const isFlee = isPokemonFlee(pokemon, scene.currentBattle.mysteryEncounter.misc.fleeStage); +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 (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + if (encounter.misc.safariPokemonRemaining > 0) { await summonSafariPokemon(scene); initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); } else { @@ -471,7 +489,7 @@ async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: stri leaveEncounterWithoutBattle(scene, true); } } else { - scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 1000); + 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 index b1beee30247..0325eb2f2a6 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -5,16 +5,21 @@ 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 i18next from "i18next"; import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; -import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** the i18n namespace for this encounter */ -const namespace = "mysteryEncounter:shady_vitamin_dealer"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.SHADY_VITAMIN_DEALER @@ -44,26 +49,26 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, { - text: `${namespace}_intro_dialogue`, - speaker: `${namespace}_speaker`, + text: `${namespace}:intro_dialogue`, + speaker: `${namespace}:speaker`, }, ]) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 .withDialogue({ - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ], }) @@ -90,7 +95,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = // If pokemon meets primary pokemon reqs, it can be selected const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); if (!meetsReqs) { - return i18next.t(`${namespace}_invalid_selection`); + return getEncounterText(scene, `${namespace}:invalid_selection`); } return null; @@ -118,20 +123,19 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = const chosenPokemon = encounter.misc.chosenPokemon; // Pokemon takes 1/3 max HP damage - const damage = Math.round(chosenPokemon.getMaxHp() / 3); - chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); + 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`); + queueEncounterMessage(scene, `${namespace}:bad_poison`); } else { // Pokemon immune or something else prevents status - queueEncounterMessage(scene, `${namespace}_damage_only`); + queueEncounterMessage(scene, `${namespace}:damage_only`); } } else { - queueEncounterMessage(scene, `${namespace}_damage_only`); + queueEncounterMessage(scene, `${namespace}:damage_only`); } setEncounterExp(scene, [chosenPokemon.id], 100); @@ -145,11 +149,11 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5 .withDialogue({ - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, selected: [ { - text: `${namespace}_option_selected`, + text: `${namespace}:option:selected`, }, ], }) @@ -176,7 +180,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = // If pokemon meets primary pokemon reqs, it can be selected const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); if (!meetsReqs) { - return i18next.t(`${namespace}_invalid_selection`); + return getEncounterText(scene, `${namespace}:invalid_selection`); } return null; @@ -207,13 +211,13 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = if (randSeedInt(10) < 2) { if (chosenPokemon.trySetStatus(StatusEffect.POISON)) { // Poison applied - queueEncounterMessage(scene, `${namespace}_poison`); + queueEncounterMessage(scene, `${namespace}:poison`); } else { // Pokemon immune or something else prevents status - queueEncounterMessage(scene, `${namespace}_no_bad_effects`); + queueEncounterMessage(scene, `${namespace}:no_bad_effects`); } } else { - queueEncounterMessage(scene, `${namespace}_no_bad_effects`); + queueEncounterMessage(scene, `${namespace}:no_bad_effects`); } setEncounterExp(scene, [chosenPokemon.id], 100); @@ -224,8 +228,8 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = ) .withSimpleOption( { - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, }, async (scene: BattleScene) => { // Leave encounter with no rewards or exp diff --git a/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts deleted file mode 100644 index 0bb7f4bfd52..00000000000 --- a/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts +++ /dev/null @@ -1,166 +0,0 @@ -import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { BerryType } from "#enums/berry-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import * as Utils from "../../../utils"; -import { getPokemonSpecies } from "../../pokemon-species"; -import { Status, StatusEffect } from "../../status-effect"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoveRequirement } from "../mystery-encounter-requirements"; -import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; -import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; - -/** i18n namespace for the encounter */ -const namespace = "mysteryEncounter:sleeping_snorlax"; - -export const SleepingSnorlaxEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.SLEEPING_SNORLAX - ) - .withEncounterTier(MysteryEncounterTier.ULTRA) - .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_message`, - }, - ]) - .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, - spriteScale: 1.5 - }; - const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 2, - pokemonConfigs: [pokemonConfig], - }; - encounter.enemyPartyConfigs = [config]; - 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_message`, - }, - ], - }, - async (scene: BattleScene) => { - // Pick battle - // TODO: do we want special rewards for this? - // setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: 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_message`, - }, - ], - }, - async (scene: BattleScene) => { - const instance = scene.currentBattle.mysteryEncounter; - let roll: integer; - scene.executeWithSeedOffset(() => { - roll = Utils.randSeedInt(16, 0); - }, scene.currentBattle.waveIndex); - - // Half Snorlax exp to entire party - setEncounterExp( - scene, - scene.getParty().map((p) => p.id), - 98 - ); - - if (roll > 4) { - // Fall asleep and get a sitrus berry (75%) - const p = instance.primaryPokemon; - p.status = new Status(StatusEffect.SLEEP, 0, 3); - p.updateInfo(true); - // const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); - const sitrus = generateModifierTypeOption( - scene, - modifierTypes.BERRY, - [BerryType.SITRUS] - ); - - setEncounterRewards(scene, { - guaranteedModifierTypeOptions: [sitrus], - fillRemaining: false, - }); - queueEncounterMessage(scene, `${namespace}_option_2_bad_result`); - leaveEncounterWithoutBattle(scene); - } else { - // Heal to full (25%) - for (const pokemon of scene.getParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(); - for (const move of pokemon.moveset) { - move.ppUsed = 0; - } - pokemon.updateInfo(true); - } - - queueEncounterMessage(scene, `${namespace}_option_2_good_result`); - leaveEncounterWithoutBattle(scene); - } - } - ) - .withOption( - new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.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`, - }) - .withOptionPhase(async (scene: BattleScene) => { - // Steal the Snorlax's Leftovers - const instance = scene.currentBattle.mysteryEncounter; - setEncounterRewards(scene, { - guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], - fillRemaining: false, - }); - queueEncounterMessage(scene, `${namespace}_option_3_good_result`); - // Snorlax exp to Pokemon that did the stealing - setEncounterExp(scene, [instance.primaryPokemon.id], 189); - leaveEncounterWithoutBattle(scene); - }) - .build() - ) - .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..c905e0bd222 --- /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 IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; +import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MoveRequirement } from "../mystery-encounter-requirements"; +import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, initCustomMovesForEncounter, 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 { PartyHealPhase } from "#app/phases"; + +/** 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: IMysteryEncounter = + 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 + initCustomMovesForEncounter(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( + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.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/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 019eac98d7c..b199b43e952 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -1,25 +1,30 @@ -import { EnemyPartyConfig, hideMysteryEncounterIntroVisuals, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { modifierTypes, } from "#app/modifier/modifier-type"; +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, 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 "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; import { Nature } from "#app/data/nature"; -import { PlayerPokemon } from "#app/field/pokemon"; -import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +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 { StatChangePhase } from "#app/phases"; +import { BattleStat } from "#app/data/battle-stat"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:theStrongStuff"; export const TheStrongStuffEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.THE_STRONG_STUFF - ) + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 .withHideWildIntroMessage(true) - .withHideIntroVisuals(false) + .withAutoHideIntroVisuals(false) .withIntroSpriteConfigs([ { spriteKey: "berry_juice", @@ -52,19 +57,35 @@ export const TheStrongStuffEncounter: IMysteryEncounter = // Calculate boss mon const config: EnemyPartyConfig = { levelAdditiveMultiplier: 1, + disableSwitch: true, pokemonConfigs: [ { species: getPokemonSpecies(Species.SHUCKLE), isBoss: true, bossSegments: 5, - spriteScale: 2, + spriteScale: 1.5, nature: Nature.BOLD, - // moves: [Moves.INFESTATION, Moves.SALT_CURE, Moves.STEALTH_ROCK, Moves.RECOVER] + moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], + modifierTypes: [ + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.APICOT]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType, + generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType + ], + 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]; + initCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]); + return true; }) .withTitle(`${namespace}:title`) @@ -83,8 +104,8 @@ export const TheStrongStuffEncounter: IMysteryEncounter = async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; // Do blackout and hide intro visuals during blackout - scene.time.delayedCall(700, () => { - hideMysteryEncounterIntroVisuals(scene); + 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 @@ -110,26 +131,19 @@ export const TheStrongStuffEncounter: IMysteryEncounter = highestBst = party[0]; } - console.log("BST pre change: " + highestBst.getSpeciesForm().baseStats); - highestBst.getSpeciesForm().baseStats = [...highestBst.getSpeciesForm().baseStats].map(v => v - 20); - console.log("Species BST: " + getPokemonSpecies(highestBst.getSpeciesForm().speciesId).baseStats); - console.log("Pokemon BST: " + highestBst.getSpeciesForm().baseStats); - highestBst.calculateStats(); - highestBst.updateInfo(); + modifyPlayerPokemonBST(highestBst, -20); for (const pokemon of party) { if (highestBst.id === pokemon.id) { continue; } - pokemon.getSpeciesForm().baseStats = [...pokemon.getSpeciesForm().baseStats].map(v => v + 10); - // pokemon.summonData.getSpeciesForm()Form.baseStats = pokemon.getSpeciesForm().baseStats; - pokemon.calculateStats(); - pokemon.updateInfo(); + modifyPlayerPokemonBST(pokemon, 10); } encounter.setDialogueToken("highBstPokemon", highestBst.name); await showEncounterText(scene, `${namespace}:option:1:selected_2`, null, true); + setEncounterRewards(scene, { fillRemaining: true }); leaveEncounterWithoutBattle(scene, true); return true; } @@ -146,8 +160,24 @@ export const TheStrongStuffEncounter: IMysteryEncounter = }, async (scene: BattleScene) => { // Pick battle + const encounter = scene.currentBattle.mysteryEncounter; setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true }); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); + 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/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index fd4c7d0e7cc..2c09b0a01c7 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -19,8 +19,13 @@ import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-e import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** The i18n namespace for the encounter */ -const namespace = "mysteryEncounter:training_session"; +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: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.TRAINING_SESSION @@ -40,21 +45,21 @@ export const TrainingSessionEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: `${namespace}_intro_message`, + text: `${namespace}:intro`, }, ]) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }) @@ -165,7 +170,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); - queueEncounterMessage(scene, `${namespace}_battle_finished_1`); + queueEncounterMessage(scene, `${namespace}:option:1:finished`); }; setEncounterRewards( @@ -183,12 +188,12 @@ export const TrainingSessionEncounter: IMysteryEncounter = new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, - secondOptionPrompt: `${namespace}_option_2_select_prompt`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + secondOptionPrompt: `${namespace}:option:2:select_prompt`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }) @@ -237,7 +242,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - queueEncounterMessage(scene, `${namespace}_battle_finished_2`); + 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); @@ -265,12 +270,12 @@ export const TrainingSessionEncounter: IMysteryEncounter = new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ - buttonLabel: `${namespace}_option_3_label`, - buttonTooltip: `${namespace}_option_3_tooltip`, - secondOptionPrompt: `${namespace}_option_3_select_prompt`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + secondOptionPrompt: `${namespace}:option:3:select_prompt`, selected: [ { - text: `${namespace}_option_selected_message`, + text: `${namespace}:option:selected`, }, ], }) @@ -332,7 +337,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - queueEncounterMessage(scene, `${namespace}_battle_finished_3`); + queueEncounterMessage(scene, `${namespace}:option:3:finished`); // Add the pokemon back to party with ability change const abilityIndex = encounter.misc.abilityIndex; if (!!playerPokemon.getFusionSpeciesForm()) { diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts index cbf939744c4..34f5f4eb169 100644 --- a/src/data/mystery-encounters/mystery-encounter-dialogue.ts +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -23,12 +23,6 @@ export class EncounterOptionsDialogue { options?: [...OptionTextDisplay[]]; // Options array with minimum 2 options } -export default class MysteryEncounterDialogue { - intro?: TextDisplay[]; - encounterOptionsDialogue?: EncounterOptionsDialogue; - outro?: TextDisplay[]; -} - /** * Example MysteryEncounterDialogue object: * @@ -72,3 +66,9 @@ export default class MysteryEncounterDialogue { } * */ +export default class MysteryEncounterDialogue { + intro?: TextDisplay[]; + encounterOptionsDialogue?: EncounterOptionsDialogue; + outro?: TextDisplay[]; +} + diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 6ec5d652fb4..ac1d49c919e 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -103,12 +103,7 @@ export class TimeOfDayRequirement extends EncounterSceneRequirement { constructor(timeOfDay: TimeOfDay | TimeOfDay[]) { super(); - if (timeOfDay instanceof Array) { - this.requiredTimeOfDay = timeOfDay; - } else { - this.requiredTimeOfDay = []; - this.requiredTimeOfDay.push(timeOfDay); - } + this.requiredTimeOfDay = Array.isArray(timeOfDay) ? timeOfDay : [timeOfDay]; } meetsRequirement(scene: BattleScene): boolean { @@ -130,12 +125,7 @@ export class WeatherRequirement extends EncounterSceneRequirement { constructor(weather: WeatherType | WeatherType[]) { super(); - if (weather instanceof Array) { - this.requiredWeather = weather; - } else { - this.requiredWeather = []; - this.requiredWeather.push(weather); - } + this.requiredWeather = Array.isArray(weather) ? weather : [weather]; } meetsRequirement(scene: BattleScene): boolean { @@ -185,12 +175,7 @@ export class PersistentModifierRequirement extends EncounterSceneRequirement { requiredItems?: ModifierType[]; // TODO: not implemented constructor(item: ModifierType | ModifierType[]) { super(); - if (item instanceof Array) { - this.requiredItems = item; - } else { - this.requiredItems = []; - this.requiredItems.push(item); - } + this.requiredItems = Array.isArray(item) ? item : [item]; } meetsRequirement(scene: BattleScene): boolean { @@ -251,12 +236,7 @@ export class SpeciesRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (species instanceof Array) { - this.requiredSpecies = species; - } else { - this.requiredSpecies = []; - this.requiredSpecies.push(species); - } + this.requiredSpecies = Array.isArray(species) ? species : [species]; } meetsRequirement(scene: BattleScene): boolean { @@ -294,12 +274,7 @@ export class NatureRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (nature instanceof Array) { - this.requiredNature = nature; - } else { - this.requiredNature = []; - this.requiredNature.push(nature); - } + this.requiredNature = Array.isArray(nature) ? nature : [nature]; } meetsRequirement(scene: BattleScene): boolean { @@ -338,12 +313,7 @@ export class TypeRequirement extends EncounterPokemonRequirement { this.excludeFainted = excludeFainted; this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (type instanceof Array) { - this.requiredType = type; - } else { - this.requiredType = []; - this.requiredType.push(type); - } + this.requiredType = Array.isArray(type) ? type : [type]; } meetsRequirement(scene: BattleScene): boolean { @@ -388,12 +358,7 @@ export class MoveRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (moves instanceof Array) { - this.requiredMoves = moves; - } else { - this.requiredMoves = []; - this.requiredMoves.push(moves); - } + this.requiredMoves = Array.isArray(moves) ? moves : [moves]; } meetsRequirement(scene: BattleScene): boolean { @@ -437,12 +402,7 @@ export class CompatibleMoveRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (learnableMove instanceof Array) { - this.requiredMoves = learnableMove; - } else { - this.requiredMoves = []; - this.requiredMoves.push(learnableMove); - } + this.requiredMoves = Array.isArray(learnableMove) ? learnableMove : [learnableMove]; } meetsRequirement(scene: BattleScene): boolean { @@ -482,12 +442,7 @@ export class EvolutionTargetSpeciesRequirement extends EncounterPokemonRequireme super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (evolutionTargetSpecies instanceof Array) { - this.requiredEvolutionTargetSpecies = evolutionTargetSpecies; - } else { - this.requiredEvolutionTargetSpecies = []; - this.requiredEvolutionTargetSpecies.push(evolutionTargetSpecies); - } + this.requiredEvolutionTargetSpecies = Array.isArray(evolutionTargetSpecies) ? evolutionTargetSpecies : [evolutionTargetSpecies]; } meetsRequirement(scene: BattleScene): boolean { @@ -526,12 +481,7 @@ export class AbilityRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (abilities instanceof Array) { - this.requiredAbilities = abilities; - } else { - this.requiredAbilities = []; - this.requiredAbilities.push(abilities); - } + this.requiredAbilities = Array.isArray(abilities) ? abilities : [abilities]; } meetsRequirement(scene: BattleScene): boolean { @@ -571,12 +521,7 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (statusEffect instanceof Array) { - this.requiredStatusEffect = statusEffect; - } else { - this.requiredStatusEffect = []; - this.requiredStatusEffect.push(statusEffect); - } + this.requiredStatusEffect = Array.isArray(statusEffect) ? statusEffect : [statusEffect]; } meetsRequirement(scene: BattleScene): boolean { @@ -646,12 +591,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (formChangeItem instanceof Array) { - this.requiredFormChangeItem = formChangeItem; - } else { - this.requiredFormChangeItem = []; - this.requiredFormChangeItem.push(formChangeItem); - } + this.requiredFormChangeItem = Array.isArray(formChangeItem) ? formChangeItem : [formChangeItem]; } meetsRequirement(scene: BattleScene): boolean { @@ -703,12 +643,7 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (evolutionItems instanceof Array) { - this.requiredEvolutionItem = evolutionItems; - } else { - this.requiredEvolutionItem = []; - this.requiredEvolutionItem.push(evolutionItems); - } + this.requiredEvolutionItem = Array.isArray(evolutionItems) ? evolutionItems : [evolutionItems]; } meetsRequirement(scene: BattleScene): boolean { @@ -757,12 +692,7 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (heldItem instanceof Array) { - this.requiredHeldItemModifier = heldItem; - } else { - this.requiredHeldItemModifier = []; - this.requiredHeldItemModifier.push(heldItem); - } + this.requiredHeldItemModifier = Array.isArray(heldItem) ? heldItem : [heldItem]; } meetsRequirement(scene: BattleScene): boolean { diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index c6fc8c1ca66..b18707968a3 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -1,5 +1,5 @@ import { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { isNullOrUndefined } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../battle-scene"; @@ -18,6 +18,8 @@ import { StatusEffectRequirement, WaveRangeRequirement } from "./mystery-encounter-requirements"; +import { BattlerIndex } from "#app/battle"; +import { EncounterAnim } from "#app/data/battle-anims"; export enum MysteryEncounterVariant { DEFAULT, @@ -25,15 +27,28 @@ export enum MysteryEncounterVariant { WILD_BATTLE, BOSS_BATTLE, NO_BATTLE, - SAFARI_BATTLE + /** For spawning new encounter queries instead of continuing to next wave */ + CONTINUOUS_ENCOUNTER } +/** + * Enum values are base spawn weights of each tier + */ export enum MysteryEncounterTier { - COMMON, - GREAT, - ULTRA, - ROGUE, - MASTER // Not currently used + COMMON = 64, + GREAT = 40, + ULTRA = 21, + ROGUE = 3, + MASTER = 0 // Not currently used +} + +export interface StartOfBattleEffect { + sourcePokemon?: Pokemon; + sourceBattlerIndex?: BattlerIndex; + targets: BattlerIndex[]; + move: PokemonMove; + ignorePp: boolean; + followUp?: boolean; } export default interface IMysteryEncounter { @@ -47,13 +62,23 @@ export default interface IMysteryEncounter { * Optional params */ encounterTier?: MysteryEncounterTier; + encounterAnimations?: EncounterAnim[]; hideBattleIntroMessage?: boolean; - hideIntroVisuals?: boolean; + autoHideIntroVisuals?: boolean; catchAllowed?: boolean; maxAllowedEncounters?: number; - doEncounterExp?: (scene: BattleScene) => boolean; - doEncounterRewards?: (scene: BattleScene) => boolean; + + /** + * 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; /** * Requirements @@ -112,20 +137,23 @@ export default interface IMysteryEncounter { * 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?: StartOfBattleEffect[]; /** * Can be set higher or lower based on the type of battle or exp gained for an option/encounter * Defaults to 1 */ expMultiplier?: number; - /** - * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave - * You should never need to modify this - */ - seedOffset?: any; /** * Generic property to set any custom data required for the encounter * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase @@ -139,6 +167,12 @@ export default interface IMysteryEncounter { * Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class */ export default class IMysteryEncounter implements IMysteryEncounter { + /** + * 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) { if (!isNullOrUndefined(encounter)) { Object.assign(this, encounter); @@ -150,9 +184,11 @@ export default class IMysteryEncounter implements IMysteryEncounter { this.encounterVariant = MysteryEncounterVariant.DEFAULT; this.requirements = this.requirements ? this.requirements : []; this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false; - this.hideIntroVisuals = !isNullOrUndefined(this.hideIntroVisuals) ? this.hideIntroVisuals : true; + this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true; + this.startOfBattleEffects = this.startOfBattleEffects ?? []; // Reset any dirty flags or encounter data + this.startOfBattleEffectsComplete = false; this.lockEncounterRewardTiers = true; this.dialogueTokens = {}; this.enemyPartyConfigs = []; @@ -172,13 +208,6 @@ export default class IMysteryEncounter implements IMysteryEncounter { const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); - // console.log("-------" + MysteryEncounterType[this.encounterType] + " Encounter Check -------"); - // console.log(this); - // console.log( "sceneCheck: " + sceneReq); - // console.log( "primaryCheck: " + priReqs); - // console.log( "secondaryCheck: " + secReqs); - // console.log(MysteryEncounterTier[this.encounterTier]); - return sceneReq && secReqs && priReqs; } @@ -343,6 +372,27 @@ export default class IMysteryEncounter implements IMysteryEncounter { this.dialogueTokens[key] = value; } + /** + * If an encounter uses {@link MysteryEncounterVariant.CONTINUOUS_ENCOUNTER}, + * 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; + } + private capitalizeFirstLetter?(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } @@ -355,14 +405,18 @@ export class MysteryEncounterBuilder implements Partial { dialogue?: MysteryEncounterDialogue; encounterTier?: MysteryEncounterTier; + encounterAnimations?: EncounterAnim[]; requirements?: EncounterSceneRequirement[] = []; primaryPokemonRequirements?: EncounterPokemonRequirement[] = []; secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = []; excludePrimaryFromSupportRequirements?: boolean; dialogueTokens?: Record; + doEncounterExp?: (scene: BattleScene) => boolean; doEncounterRewards?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean; + onVisualsStart?: (scene: BattleScene) => boolean; + hideBattleIntroMessage?: boolean; hideIntroVisuals?: boolean; enemyPartyConfigs?: EnemyPartyConfig[] = []; @@ -410,7 +464,7 @@ export class MysteryEncounterBuilder implements Partial { * @param callback - {@linkcode OptionPhaseCallback} * @returns */ - withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback) { + withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); } @@ -453,6 +507,18 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { encounterTier: encounterTier }); } + /** + * Defines any EncounterAnim animations that are intended to be used during the encounter + * EncounterAnims 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 }); + } + /** * Sets the maximum number of times that an encounter can spawn in a given Classic run * @param maxAllowedEncounters @@ -582,6 +648,16 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { onInit: 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 }); + } + /** * Defines any enemies to use for a battle from the mystery encounter * @param enemyPartyConfig @@ -611,11 +687,11 @@ export class MysteryEncounterBuilder implements Partial { } /** - * @param hideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter + * @param autoHideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter * @returns */ - withHideIntroVisuals(hideIntroVisuals: boolean): this & Required> { - return Object.assign(this, { hideIntroVisuals: hideIntroVisuals }); + withAutoHideIntroVisuals(autoHideIntroVisuals: boolean): this & Required> { + return Object.assign(this, { autoHideIntroVisuals: autoHideIntroVisuals }); } /** diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 6d16be83d22..7679ab20d58 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -8,10 +8,11 @@ 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 { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax-encounter"; +import { SlumberingSnorlaxEncounter } from "./encounters/slumbering-snorlax-encounter"; import { TrainingSessionEncounter } from "./encounters/training-session-encounter"; import IMysteryEncounter 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"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 @@ -158,14 +159,15 @@ const anyBiomeEncounters: MysteryEncounterType[] = [ */ export const mysteryEncountersByBiome = new Map([ [Biome.TOWN, []], - [Biome.PLAINS, []], + [Biome.PLAINS, [ + MysteryEncounterType.SLUMBERING_SNORLAX + ]], [Biome.GRASS, [ - MysteryEncounterType.SLEEPING_SNORLAX, + MysteryEncounterType.SLUMBERING_SNORLAX, ]], [Biome.TALL_GRASS, []], [Biome.METROPOLIS, []], [Biome.FOREST, [ - MysteryEncounterType.SLEEPING_SNORLAX, MysteryEncounterType.SAFARI_ZONE ]], @@ -178,18 +180,18 @@ export const mysteryEncountersByBiome = new Map([ [Biome.BEACH, []], [Biome.LAKE, []], [Biome.SEABED, []], - [Biome.MOUNTAIN, [ - MysteryEncounterType.SLEEPING_SNORLAX - ]], + [Biome.MOUNTAIN, []], [Biome.BADLANDS, []], [Biome.CAVE, [ - MysteryEncounterType.SLEEPING_SNORLAX + MysteryEncounterType.THE_STRONG_STUFF ]], [Biome.DESERT, []], [Biome.ICE_CAVE, []], [Biome.MEADOW, []], [Biome.POWER_PLANT, []], - [Biome.VOLCANO, []], + [Biome.VOLCANO, [ + MysteryEncounterType.FIERY_FALLOUT + ]], [Biome.GRAVEYARD, []], [Biome.DOJO, []], [Biome.FACTORY, []], @@ -215,12 +217,13 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.DARK_DEAL] = DarkDealEncounter; allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter; allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter; - allMysteryEncounters[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxEncounter; + 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; // Add extreme encounters to biome map diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 92acc65326c..ac8c54d266b 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,15 +1,14 @@ -import { BattleType } from "#app/battle"; +import { BattlerIndex, BattleType } from "#app/battle"; import { biomeLinks } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; -import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; -import { getPokemonNameWithAffix } from "#app/messages"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import * as Overrides from "#app/overrides"; -import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; -import { MysteryEncounterBattlePhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; +import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; +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 } from "#app/ui/party-ui-handler"; @@ -26,33 +25,67 @@ import PokemonSpecies from "../../pokemon-species"; import { Status, StatusEffect } from "../../status-effect"; import { TrainerConfig, trainerConfigs, TrainerSlot } from "../../trainer-config"; import { MysteryEncounterVariant } from "../mystery-encounter"; +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"; -export class EnemyPokemonConfig { - species: PokemonSpecies; - isBoss: boolean = false; - bossSegments?: number; - bossSegmentModifier?: number; // Additive to the determined segment number - formIndex?: number; - level?: number; - nature?: Nature; - ivs?: [integer, integer, integer, integer, integer, integer]; - modifierTypes?: PokemonHeldItemModifierType[]; - dataSource?: PokemonData; - tags?: BattlerTagType[]; - mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; - status?: StatusEffect; - passive?: boolean; - spriteScale?: number; +/** + * Animates exclamation sprite over trainer's head at start of encounter + * @param scene + */ +export function doTrainerExclamation(scene: BattleScene) { + const exclamationSprite = scene.addFieldSprite(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.wav", { volume: 0.8 }); } -export class EnemyPartyConfig { - levelAdditiveMultiplier?: number = 0; // Formula for enemy: level += waveIndex / 10 * levelAdditive - doubleBattle?: boolean = false; +export interface EnemyPokemonConfig { + species: PokemonSpecies; + isBoss: boolean; + bossSegments?: number; + bossSegmentModifier?: number; // Additive to the determined segment number + spriteScale?: number; + formIndex?: number; + level?: number; + gender?: Gender; + passive?: boolean; + moveSet?: Moves[]; + nature?: Nature; + ivs?: [integer, integer, integer, integer, integer, integer]; + /** Can set just the status, or pass a timer on the status turns */ + status?: StatusEffect | [StatusEffect, number]; + mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + modifierTypes?: PokemonHeldItemModifierType[]; + 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 } /** @@ -198,24 +231,45 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: } // Set Status - if (config.status) { + const statusEffects = config.status; + if (statusEffects) { // Default to cureturn 3 for sleep - const cureTurn = config.status === StatusEffect.SLEEP ? 3 : null; - enemyPokemon.status = new Status(config.status, 0, cureTurn); + const status = Array.isArray(statusEffects) ? statusEffects[0] : statusEffects; + const cureTurn = Array.isArray(statusEffects) ? statusEffects[1] : statusEffects === StatusEffect.SLEEP ? 3 : null; + enemyPokemon.status = new Status(status, 0, cureTurn); + } + + // Set summon data fields + + // Set gender + if (!isNullOrUndefined(config.gender)) { + enemyPokemon.gender = config.gender; + enemyPokemon.summonData.gender = config.gender; + } + + // Set moves + if (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?.length > 0) { const tags = config.tags; tags.forEach(tag => enemyPokemon.addTag(tag)); - // mysteryEncounterBattleEffects can be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied - enemyPokemon.summonData.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects; - - // Requires re-priming summon data so that tags are not cleared on SummonPhase - enemyPokemon.primeSummonData(enemyPokemon.summonData); } + // 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); } loadEnemyAssets.push(enemyPokemon.loadAssets()); @@ -223,7 +277,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats); }); - scene.pushPhase(new MysteryEncounterBattlePhase(scene)); + scene.pushPhase(new MysteryEncounterBattlePhase(scene, partyConfig.disableSwitch)); await Promise.all(loadEnemyAssets); battle.enemyParty.forEach((enemyPokemon_2, e_1) => { @@ -243,6 +297,20 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: } } +/** + * 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 initCustomMovesForEncounter(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 @@ -374,10 +442,10 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p * Can have shop displayed or skipped * @param scene - Battle Scene * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers - * @param nonShopRewards - will add a non-shop reward phase for each specified item/modifier (can happen in addition to a shop) + * @param nonShopPlayerItemRewards - will add a non-shop reward phase for each specified item/modifier (can happen in addition to a shop) * @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, nonShopRewards?: ModifierTypeFunc[], preRewardsCallback?: Function) { +export function setEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, nonShopPlayerItemRewards?: ModifierTypeFunc[], preRewardsCallback?: Function) { scene.currentBattle.mysteryEncounter.doEncounterRewards = (scene: BattleScene) => { if (preRewardsCallback) { preRewardsCallback(); @@ -389,8 +457,8 @@ export function setEncounterRewards(scene: BattleScene, customShopRewards?: Cust scene.tryRemovePhase(p => p instanceof SelectModifierPhase); } - if (nonShopRewards?.length > 0) { - nonShopRewards.forEach((reward) => { + if (nonShopPlayerItemRewards?.length > 0) { + nonShopPlayerItemRewards.forEach((reward) => { scene.unshiftPhase(new ModifierRewardPhase(scene, reward)); }); } else { @@ -432,7 +500,8 @@ export function setEncounterExp(scene: BattleScene, participantId: integer | int const nonFaintedPartyMembers = party.filter(p => p.hp); const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < scene.getMaxExpLevel()); const partyMemberExp = []; - let expValue = baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1); + // 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.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { @@ -551,8 +620,10 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: return; } - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.SAFARI_BATTLE) { - scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + // If in repeated encounter variant, do nothing + // Variant must eventually be swapped in order to handle "true" end of the encounter + if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.CONTINUOUS_ENCOUNTER) { + return; } else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); @@ -568,23 +639,40 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: } } -export function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { +/** + * + * @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; if (introVisuals) { - // Hide + if (!hide) { + // Make sure visuals are in proper state for showing + introVisuals.setVisible(true); + introVisuals.x += 16; + introVisuals.y -= 16; + introVisuals.alpha = 0; + } + + // Transition scene.tweens.add({ targets: introVisuals, - x: "+=16", - y: "-=16", - alpha: 0, + x: `${hide? "+" : "-"}=16`, + y: `${hide ? "-" : "+"}=16`, + alpha: hide ? 0 : 1, ease: "Sine.easeInOut", - duration: 750, + duration, onComplete: () => { - scene.field.remove(introVisuals); - introVisuals.setVisible(false); - introVisuals.destroy(); - scene.currentBattle.mysteryEncounter.introVisuals = null; + if (hide && destroy) { + scene.field.remove(introVisuals); + introVisuals.setVisible(false); + introVisuals.destroy(); + scene.currentBattle.mysteryEncounter.introVisuals = null; + } resolve(true); } }); @@ -594,6 +682,44 @@ export function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { + 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 @@ -705,65 +831,3 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n console.log(`Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Uncommons: ${uncommonMean}\nAvg Rares: ${rareMean}\nAvg Super Rares: ${superRareMean}`); } - -/** - * 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); -} diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 48bfbb183e4..07955a08a7d 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -15,7 +15,8 @@ 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 { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; export interface MysteryEncounterPokemonData { spriteScale?: number @@ -102,8 +103,8 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boole * @returns */ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species { - let min = starterTiers instanceof Array ? starterTiers[0] : starterTiers; - let max = starterTiers instanceof Array ? starterTiers[1] : starterTiers; + 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]) @@ -135,10 +136,81 @@ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, nu return Species.BULBASAUR; } -export function koPlayerPokemon(pokemon: PlayerPokemon) { +/** + * 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 function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) { + pokemon.getSpeciesForm().baseStats = [...pokemon.getSpeciesForm().baseStats].map(v => { + const newVal = Math.floor(v + value); + return Math.min(newVal, 1); + }); + pokemon.calculateStats(); + pokemon.updateInfo(); } /** @@ -171,7 +243,6 @@ export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, pokeball.setOrigin(0.5, 0.625); scene.field.add(pokeball); - scene.playSound("pb_throw"); scene.time.delayedCall(300, () => { scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); }); @@ -179,6 +250,8 @@ export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, 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, () => { @@ -408,8 +481,9 @@ function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { }); } -export function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { - return new Promise(resolve => { +export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + await new Promise(resolve => { + scene.playSound("flee"); // Ease pokemon out scene.tweens.add({ targets: pokemon, diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 7f8ed906fa7..d4d0b479f9c 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -3,12 +3,13 @@ export enum MysteryEncounterType { MYSTERIOUS_CHEST, DARK_DEAL, FIGHT_OR_FLIGHT, - SLEEPING_SNORLAX, + 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 } diff --git a/src/field/arena.ts b/src/field/arena.ts index b4c474ce474..d767f84a135 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -317,7 +317,7 @@ export class Arena { this.eventTarget.dispatchEvent(new WeatherChangedEvent(oldWeatherType, this.weather?.weatherType, this.weather?.turnsLeft)); 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)); } else { this.scene.queueMessage(getWeatherClearMessage(oldWeatherType)); diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 4767e58d79f..84f6488c439 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -32,6 +32,8 @@ export class MysteryEncounterSpriteConfig { disableAnimation?: boolean = false; /** Repeat the animation. Defaults to `false` */ repeat?: boolean = false; + /** Hidden at start of encounter. Defaults to `false` */ + hidden?: boolean = false; /** Tint color. `0` - `1`. Higher means darker tint. */ tint?: number; /** X offset */ @@ -105,6 +107,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con tintSprite = getItemSprite(spriteKey); } + sprite.setVisible(!config.hidden); tintSprite.setVisible(false); if (scale) { @@ -345,6 +348,17 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con 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 { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5eea5d9c4b7..8b2e65e4abb 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -103,6 +103,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { 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 = null; + public fieldPosition: FieldPosition; public maskEnabled: boolean; @@ -3935,7 +3938,6 @@ export class PokemonSummonData { public moveset: PokemonMove[]; // If not initialized this value will not be populated from save data. public types: Type[] = null; - public mysteryEncounterBattleEffects: (pokemon: Pokemon) => void = null; } export class PokemonBattleData { diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 5effff0b02a..891d6dc299b 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -1,4 +1,15 @@ -import { lostAtSea } from "./mystery-encounters/lost-at-sea"; +import { lostAtSeaDialogue } from "./mystery-encounters/lost-at-sea-dialogue"; +import { mysteriousChestDialogue } from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue"; +import { mysteriousChallengersDialogue } from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue"; +import { darkDealDialogue } from "#app/locales/en/mystery-encounters/dark-deal-dialogue"; +import { departmentStoreSaleDialogue } from "#app/locales/en/mystery-encounters/department-store-sale-dialogue"; +import { fieldTripDialogue } from "#app/locales/en/mystery-encounters/field-trip-dialogue"; +import { fieryFalloutDialogue } from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue"; +import { fightOrFlightDialogue } from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue"; +import { safariZoneDialogue } from "#app/locales/en/mystery-encounters/safari-zone-dialogue"; +import { shadyVitaminDealerDialogue } from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue"; +import { slumberingSnorlaxDialogue } from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue"; +import { trainingSessionDialogue } from "#app/locales/en/mystery-encounters/training-session-dialogue"; import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue"; /** @@ -22,225 +33,17 @@ export const mysteryEncounter = { "paid_money": "You paid ₽{{amount, number}}.", "receive_money": "You received ₽{{amount, number}}!", - // Mystery Encounters -- Common Tier - - "mysterious_chest_intro_message": "You found...@d{32} a chest?", - "mysterious_chest_title": "The Mysterious Chest", - "mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", - "mysterious_chest_query": "Will you open it?", - "mysterious_chest_option_1_label": "Open it", - "mysterious_chest_option_1_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}", - "mysterious_chest_option_2_label": "It's too risky, leave", - "mysterious_chest_option_2_tooltip": "(-) No Rewards", - "mysterious_chest_option_1_selected_message": "You open the chest to find...", - "mysterious_chest_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.", - "mysterious_chest_option_1_normal_result": "Just some normal tools and items.", - "mysterious_chest_option_1_good_result": "Some pretty nice tools and items.", - "mysterious_chest_option_1_great_result": "A couple great tools and items!", - "mysterious_chest_option_1_amazing_result": "Whoa! An amazing item!", - "mysterious_chest_option_1_bad_result": `Oh no!@d{32}\nThe chest was trapped! - $Your {{pokeName}} jumps in front of you\nbut is KOed in the process.`, - - "fight_or_flight_intro_message": "Something shiny is sparkling\non the ground near that Pokémon!", - "fight_or_flight_title": "Fight or Flight", - "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.", - "fight_or_flight_query": "What will you do?", - "fight_or_flight_option_1_label": "Battle the Pokémon", - "fight_or_flight_option_1_tooltip": "(-) Hard Battle\n(+) New Item", - "fight_or_flight_option_2_label": "Steal the item", - "fight_or_flight_option_2_tooltip": "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}", - "fight_or_flight_option_2_steal_tooltip": "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", - "fight_or_flight_option_3_label": "Leave", - "fight_or_flight_option_3_tooltip": "(-) No Rewards", - "fight_or_flight_option_1_selected_message": "You approach the\nPokémon without fear.", - "fight_or_flight_option_2_good_result": `.@d{32}.@d{32}.@d{32} - $You manage to sneak your way\npast and grab the item!`, - "fight_or_flight_option_2_steal_result": `.@d{32}.@d{32}.@d{32} - $Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}! - $ You nabbed the item!`, - "fight_or_flight_option_2_bad_result": `.@d{32}.@d{32}.@d{32} - $The Pokémon catches you\nas you try to sneak around!`, - "fight_or_flight_boss_enraged": "The opposing {{enemyPokemon}} has become enraged!", - "fight_or_flight_option_3_selected": "You leave the strong Pokémon\nwith its prize and continue on.", - - "department_store_sale_intro_message": "It's a lady with a ton of shopping bags.", - "department_store_sale_speaker": "Shopper", - "department_store_sale_intro_dialogue": `Hello! Are you here for\nthe amazing sales too? - $There's a special coupon that you can\nredeem for a free item during the sale! - $I have an extra one. Here you go!`, - "department_store_sale_title": "Department Store Sale", - "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!", - "department_store_sale_query": "Which counter will you go to?", - "department_store_sale_option_1_label": "TM Counter", - "department_store_sale_option_1_tooltip": "(+) TM Shop", - "department_store_sale_option_2_label": "Vitamin Counter", - "department_store_sale_option_2_tooltip": "(+) Vitamin Shop", - "department_store_sale_option_3_label": "Battle Item Counter", - "department_store_sale_option_3_tooltip": "(+) X Item Shop", - "department_store_sale_option_4_label": "Pokéball Counter", - "department_store_sale_option_4_tooltip": "(+) Pokéball Shop", - "department_store_sale_outro": "What a deal! You should shop there more often.", - - "shady_vitamin_dealer_intro_message": "A man in a dark coat approaches you.", - "shady_vitamin_dealer_speaker": "Shady Salesman", - "shady_vitamin_dealer_intro_dialogue": `.@d{16}.@d{16}.@d{16} - $I've got the goods if you've got the money. - $Make sure your Pokémon can handle it though.`, - "shady_vitamin_dealer_title": "The Vitamin Dealer", - "shady_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.", - "shady_vitamin_dealer_query": "Which deal will choose?", - "shady_vitamin_dealer_invalid_selection": "Pokémon must be healthy enough.", - "shady_vitamin_dealer_option_1_label": "The Cheap Deal", - "shady_vitamin_dealer_option_1_tooltip": "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", - "shady_vitamin_dealer_option_2_label": "The Pricey Deal", - "shady_vitamin_dealer_option_2_tooltip": "(-) Pay {{option2Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", - "shady_vitamin_dealer_option_selected": `The man hands you two bottles and quickly disappears. - \${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!`, - "shady_vitamin_dealer_damage_only": `But the medicine had some side effects! - $Your {{selectedPokemon}} takes some damage...`, - "shady_vitamin_dealer_bad_poison": `But the medicine had some side effects! - $Your {{selectedPokemon}} takes some damage\nand becomes badly poisoned...`, - "shady_vitamin_dealer_poison": `But the medicine had some side effects! - $Your {{selectedPokemon}} becomes poisoned...`, - "shady_vitamin_dealer_no_bad_effects": "Looks like there were no side-effects this time.", - "shady_vitamin_dealer_option_3_label": "Leave", - "shady_vitamin_dealer_option_3_tooltip": "(-) No Rewards", - - "field_trip_intro_message": "It's a teacher and some school children!", - "field_trip_speaker": "Teacher", - "field_trip_intro_dialogue": `Hello, there! Would you be able to\nspare a minute for my students? - $I'm teaching them about Pokémon moves\nand would love to show them a demonstration. - $Would you mind showing us one of\nthe moves your Pokémon can use?`, - "field_trip_title": "Field Trip", - "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.", - "field_trip_query": "Which move category will you show off?", - "field_trip_option_1_label": "A Physical Move", - "field_trip_option_1_tooltip": "(+) Physical Item Rewards", - "field_trip_option_2_label": "A Special Move", - "field_trip_option_2_tooltip": "(+) Special Item Rewards", - "field_trip_option_3_label": "A Status Move", - "field_trip_option_3_tooltip": "(+) Status Item Rewards", - "field_trip_second_option_prompt": "Choose a move for your Pokémon to use.", - "field_trip_option_selected": "{{pokeName}} shows off an awesome display of {{move}}!", - "field_trip_option_incorrect": `... - $That isn't a {{moveCategory}} move! - $I'm sorry, but I can't give you anything.`, - "field_trip_lesson_learned": `Looks like you learned a valuable lesson? - $Your Pokémon also gained some knowledge.`, - "field_trip_outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!", - "field_trip_outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere.", - - // Mystery Encounters -- Great Tier - - "mysterious_challengers_intro_message": "Mysterious challengers have appeared!", - "mysterious_challengers_title": "Mysterious Challengers", - "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?", - "mysterious_challengers_query": "Who will you battle?", - "mysterious_challengers_option_1_label": "A clever, mindful foe", - "mysterious_challengers_option_1_tooltip": "(-) Standard Battle\n(+) Move Item Rewards", - "mysterious_challengers_option_2_label": "A strong foe", - "mysterious_challengers_option_2_tooltip": "(-) Hard Battle\n(+) Good Rewards", - "mysterious_challengers_option_3_label": "The mightiest foe", - "mysterious_challengers_option_3_tooltip": "(-) Brutal Battle\n(+) Great Rewards", - "mysterious_challengers_option_selected_message": "The trainer steps forward...", - "mysterious_challengers_outro_win": "The mysterious challenger was defeated!", - - "safari_zone_intro_message": "It's a safari zone!", - "safari_zone_title": "The Safari Zone", - "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!", - "safari_zone_query": "Would you like to enter?", - "safari_zone_option_1_label": "Enter", - "safari_zone_option_1_tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", - "safari_zone_option_2_label": "Leave", - "safari_zone_option_2_tooltip": "(-) No Rewards", - "safari_zone_option_1_selected_message": "Time to test your luck!", - "safari_zone_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.", - "safari_zone_pokeball_option_label": "Throw a Pokéball", - "safari_zone_pokeball_option_tooltip": "(+) Throw a Pokéball", - "safari_zone_pokeball_option_selected": "You throw a Pokéball!", - "safari_zone_bait_option_label": "Throw bait", - "safari_zone_bait_option_tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", - "safari_zone_bait_option_selected": "You throw some bait!", - "safari_zone_mud_option_label": "Throw mud", - "safari_zone_mud_option_tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", - "safari_zone_mud_option_selected": "You throw some mud!", - "safari_zone_flee_option_label": "Flee", - "safari_zone_flee_option_tooltip": "(?) Flee from this Pokémon", - "safari_zone_pokemon_watching": "{{pokemonName}} is watching carefully!", - "safari_zone_pokemon_eating": "{{pokemonName}} is eating!", - "safari_zone_pokemon_busy_eating": "{{pokemonName}} is busy eating!", - "safari_zone_pokemon_angry": "{{pokemonName}} is angry!", - "safari_zone_pokemon_beside_itself_angry": "{{pokemonName}} is beside itself with anger!", - "safari_zone_remaining_count": "{{remainingCount}} Pokémon remaining!", - - // Mystery Encounters -- Ultra Tier - - "training_session_intro_message": "You've come across some\ntraining tools and supplies.", - "training_session_title": "Training Session", - "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.", - "training_session_query": "How should you train?", - "training_session_option_1_label": "Light Training", - "training_session_option_1_tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", - "training_session_option_2_label": "Moderate Training", - "training_session_option_2_tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature", - "training_session_option_2_select_prompt": "Select a new nature\nto train your Pokémon in.", - "training_session_option_3_label": "Heavy Training", - "training_session_option_3_tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability", - "training_session_option_3_select_prompt": "Select a new ability\nto train your Pokémon in.", - "training_session_option_selected_message": "{{selectedPokemon}} moves across\nthe clearing to face you...", - "training_session_battle_finished_1": `{{selectedPokemon}} returns, feeling\nworn out but accomplished! - $Its {{stat1}} and {{stat2}} IVs were improved!`, - "training_session_battle_finished_2": `{{selectedPokemon}} returns, feeling\nworn out but accomplished! - $Its nature was changed to {{nature}}!`, - "training_session_battle_finished_3": `{{selectedPokemon}} returns, feeling\nworn out but accomplished! - $Its ability was changed to {{ability}}!`, - "training_session_outro_win": "That was a successful training session!", - - // Mystery Encounters -- Rogue Tier - - "dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...", - "dark_deal_speaker": "Shady Guy", - "dark_deal_intro_dialogue": `Hey, you! - $I've been working on a new device\nto bring out a Pokémon's latent power! - $It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form. - $Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.`, - "dark_deal_title": "Dark Deal", - "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...\"", - "dark_deal_query": "What will you do?", - "dark_deal_option_1_label": "Accept", - "dark_deal_option_1_tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", - "dark_deal_option_2_label": "Refuse", - "dark_deal_option_2_tooltip": "(-) No Rewards", - "dark_deal_option_1_selected": `Let's see, that {{pokeName}} will do nicely! - $Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...`, - "dark_deal_option_1_selected_message": `The man hands you 5 Rogue Balls. - \${{pokeName}} hops into the strange machine... - $Flashing lights and weird noises\nstart coming from the machine! - $...@d{96} Something emerges\nfrom the device, raging wildly!`, - "dark_deal_option_2_selected": "Not gonna help a poor fellow out?\nPah!", - "dark_deal_outro": "After the harrowing encounter,\nyou collect yourself and depart.", - - "sleeping_snorlax_intro_message": `As you walk down a narrow pathway, you see a towering silhouette blocking your path. - $You get closer to see a Snorlax sleeping peacefully.\nIt seems like there's no way around it.`, - "sleeping_snorlax_title": "Sleeping Snorlax", - "sleeping_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...", - "sleeping_snorlax_query": "What will you do?", - "sleeping_snorlax_option_1_label": "Fight it", - "sleeping_snorlax_option_1_tooltip": "(-) Fight Sleeping Snorlax", - "sleeping_snorlax_option_2_label": "Wait for it to move", - "sleeping_snorlax_option_2_tooltip": "@[SUMMARY_BLUE]{(75%) Wait a short time}\n@[SUMMARY_BLUE]{(25%) Wait a long time}", - "sleeping_snorlax_option_3_label": "Steal its item", - "sleeping_snorlax_option_3_tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) Leftovers", - "sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this", - "sleeping_snorlax_option_1_selected_message": "You approach the\nPokémon without fear.", - "sleeping_snorlax_option_2_selected_message": `.@d{32}.@d{32}.@d{32} - $You wait for a time, but the Snorlax's yawns make your party sleepy.`, - "sleeping_snorlax_option_2_good_result": "When you all awaken, the Snorlax is no where to be found - but your Pokémon are all healed!", - "sleeping_snorlax_option_2_bad_result": `Your {{primaryName}} is still asleep... - $But on the bright side, the Snorlax left something behind... - $@s{item_fanfare}You gained a Berry!`, - "sleeping_snorlax_option_3_good_result": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}}! @s{item_fanfare}It steals Leftovers off the sleeping Snorlax and you make out like bandits!", - - lostAtSea, + 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, } as const; diff --git a/src/locales/en/mystery-encounters/dark-deal-dialogue.ts b/src/locales/en/mystery-encounters/dark-deal-dialogue.ts new file mode 100644 index 00000000000..113209bf61a --- /dev/null +++ b/src/locales/en/mystery-encounters/dark-deal-dialogue.ts @@ -0,0 +1,29 @@ +export const darkDealDialogue = { + intro: "A strange man in a tattered coat\nstands in your way...", + speaker: "Shady Guy", + intro_dialogue: `Hey, you! + $I've been working on a new device\nto bring out a Pokémon's latent power! + $It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form. + $Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.`, + title: "Dark Deal", + description: "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"", + query: "What will you do?", + option: { + 1: { + label: "Accept", + tooltip: "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", + selected_dialogue: `Let's see, that {{pokeName}} will do nicely! + $Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...`, + selected_message: `The man hands you 5 Rogue Balls. + \${{pokeName}} hops into the strange machine... + $Flashing lights and weird noises\nstart coming from the machine! + $...@d{96} Something emerges\nfrom the device, raging wildly!` + }, + 2: { + label: "Refuse", + tooltip: "(-) No Rewards", + selected: "Not gonna help a poor fellow out?\nPah!", + } + }, + outro: "After the harrowing encounter,\nyou collect yourself and depart." +}; diff --git a/src/locales/en/mystery-encounters/department-store-sale-dialogue.ts b/src/locales/en/mystery-encounters/department-store-sale-dialogue.ts new file mode 100644 index 00000000000..5b7fe9a0af0 --- /dev/null +++ b/src/locales/en/mystery-encounters/department-store-sale-dialogue.ts @@ -0,0 +1,29 @@ +export const departmentStoreSaleDialogue = { + intro: "It's a lady with a ton of shopping bags.", + speaker: "Shopper", + intro_dialogue: `Hello! Are you here for\nthe amazing sales too? + $There's a special coupon that you can\nredeem for a free item during the sale! + $I have an extra one. Here you go!`, + title: "Department Store Sale", + description: "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!", + query: "Which counter will you go to?", + option: { + 1: { + label: "TM Counter", + tooltip: "(+) TM Shop", + }, + 2: { + label: "Vitamin Counter", + tooltip: "(+) Vitamin Shop", + }, + 3: { + label: "Battle Item Counter", + tooltip: "(+) X Item Shop", + }, + 4: { + label: "Pokéball Counter", + tooltip: "(+) Pokéball Shop", + }, + }, + outro: "What a deal! You should shop there more often." +}; diff --git a/src/locales/en/mystery-encounters/field-trip-dialogue.ts b/src/locales/en/mystery-encounters/field-trip-dialogue.ts new file mode 100644 index 00000000000..62ee175e073 --- /dev/null +++ b/src/locales/en/mystery-encounters/field-trip-dialogue.ts @@ -0,0 +1,33 @@ +export const fieldTripDialogue = { + intro: "It's a teacher and some school children!", + speaker: "Teacher", + intro_dialogue: `Hello, there! Would you be able to\nspare a minute for my students? + $I'm teaching them about Pokémon moves\nand would love to show them a demonstration. + $Would you mind showing us one of\nthe moves your Pokémon can use?`, + title: "Field Trip", + description: "A teacher is requesting a move demonstration from a Pokémon. Depending on the move you choose, she might have something useful for you in exchange.", + query: "Which move category will you show off?", + option: { + 1: { + label: "A Physical Move", + tooltip: "(+) Physical Item Rewards", + }, + 2: { + label: "A Special Move", + tooltip: "(+) Special Item Rewards", + }, + 3: { + label: "A Status Move", + tooltip: "(+) Status Item Rewards", + }, + selected: "{{pokeName}} shows off an awesome display of {{move}}!", + incorrect: `... + $That isn't a {{moveCategory}} move! + $I'm sorry, but I can't give you anything.`, + lesson_learned: `Looks like you learned a valuable lesson? + $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." +}; diff --git a/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts new file mode 100644 index 00000000000..2fbf5a15bda --- /dev/null +++ b/src/locales/en/mystery-encounters/fiery-fallout-dialogue.ts @@ -0,0 +1,30 @@ +export const fieryFalloutDialogue = { + 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! + $They don't take kindly to the interruption and attack!` + }, + 2: { + label: "Hunker down", + tooltip: "(-) Suffer the effects of the weather", + selected: `The weather effects cause significant\nharm as you struggle to find shelter! + $Your party takes 20% Max HP damage!`, + target_burned: "Your {{burnedPokemon}} also became burned!" + }, + 3: { + label: "Your Fire types help", + tooltip: "(+) End the conditions\n(+) Gain a Charcoal", + disabled_tooltip: "You need at least 2 Fire Type Pokémon to choose this", + selected: `Your {{option3PrimaryName}} and {{option3SecondaryName}} guide you to where two Volcarona are in the middle of a mating dance! + $Thankfully, your Pokémon are able to calm them,\nand they depart without issue.`, + }, + }, + found_charcoal: `After the weather clears,\nyour {{leadPokemon}} spots something on the ground. + $@s{item_fanfare}{{leadPokemon}} gained a Charcoal!` +}; diff --git a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts new file mode 100644 index 00000000000..2ef9d31a821 --- /dev/null +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts @@ -0,0 +1,31 @@ +export const fightOrFlightDialogue = { + 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: "{{option2PrimaryName}} can help", + tooltip: "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}", + tooltip_special: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", + good_result: `.@d{32}.@d{32}.@d{32} + $You manage to sneak your way\npast and grab the item!`, + special_result: `.@d{32}.@d{32}.@d{32} + $Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}! + $ You nabbed the item!`, + bad_result: `.@d{32}.@d{32}.@d{32} + $The Pokémon catches you\nas you try to sneak around!`, + 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.", + }, + } +}; diff --git a/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts new file mode 100644 index 00000000000..cb1de1315cd --- /dev/null +++ b/src/locales/en/mystery-encounters/lost-at-sea-dialogue.ts @@ -0,0 +1,31 @@ +export const lostAtSeaDialogue = { + 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}} can help", + label_disabled: "Can't {{option1RequiredMove}}", + tooltip: "(+) {{option1PrimaryName}} saves you\n(+) {{option1PrimaryName}} gains some EXP", + tooltip_disabled: "You have no Pokémon to {{option1RequiredMove}} on", + selected: `{{option1PrimaryName}} swims ahead, guiding you back on track. + \${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`, + }, + 2: { + label: "{{option2PrimaryName}} can help", + label_disabled: "Can't {{option2RequiredMove}}", + tooltip: "(+) {{option2PrimaryName}} saves you\n(+) {{option2PrimaryName}} gains some EXP", + tooltip_disabled: "You have no Pokémon to {{option2RequiredMove}} with", + selected: `{{option2PrimaryName}} flies ahead of your boat, guiding you back on track. + \${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`, + }, + 3: { + label: "Wander aimlessly", + tooltip: "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", + selected: `You float about in the boat, steering without direction until you finally spot a landmark you remember. + $You and your Pokémon are fatigued from the whole ordeal.`, + }, + }, + outro: "You are back on track." +}; diff --git a/src/locales/en/mystery-encounters/lost-at-sea.ts b/src/locales/en/mystery-encounters/lost-at-sea.ts deleted file mode 100644 index 6e37168a255..00000000000 --- a/src/locales/en/mystery-encounters/lost-at-sea.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const lostAtSea = { - intro: "Wandering aimlessly, you effectively get nowhere.", - title: "Lost at sea", - description: "The sea is turbulent in this area, and you seem to be running out of fuel.\nThis is bad. Is there a way out of the situation?", - query: "What will you do?", - option: { - 1: { - label: "{{option1PrimaryName}} can 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}} can 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 it aimlessly until you finally get back on track. - $You and your Pokémon get very fatigued during the whole ordeal.`, - }, - }, - outro: "You are back on track." -}; diff --git a/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts new file mode 100644 index 00000000000..56c78e7e5f8 --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-challengers-dialogue.ts @@ -0,0 +1,22 @@ +export const mysteriousChallengersDialogue = { + 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!" +}; diff --git a/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts new file mode 100644 index 00000000000..74a578cfe39 --- /dev/null +++ b/src/locales/en/mystery-encounters/mysterious-chest-dialogue.ts @@ -0,0 +1,27 @@ +export const mysteriousChestDialogue = { + 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! + $Your {{pokeName}} jumps in front of you\nbut is KOed in the process.`, + }, + 2: { + label: "It's too risky, leave", + tooltip: "(-) No Rewards", + selected: "You hurry along your way,\nwith a slight feeling of regret.", + }, + } +}; + + + diff --git a/src/locales/en/mystery-encounters/safari-zone-dialogue.ts b/src/locales/en/mystery-encounters/safari-zone-dialogue.ts new file mode 100644 index 00000000000..ae77aa447b8 --- /dev/null +++ b/src/locales/en/mystery-encounters/safari-zone-dialogue.ts @@ -0,0 +1,46 @@ +export const safariZoneDialogue = { + 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!" +}; diff --git a/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts new file mode 100644 index 00000000000..ecf88577c75 --- /dev/null +++ b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts @@ -0,0 +1,40 @@ +export const shadyVitaminDealerDialogue = { + intro: "A man in a dark coat approaches you.", + speaker: "Shady Salesman", + intro_dialogue: `.@d{16}.@d{16}.@d{16} + $I've got the goods if you've got the money. + $Make sure your Pokémon can handle it though.`, + title: "The Vitamin Dealer", + description: "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", + query: "Which deal will 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", + selected: `{{option1PrimaryName}} swims ahead, guiding you back on track. + \${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`, + }, + 2: { + label: "The Pricey Deal", + tooltip: "(-) Pay {{option2Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", + selected: `{{option2PrimaryName}} flies ahead of your boat, guiding you back on track. + \${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`, + }, + 3: { + label: "Leave", + tooltip: "(-) No Rewards", + selected: `You float about in the boat, steering without direction until you finally spot a landmark you remember. + $You and your Pokémon are fatigued from the whole ordeal.`, + }, + selected: `The man hands you two bottles and quickly disappears. + \${{selectedPokemon}} gained {{boost1}} and {{boost2}} boosts!` + }, + damage_only: `But the medicine had some side effects! + $Your {{selectedPokemon}} takes some damage...`, + bad_poison: `But the medicine had some side effects! + $Your {{selectedPokemon}} takes some damage\nand becomes badly poisoned...`, + poison: `But the medicine had some side effects! + $Your {{selectedPokemon}} becomes poisoned...`, + no_bad_effects: "Looks like there were no side-effects this time.", +}; diff --git a/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts new file mode 100644 index 00000000000..92244573c9b --- /dev/null +++ b/src/locales/en/mystery-encounters/slumbering-snorlax-dialogue.ts @@ -0,0 +1,28 @@ +export const slumberingSnorlaxDialogue = { + intro: `As you walk down a narrow pathway, you see a towering silhouette blocking your path. + $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} + $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}}! + $@s{item_fanfare}It steals Leftovers off the sleeping\nSnorlax and you make out like bandits!`, + }, + } +}; diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts index a165c9718a8..c65019b539f 100644 --- a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts @@ -17,9 +17,9 @@ export const theStrongStuffDialogue = { 2: { label: "Battle the Shuckle", tooltip: "(-) Hard Battle\n(+) Special Rewards", - 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.", + selected: "Enraged, the Shuckle drinks some of its juice and attacks!", + stat_boost: "The Shuckle's juice boosts its stats!", }, }, - outro: "You are back on track." + outro: "What a bizarre turn of events." }; diff --git a/src/locales/en/mystery-encounters/training-session-dialogue.ts b/src/locales/en/mystery-encounters/training-session-dialogue.ts new file mode 100644 index 00000000000..12e84dd8d0d --- /dev/null +++ b/src/locales/en/mystery-encounters/training-session-dialogue.ts @@ -0,0 +1,30 @@ +export const trainingSessionDialogue = { + 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?", + option: { + 1: { + label: "Light Training", + tooltip: "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon", + finished: `{{selectedPokemon}} returns, feeling\nworn out but accomplished! + $Its {{stat1}} and {{stat2}} IVs were improved!`, + }, + 2: { + label: "Moderate Training", + tooltip: "(-) Moderate Battle\n(+) Change Pokémon's Nature", + select_prompt: "Select a new nature\nto train your Pokémon in.", + finished: `{{selectedPokemon}} returns, feeling\nworn out but accomplished! + $Its nature was changed to {{nature}}!`, + }, + 3: { + label: "Heavy Training", + tooltip: "(-) Each of your Pokémon lose {{damagePercentage}}% of their total HP", + select_prompt: "Select a new ability\nto train your Pokémon in.", + finished: `{{selectedPokemon}} returns, feeling\nworn out but accomplished! + $Its ability was changed to {{ability}}!`, + }, + selected: "{{selectedPokemon}} moves across\nthe clearing to face you...", + }, + outro: "That was a successful training session!", +}; diff --git a/src/phases.ts b/src/phases.ts index c0ecea19eb7..904e860935b 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1,15 +1,16 @@ import BattleScene, { bypassLogin } from "./battle-scene"; -import { default as Pokemon, PlayerPokemon, EnemyPokemon, PokemonMove, MoveResult, DamageResult, FieldPosition, HitResult, TurnMove } from "./field/pokemon"; +import { DamageResult, default as Pokemon, EnemyPokemon, FieldPosition, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "./field/pokemon"; import * as Utils from "./utils"; -import { allMoves, applyMoveAttrs, BypassSleepAttr, ChargeAttr, applyFilteredMoveAttrs, HitsTagAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveFlags, MultiHitAttr, OverrideMoveEffectAttr, VariableAccuracyAttr, MoveTarget, getMoveTargets, MoveTargetSet, MoveEffectTrigger, CopyMoveAttr, AttackMove, SelfStatusMove, PreMoveMessageAttr, HealStatusEffectAttr, IgnoreOpponentStatChangesAttr, NoEffectAttr, BypassRedirectAttr, FixedDamageAttr, PostVictoryStatChangeAttr, OneHitKOAccuracyAttr, ForceSwitchOutAttr, VariableTargetAttr, IncrementMovePriorityAttr } from "./data/move"; +import { isNullOrUndefined } from "./utils"; +import { allMoves, applyFilteredMoveAttrs, applyMoveAttrs, AttackMove, BypassRedirectAttr, BypassSleepAttr, ChargeAttr, CopyMoveAttr, FixedDamageAttr, ForceSwitchOutAttr, getMoveTargets, HealStatusEffectAttr, HitsTagAttr, IgnoreOpponentStatChangesAttr, IncrementMovePriorityAttr, MissEffectAttr, MoveAttr, MoveEffectAttr, MoveEffectTrigger, MoveFlags, MoveTarget, MoveTargetSet, MultiHitAttr, NoEffectAttr, OneHitKOAccuracyAttr, OverrideMoveEffectAttr, PostVictoryStatChangeAttr, PreMoveMessageAttr, SelfStatusMove, VariableAccuracyAttr, VariableTargetAttr } from "./data/move"; import { Mode } from "./ui/ui"; import { Command } from "./ui/command-ui-handler"; import { Stat } from "./data/pokemon-stat"; -import { BerryModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, LapsingPersistentModifier, MapModifier, Modifier, MultipleParticipantExpBonusModifier, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, MoneyMultiplierModifier, MoneyInterestModifier, IvScannerModifier, LapsingPokemonHeldItemModifier, PokemonMultiHitModifier, PokemonMoveAccuracyBoosterModifier, overrideModifiers, overrideHeldItems, BypassSpeedChanceModifier, TurnStatusEffectModifier } from "./modifier/modifier"; +import { BerryModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, EnemyAttackStatusEffectChanceModifier, EnemyPersistentModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, HealingBoosterModifier, HitHealModifier, IvScannerModifier, LapsingPersistentModifier, LapsingPokemonHeldItemModifier, MapModifier, Modifier, MoneyInterestModifier, MoneyMultiplierModifier, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, SwitchEffectTransferModifier, TempBattleStatBoosterModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier } from "./modifier/modifier"; import PartyUiHandler, { PartyOption, PartyUiMode } from "./ui/party-ui-handler"; import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "./data/pokeball"; -import { CommonAnim, CommonBattleAnim, MoveAnim, initMoveAnim, loadMoveAnimAssets } from "./data/battle-anims"; -import { StatusEffect, getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText } from "./data/status-effect"; +import { CommonAnim, CommonBattleAnim, initEncounterAnims, initMoveAnim, loadEncounterAnimAssets, loadMoveAnimAssets, MoveAnim } from "./data/battle-anims"; +import { getStatusEffectActivationText, getStatusEffectCatchRateMultiplier, getStatusEffectHealText, getStatusEffectObtainText, getStatusEffectOverlapText, StatusEffect } from "./data/status-effect"; import { SummaryUiMode } from "./ui/summary-ui-handler"; import EvolutionSceneHandler from "./ui/evolution-scene-handler"; import { EvolutionPhase } from "./evolution-phase"; @@ -17,21 +18,21 @@ import { Phase } from "./phase"; import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat"; import { biomeLinks, getBiomeName } from "./data/biomes"; import { ModifierTier } from "./modifier/modifier-tier"; -import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds, CustomModifierSettings } from "./modifier/modifier-type"; +import { CustomModifierSettings, FusePokemonModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, modifierTypes, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, regenerateModifierPoolThresholds, RememberMoveModifierType, TmModifierType } from "./modifier/modifier-type"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag, MysteryEncounterPostSummonTag } from "./data/battler-tags"; +import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, MysteryEncounterPostSummonTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "./messages"; import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; -import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; +import { getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage, Weather, WeatherType } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; -import { BattleType, BattlerIndex, TurnCommand } from "./battle"; -import { ChallengeAchv, HealAchv, LevelAchv, achvs } from "./system/achv"; -import { TrainerSlot, trainerConfigs } from "./data/trainer-config"; +import { BattlerIndex, BattleType, TurnCommand } from "./battle"; +import { achvs, ChallengeAchv, HealAchv, LevelAchv } from "./system/achv"; +import { trainerConfigs, TrainerSlot } from "./data/trainer-config"; import { EggHatchPhase } from "./egg-hatch-phase"; import { Egg } from "./data/egg"; import { vouchers } from "./system/voucher"; @@ -41,7 +42,7 @@ import { addPokeballCaptureStars, addPokeballOpenParticles } from "./field/anims import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeManualTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangePreMoveTrigger } from "./data/pokemon-forms"; import { battleSpecDialogue, getCharVariantFromDialogue, miscDialogue } from "./data/dialogue"; import { SettingKeys } from "./system/settings/settings"; -import { Tutorial, handleTutorial } from "./tutorial"; +import { handleTutorial, Tutorial } from "./tutorial"; import { TerrainType } from "./data/terrain"; import { OptionSelectConfig, OptionSelectItem } from "./ui/abstact-option-select-ui-handler"; import { SaveSlotUiMode } from "./ui/save-slot-select-ui-handler"; @@ -50,7 +51,7 @@ import { GameMode, GameModes, getGameMode } from "./game-mode"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "./data/pokemon-species"; import i18next from "./plugins/i18n"; import * as Overrides from "./overrides"; -import { TextStyle, addTextObject } from "./ui/text"; +import { addTextObject, TextStyle } from "./ui/text"; import { Type } from "./data/type"; import { BerryUsedEvent, EncounterPhaseEvent, MoveUsedEvent, TurnEndEvent, TurnInitEvent } from "./events/battle-scene"; import { Abilities } from "#enums/abilities"; @@ -65,10 +66,9 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { MysteryEncounterVariant } from "#app/data/mystery-encounters/mystery-encounter"; -import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phase"; -import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { doTrainerExclamation, handleMysteryEncounterBattleStartEffects, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; -import { isNullOrUndefined } from "./utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; const { t } = i18next; @@ -832,6 +832,11 @@ export class EncounterPhase extends BattlePhase { 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); @@ -896,6 +901,15 @@ export class EncounterPhase extends BattlePhase { 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.wav", "battle_anims", "GEN8- Exclaim.wav"); + this.scene.loadAtlas("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) { @@ -1076,14 +1090,18 @@ export class EncounterPhase extends BattlePhase { const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals; introVisuals.playAnim(); + if (this.scene.currentBattle.mysteryEncounter.onVisualsStart) { + this.scene.currentBattle.mysteryEncounter.onVisualsStart(this.scene); + } + const doEncounter = () => { this.scene.playBgm(undefined); const doShowEncounterOptions = () => { this.scene.ui.clearText(); this.scene.ui.getMessageHandler().hideNameText(); - this.scene.unshiftPhase(new MysteryEncounterPhase(this.scene)); + this.scene.unshiftPhase(new MysteryEncounterPhase(this.scene)); this.end(); }; @@ -1116,6 +1134,7 @@ export class EncounterPhase extends BattlePhase { if (!encounterMessage) { doEncounter(); } else { + doTrainerExclamation(this.scene); this.scene.ui.showDialogue(encounterMessage, "???", null, () => { this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doEncounter())); }); @@ -2035,6 +2054,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()) { @@ -2695,12 +2716,14 @@ export class NewBattlePhase extends BattlePhase { export class CommonAnimPhase extends PokemonPhase { private anim: CommonAnim; private targetIndex: integer; + private playOnEmptyField: boolean; - constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, anim: CommonAnim) { + constructor(scene: BattleScene, battlerIndex: BattlerIndex, targetIndex: BattlerIndex, anim: CommonAnim, playOnEmptyField: boolean = false) { super(scene, battlerIndex); this.anim = anim; this.targetIndex = targetIndex; + this.playOnEmptyField = playOnEmptyField; } setAnimation(anim: CommonAnim) { @@ -2708,7 +2731,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(); }); } @@ -5362,10 +5386,10 @@ export class SelectModifierPhase extends BattlePhase { return true; case 1: if (typeOptions.length === 0) { - this.scene.ui.revertMode(); + this.scene.ui.clearText(); this.scene.ui.setMode(Mode.MESSAGE); super.end(); - return; + return true; } modifierType = typeOptions[cursor].type; break; diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phases.ts similarity index 90% rename from src/phases/mystery-encounter-phase.ts rename to src/phases/mystery-encounter-phases.ts index cebba02f585..11ff4766695 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -2,7 +2,7 @@ import i18next from "i18next"; import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; -import { hideMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; +import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; import { CheckSwitchPhase, NewBattlePhase, ReturnPhase, ScanIvsPhase, SelectModifierPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases"; import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterVariant } from "../data/mystery-encounters/mystery-encounter"; @@ -14,6 +14,7 @@ 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"; /** * Will handle (in order): @@ -27,6 +28,8 @@ import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-d export class MysteryEncounterPhase extends Phase { optionSelectSettings: OptionSelectSettings; + private FIRST_DIALOGUE_PROMPT_DELAY = 300; + /** * * @param scene @@ -45,9 +48,7 @@ export class MysteryEncounterPhase extends Phase { this.scene.clearPhaseQueue(); this.scene.clearPhaseQueueSplice(); - // Generates seed offset for RNG consistency, but incremented if the same MysteryEncounter has multiple option select cycles - const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000; - this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512; + 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 @@ -78,7 +79,7 @@ export class MysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); } else { this.continueEncounter(); } @@ -108,9 +109,9 @@ export class MysteryEncounterPhase extends Phase { } if (title) { - this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0); + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); } else { - this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true); + this.scene.ui.showText(text, null, nextAction, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); } i++; }; @@ -147,24 +148,46 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { start() { super.start(); - if (this.scene.currentBattle.mysteryEncounter.hideIntroVisuals) { - hideMysteryEncounterIntroVisuals(this.scene).then(() => { + if (this.scene.currentBattle.mysteryEncounter.autoHideIntroVisuals) { + transitionMysteryEncounterIntroVisuals(this.scene).then(() => { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); }); } else { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, 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); + }); + + super.end(); + } +} + /** * Will handle (in order): * - Setting BGM @@ -173,8 +196,11 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { * - Queue the SummonPhases, PostSummonPhases, etc., required to initialize the phase queue for a battle */ export class MysteryEncounterBattlePhase extends Phase { - constructor(scene: BattleScene) { + disableSwitch: boolean; + + constructor(scene: BattleScene, disableSwitch = false) { super(scene); + this.disableSwitch = disableSwitch; } start() { @@ -219,7 +245,7 @@ export class MysteryEncounterBattlePhase extends Phase { } if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) { - scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 1500); + scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 0); } else { this.endBattleSetup(scene); } @@ -240,7 +266,7 @@ export class MysteryEncounterBattlePhase extends Phase { this.endBattleSetup(scene); }; if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) { - scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1500, true); + scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1000, true); } else { doTrainerSummon(); } @@ -253,7 +279,7 @@ export class MysteryEncounterBattlePhase extends Phase { } else { const trainer = this.scene.currentBattle.trainer; let message: string; - scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.seedOffset); + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.getSeedOffset()); const showDialogueAndSummon = () => { scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { @@ -302,7 +328,7 @@ export class MysteryEncounterBattlePhase extends Phase { scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); } - if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE && (scene.currentBattle.waveIndex > 1 || !scene.gameMode.isDaily)) { + if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE && !this.disableSwitch) { const minPartySize = scene.currentBattle.double ? 2 : 1; if (availablePartyMembers.length > minPartySize) { scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double)); @@ -412,7 +438,7 @@ export class PostMysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); } else { this.continueEncounter(); } diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index 65f6ae0edc9..88233032ccd 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -1,17 +1,26 @@ import { Button } from "#app/enums/buttons"; -import { MessagePhase } from "#app/phases"; -import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; +import { CommandPhase, MessagePhase, VictoryPhase } from "#app/phases"; +import { 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"; + +/** + * 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 isBattle - if selecting option should lead to battle, set to true + */ +export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, isBattle: boolean = false) { + // 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); + }); -export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number) { if (game.isCurrentPhase(MessagePhase)) { - // Handle eventual weather messages (e.g. a downpour started!) - game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); await game.phaseInterceptor.run(MessagePhase); } @@ -20,35 +29,77 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); }); + + await game.phaseInterceptor.to(MysteryEncounterPhase, true); + // select the desired option - game.onNextPrompt("MysteryEncounterPhase", Mode.MYSTERY_ENCOUNTER, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that + const uiHandler = game.scene.ui.getHandler(); + uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that - switch (optionNo) { - 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; - } + switch (optionNo) { + 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); - }); - await game.phaseInterceptor.run(MysteryEncounterPhase); + uiHandler.processInput(Button.ACTION); // run the selected options phase game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); }); - await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + + // 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); + }); + + if (isBattle) { + await game.phaseInterceptor.to(CommandPhase); + } else { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } +} + +/** + * 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/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts new file mode 100644 index 00000000000..20d0426e02d --- /dev/null +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -0,0 +1,281 @@ +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 Battle from "#app/battle"; +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 { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases"; +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 { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; + +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 = 45; + +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.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWave(true); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIERY_FALLOUT]], + [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + it("should have the correct properties", async () => { + game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); + 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 () => { + vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: FieryFalloutEncounter } as Battle); + const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true); + const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = FieryFalloutEncounter; + + expect(FieryFalloutEncounter.onInit).toBeDefined(); + + 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", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[0]; + expect(option1.optionMode).toBe(EncounterOptionMode.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 runSelectMysteryEncounterOption(game, 1, 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 runSelectMysteryEncounterOption(game, 1, 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; + }, 100000000); + }); + + describe("Option 2 - Suffer the weather", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[1]; + expect(option1.optionMode).toBe(EncounterOptionMode.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 runSelectMysteryEncounterOption(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 runSelectMysteryEncounterOption(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - use FIRE types", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT); + }); + + it("should have the correct properties", () => { + const option1 = FieryFalloutEncounter.options[2]; + expect(option1.optionMode).toBe(EncounterOptionMode.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 runSelectMysteryEncounterOption(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 runSelectMysteryEncounterOption(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 index 3b8017fa4c6..04854574a77 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -9,11 +9,11 @@ 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 { workaround_reInitSceneWithOverrides } from "#app/test/utils/testUtils"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { runSelectMysteryEncounterOption } from "../encounterTestUtils"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; -const namepsace = "mysteryEncounter:lostAtSea"; +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; @@ -30,8 +30,10 @@ describe("Lost at Sea - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); game.override.mysteryEncounterChance(100); - game.override.startingBiome(defaultBiome); game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWave(true); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ [Biome.SEA, [MysteryEncounterType.LOST_AT_SEA]], @@ -45,28 +47,28 @@ describe("Lost at Sea - Mystery Encounter", () => { }); it("should have the correct properties", async () => { - await workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); + 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: `${namepsace}:intro` }]); - expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namepsace}:title`); - expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namepsace}:description`); - expect(LostAtSeaEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namepsace}:query`); + 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.startingBiome(Biome.MOUNTAIN); - await workaround_reInitSceneWithOverrides(game); 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(10); + game.override.startingWave(9); await game.runToMysteryEncounter(); @@ -74,7 +76,7 @@ describe("Lost at Sea - Mystery Encounter", () => { }); it("should not run above wave 179", async () => { - game.override.startingWave(180); + game.override.startingWave(181); await game.runToMysteryEncounter(); @@ -97,18 +99,22 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 1 - Surf", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); + }); + it("should have the correct properties", () => { const option1 = LostAtSeaEncounter.options[0]; expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ - buttonLabel: `${namepsace}:option:1:label`, - disabledButtonLabel: `${namepsace}:option:1:label_disabled`, - buttonTooltip: `${namepsace}:option:1:tooltip`, - disabledButtonTooltip: `${namepsace}:option:1:tooltip_disabled`, + buttonLabel: `${namespace}:option:1:label`, + disabledButtonLabel: `${namespace}:option:1:label_disabled`, + buttonTooltip: `${namespace}:option:1:tooltip`, + disabledButtonTooltip: `${namespace}:option:1:tooltip_disabled`, selected: [ { - text: `${namepsace}:option:1:selected`, + text: `${namespace}:option:1:selected`, }, ], }); @@ -117,23 +123,21 @@ describe("Lost at Sea - Mystery Encounter", () => { it("should award exp to surfable PKM (Blastoise)", async () => { const laprasSpecies = getPokemonSpecies(Species.LAPRAS); - await workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); const party = game.scene.getParty(); const blastoise = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); const expBefore = blastoise.exp; await runSelectMysteryEncounterOption(game, 2); - expect(blastoise.exp).toBe(expBefore + laprasSpecies.baseExp * defaultWave); + 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 workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); await runSelectMysteryEncounterOption(game, 1); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); @@ -145,19 +149,23 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 2 - Fly", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); + }); + it("should have the correct properties", () => { const option2 = LostAtSeaEncounter.options[1]; expect(option2.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT); expect(option2.dialogue).toBeDefined(); expect(option2.dialogue).toStrictEqual({ - buttonLabel: `${namepsace}:option:2:label`, - disabledButtonLabel: `${namepsace}:option:2:label_disabled`, - buttonTooltip: `${namepsace}:option:2:tooltip`, - disabledButtonTooltip: `${namepsace}:option:2:tooltip_disabled`, + buttonLabel: `${namespace}:option:2:label`, + disabledButtonLabel: `${namespace}:option:2:label_disabled`, + buttonTooltip: `${namespace}:option:2:tooltip`, + disabledButtonTooltip: `${namespace}:option:2:tooltip_disabled`, selected: [ { - text: `${namepsace}:option:2:selected`, + text: `${namespace}:option:2:selected`, }, ], }); @@ -168,23 +176,21 @@ describe("Lost at Sea - Mystery Encounter", () => { const wave = 33; game.override.startingWave(wave); - await workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + 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 runSelectMysteryEncounterOption(game, 2); - expect(pidgeot.exp).toBe(expBefore + laprasBaseExp * wave); + 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 workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); await runSelectMysteryEncounterOption(game, 2); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); @@ -196,17 +202,21 @@ describe("Lost at Sea - Mystery Encounter", () => { }); describe("Option 3 - Wander aimlessy", () => { + beforeEach(async () => { + game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA); + }); + it("should have the correct properties", () => { const option3 = LostAtSeaEncounter.options[2]; expect(option3.optionMode).toBe(EncounterOptionMode.DEFAULT); expect(option3.dialogue).toBeDefined(); expect(option3.dialogue).toStrictEqual({ - buttonLabel: `${namepsace}:option:3:label`, - buttonTooltip: `${namepsace}:option:3:tooltip`, + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, selected: [ { - text: `${namepsace}:option:3:selected`, + text: `${namespace}:option:3:selected`, }, ], }); @@ -215,8 +225,7 @@ describe("Lost at Sea - Mystery Encounter", () => { it("should damage all (allowed in battle) party PKM by 25%", async () => { game.override.startingWave(33); - await workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); const party = game.scene.getParty(); const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA); @@ -237,8 +246,7 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.startingWave(33); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); - workaround_reInitSceneWithOverrides(game); - await game.runToMysteryEncounter(defaultParty); + await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); await runSelectMysteryEncounterOption(game, 3); 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 index 22582f8a8bc..2867bcc63bc 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -36,16 +36,12 @@ describe("Mystery Encounter Utils", () => { 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) - scene.waveSeed = "random"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random"); let result = getRandomPlayerPokemon(scene); expect(result.species.speciesId).toBe(Species.MANAPHY); - scene.waveSeed = "random2"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random2"); result = getRandomPlayerPokemon(scene); expect(result.species.speciesId).toBe(Species.ARCEUS); @@ -60,16 +56,12 @@ describe("Mystery Encounter Utils", () => { }); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) - scene.waveSeed = "random"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random"); let result = getRandomPlayerPokemon(scene); expect(result.species.speciesId).toBe(Species.MANAPHY); - scene.waveSeed = "random2"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random2"); result = getRandomPlayerPokemon(scene); expect(result.species.speciesId).toBe(Species.ARCEUS); @@ -83,16 +75,12 @@ describe("Mystery Encounter Utils", () => { party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) - scene.waveSeed = "random"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random"); let result = getRandomPlayerPokemon(scene, true); expect(result.species.speciesId).toBe(Species.MANAPHY); - scene.waveSeed = "random2"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random2"); result = getRandomPlayerPokemon(scene, true); expect(result.species.speciesId).toBe(Species.MANAPHY); @@ -106,16 +94,12 @@ describe("Mystery Encounter Utils", () => { party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) - scene.waveSeed = "random"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random"); let result = getRandomPlayerPokemon(scene, true, false); expect(result.species.speciesId).toBe(Species.MANAPHY); - scene.waveSeed = "random2"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random2"); result = getRandomPlayerPokemon(scene, true, false); expect(result.species.speciesId).toBe(Species.MANAPHY); @@ -129,16 +113,12 @@ describe("Mystery Encounter Utils", () => { party[0].updateInfo(); // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) - scene.waveSeed = "random"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random"); let result = getRandomPlayerPokemon(scene, true, true); expect(result.species.speciesId).toBe(Species.ARCEUS); - scene.waveSeed = "random2"; - Phaser.Math.RND.sow([scene.waveSeed]); - scene.rngCounter = 0; + game.override.seed("random2"); result = getRandomPlayerPokemon(scene, true, true); expect(result.species.speciesId).toBe(Species.ARCEUS); diff --git a/src/test/mystery-encounter/mystery-encounter.test.ts b/src/test/mystery-encounter/mystery-encounter.test.ts index f4a083c06d4..ef0b5b3238a 100644 --- a/src/test/mystery-encounter/mystery-encounter.test.ts +++ b/src/test/mystery-encounter/mystery-encounter.test.ts @@ -1,9 +1,8 @@ -import { afterEach, beforeAll, beforeEach, expect, describe, it, vi } from "vitest"; -import * as overrides from "../../overrides"; +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-phase"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; describe("Mystery Encounters", () => { @@ -22,33 +21,21 @@ describe("Mystery Encounters", () => { beforeEach(() => { game = new GameManager(phaserGame); - vi.spyOn(overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(256); - vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(11); - vi.spyOn(overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); - - // Seed guarantees wild encounter to be replaced by ME - vi.spyOn(game.scene, "resetSeed").mockImplementation(() => { - game.scene.waveSeed = "test"; - Phaser.Math.RND.sow([game.scene.waveSeed]); - game.scene.rngCounter = 0; - }); + game.override.startingWave(11); + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + game.override.disableTrainerWave(true); }); it("Spawns a mystery encounter", async () => { - await game.runToMysteryEncounter([ - Species.CHARIZARD, - Species.VOLCARONA - ]); + 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([ - Species.CHARIZARD, - Species.VOLCARONA - ]); + 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); diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 0ae9ad35ed7..2a7d3de3700 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -1,14 +1,14 @@ -import {afterEach, beforeAll, beforeEach, expect, describe, it, vi} from "vitest"; -import * as overrides from "../../overrides"; +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-phase"; +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 {MysteryEncounterTier} from "#app/data/mystery-encounters/mystery-encounter"; +import MessageUiHandler from "#app/ui/message-ui-handler"; describe("Mystery Encounter Phases", () => { let phaserGame: Phaser.Game; @@ -26,34 +26,23 @@ describe("Mystery Encounter Phases", () => { beforeEach(() => { game = new GameManager(phaserGame); - vi.spyOn(overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(256); - vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(11); - vi.spyOn(overrides, "MYSTERY_ENCOUNTER_OVERRIDE", "get").mockReturnValue(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); - + game.override.startingWave(11); + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); // Seed guarantees wild encounter to be replaced by ME - vi.spyOn(game.scene, "resetSeed").mockImplementation(() => { - game.scene.waveSeed = "test"; - Phaser.Math.RND.sow([ game.scene.waveSeed ]); - game.scene.rngCounter = 0; - }); + game.override.seed("test"); }); describe("MysteryEncounterPhase", () => { it("Runs to MysteryEncounterPhase", async() => { - await game.runToMysteryEncounter([ - Species.CHARIZARD, - Species.VOLCARONA - ]); + 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([ - Species.CHARIZARD, - Species.VOLCARONA - ]); + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); game.onNextPrompt("MysteryEncounterPhase", Mode.MYSTERY_ENCOUNTER, () => { // End phase early for test @@ -70,27 +59,28 @@ describe("Mystery Encounter Phases", () => { 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([ - Species.CHARIZARD, - Species.VOLCARONA - ]); + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, [Species.CHARIZARD, Species.VOLCARONA]); - game.onNextPrompt("MysteryEncounterPhase", Mode.MYSTERY_ENCOUNTER, () => { - // Select option 1 for encounter - const handler = game.scene.ui.getHandler() as MysteryEncounterUiHandler; - handler.unblockInput(); + game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { + const handler = game.scene.ui.getHandler() as MessageUiHandler; handler.processInput(Button.ACTION); - }, () => !game.isCurrentPhase(MysteryEncounterPhase)); + }); + await game.phaseInterceptor.run(MysteryEncounterPhase); - // After option selected - expect(game.scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterOptionSelectedPhase.name); + // 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), 750, true); + expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 300, true); }); }); diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index 34b55aa30ac..c209ece7e04 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 = []; @@ -11,6 +14,11 @@ export default class TextInterceptor { this.logs.push(text); } + showDialogue(text: string, name: string, delay?: integer, callback?: Function, callbackDelay?: integer, promptDelay?: integer): void { + console.log(name, text); + this.logs.push(name, text); + } + getLatestMessage(): string { return this.logs.pop(); } diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 6c750c60a92..7ccec8113b3 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -34,8 +34,10 @@ import { Button } from "#enums/buttons"; import { BattlerIndex } from "#app/battle.js"; import TargetSelectUiHandler from "#app/ui/target-select-ui-handler.js"; import BattleMessageUiHandler from "#app/ui/battle-message-ui-handler"; -import {MysteryEncounterPhase} from "#app/phases/mystery-encounter-phase"; +import {MysteryEncounterPhase} from "#app/phases/mystery-encounter-phases"; import { OverridesHelper } from "./overridesHelper"; +import { expect } from "vitest"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; /** * Class to manage the game state and transitions between phases. @@ -62,7 +64,7 @@ export default class GameManager { this.phaseInterceptor = new PhaseInterceptor(this.scene); this.textInterceptor = new TextInterceptor(this.scene); this.gameWrapper.setScene(this.scene); - this.override = new OverridesHelper(); + this.override = new OverridesHelper(this); } /** @@ -144,10 +146,11 @@ export default class GameManager { /** * 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(species?: Species[]) { + async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) { await this.runToTitle(); this.onNextPrompt("TitlePhase", Mode.TITLE, () => { @@ -164,6 +167,9 @@ export default class GameManager { }, () => this.isCurrentPhase(MysteryEncounterPhase), true); await this.phaseInterceptor.run(EncounterPhase); + if (encounterType) { + expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType); + } } /** diff --git a/src/test/utils/mocks/mocksContainer/mockContainer.ts b/src/test/utils/mocks/mocksContainer/mockContainer.ts index 4f8a58f7251..ea06c9de326 100644 --- a/src/test/utils/mocks/mocksContainer/mockContainer.ts +++ b/src/test/utils/mocks/mocksContainer/mockContainer.ts @@ -154,6 +154,10 @@ export default class MockContainer { // 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. } diff --git a/src/test/utils/mocks/mocksContainer/mockText.ts b/src/test/utils/mocks/mocksContainer/mockText.ts index 1dd440fde7c..2e6ed67f21f 100644 --- a/src/test/utils/mocks/mocksContainer/mockText.ts +++ b/src/test/utils/mocks/mocksContainer/mockText.ts @@ -17,6 +17,7 @@ export default class MockText { // Phaser.GameObjects.Text.prototype.updateText = () => null; // Phaser.Textures.TextureManager.prototype.addCanvas = () => {}; UI.prototype.showText = this.showText; + UI.prototype.showDialogue = this.showDialogue; // super(scene, x, y); // this.phaserText = new Phaser.GameObjects.Text(scene, x, y, content, styleOptions); } @@ -79,6 +80,13 @@ export default class MockText { } } + 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); } diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts index dd45d972b50..4116810be5d 100644 --- a/src/test/utils/overridesHelper.ts +++ b/src/test/utils/overridesHelper.ts @@ -1,61 +1,113 @@ import { Weather, WeatherType } from "#app/data/weather"; import { Biome } from "#app/enums/biome"; import * as Overrides from "#app/overrides"; -import { vi } from "vitest"; +import { MockInstance, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import * as overrides from "#app/overrides"; +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; /** * Helper to handle overrides in tests */ export class OverridesHelper { - constructor() {} + 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); - vi.spyOn(Overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(rate); + 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 The biome will not be overridden unless you call `workaround_reInitSceneWithOverrides()` (testUtils) + * @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) { - vi.spyOn(Overrides, "STARTING_BIOME_OVERRIDE", "get").mockReturnValue(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) { - vi.spyOn(Overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(wave); + 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 + */ + disableTrainerWave(disable: boolean): MockInstance { + const spy = vi.spyOn(this.game.scene.gameMode, "isWaveTrainer").mockReturnValue(!disable); + 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) { - vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); + const spy = vi.spyOn(Overrides, "WEATHER_OVERRIDE", "get").mockReturnValue(type); this.log(`Weather set to ${Weather[type]} (=${type})!`); + return spy; } /** * Override the seed - * @warning The seed will not be overridden unless you call `workaround_reInitSceneWithOverrides()` (testUtils) * @param seed the seed to set + * @returns spy instance */ seed(seed: string) { - vi.spyOn(Overrides, "SEED_OVERRIDE", "get").mockReturnValue(seed); + 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; } private log(...params: any[]) { diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 71cf6b495ed..7114d63afa0 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -44,7 +44,7 @@ import { MysteryEncounterPhase, MysteryEncounterRewardsPhase, PostMysteryEncounterPhase -} from "#app/phases/mystery-encounter-phase"; +} from "#app/phases/mystery-encounter-phases"; export default class PhaseInterceptor { public scene; @@ -104,11 +104,12 @@ export default class PhaseInterceptor { [MysteryEncounterBattlePhase, this.startPhase], [MysteryEncounterRewardsPhase, this.startPhase], [PostMysteryEncounterPhase, this.startPhase], - [LearnMovePhase, this.startPhase] + [LearnMovePhase, this.startPhase], + // [CommonAnimPhase, this.startPhase] ]; private endBySetMode = [ - TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase, PostMysteryEncounterPhase + TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase, MysteryEncounterPhase, PostMysteryEncounterPhase ]; /** diff --git a/src/test/utils/testUtils.ts b/src/test/utils/testUtils.ts index a8461b3a5db..b922fc9c61c 100644 --- a/src/test/utils/testUtils.ts +++ b/src/test/utils/testUtils.ts @@ -1,6 +1,5 @@ import i18next, { type ParseKeys } from "i18next"; import { vi } from "vitest"; -import GameManager from "./gameManager"; /** * Sets up the i18next mock. @@ -22,15 +21,3 @@ export function mockI18next() { export function arrayOfRange(start: integer, end: integer) { return Array.from({ length: end - start }, (_v, k) => k + start); } - -/** - * Woraround to reinitialize the game scene with overrides being set properly. - * By default the scene is initialized without all overrides even having a chance to be applied. - * @warning USE AT YOUR OWN RISK! Might be deleted in the future - * @param game The game manager - * @deprecated - */ -export async function workaround_reInitSceneWithOverrides(game: GameManager) { - await game.runToTitle(); - game.gameWrapper.setScene(game.scene); -} diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index e6699afbfe8..3031d3d24c5 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -4,7 +4,7 @@ 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-phase"; +import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases"; import { PartyUiMode } from "./party-ui-handler"; import MysteryEncounterOption, { EncounterOptionMode } from "../data/mystery-encounters/mystery-encounter-option"; import * as Utils from "../utils"; @@ -12,6 +12,7 @@ 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 { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -368,7 +369,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5); // Rarity of encounter - const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number); + 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 } });