diff --git a/public/battle-anims/common-powder.json b/public/battle-anims/common-powder.json new file mode 100644 index 00000000000..698d68e7e81 --- /dev/null +++ b/public/battle-anims/common-powder.json @@ -0,0 +1,2496 @@ +{ + "graphic":"PRAS- Powder", + "frames":[ + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 23, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 23, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 21, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 23, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 22, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 0 , + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 23, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 17, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 23, + "opacity": 0 , + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 17, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 24, + "opacity": 0, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 17, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 18, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 20, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 8, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 19, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":16, + "y":2, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 20, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 20, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 9, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 20, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 10, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 11, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 12, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 13, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 14, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 15, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 255, + "priority": 1, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 155, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 150, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + }, + { + "x":-6, + "y":-3, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 155, + "focus":2 + }, + { + "x":12, + "y":0, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 255, + "priority": 1, + "focus":2 + }, + { + "x":-12, + "y":-20, + "zoomX":130, + "zoomY":120, + "visible":true, + "target": 2, + "graphicFrame": 16, + "opacity": 150, + "focus":2 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + } + ], + [ + { + "x":0, + "y":0, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":0, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":2 + }, + { + "x":128, + "y":-64, + "zoomX":100, + "zoomY":100, + "visible":true, + "target":1, + "graphicFrame":0, + "opacity": 255, + "locked": true, + "priority":1, + "focus":1 + } + ] + ], + "frameTimedEvents":{ + "16":[{"frameIndex":16,"resourceName":"PRSFX- Powder common1.wav","volume":100,"pitch":100,"eventType":"AnimTimedSoundEvent"}], + "18":[{"frameIndex":18,"resourceName":"PRSFX- Powder common2.wav","volume":30,"pitch":120,"eventType":"AnimTimedSoundEvent"}] + }, + "position":3, + "hue":0 +} \ No newline at end of file diff --git a/public/images/battle_anims/PRAS- Powder.png b/public/images/battle_anims/PRAS- Powder.png index 3c4a4d2db55..9656c31e6c9 100644 Binary files a/public/images/battle_anims/PRAS- Powder.png and b/public/images/battle_anims/PRAS- Powder.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index f5e3a714df6..c430a12ae3e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2469,6 +2469,24 @@ export default class BattleScene extends SceneBase { } } + /** + * Tries to add the input phase to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} + * @param phase {@linkcode Phase} the phase to be added + * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} + * @returns `true` if a `targetPhase` was found to append to + */ + appendToPhase(phase: Phase, targetPhase: Constructor): boolean { + const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase); + + if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { + this.phaseQueue.splice(targetIndex + 1, 0, phase); + return true; + } else { + this.unshiftPhase(phase); + return false; + } + } + /** * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue * @param message string for MessagePhase diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 5928a65b4e3..26ad65bd9b0 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -90,6 +90,7 @@ export enum CommonAnim { RAGING_BULL_FIRE, RAGING_BULL_WATER, SALT_CURE, + POWDER, SUNNY = 2100, RAIN, SANDSTORM, diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 75b7cddb904..5e6ee334db7 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -856,6 +856,57 @@ export class SeedTag extends BattlerTag { } } +/** + * BattlerTag representing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Powder_(move) | Powder}. + * When the afflicted Pokemon uses a Fire-type move, the move is cancelled, and the + * Pokemon takes damage equal to 1/4 of it's maximum HP (rounded down). + */ +export class PowderTag extends BattlerTag { + constructor() { + super(BattlerTagType.POWDER, [ BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END ], 1); + } + + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + + // "{Pokemon} is covered in powder!" + pokemon.scene.queueMessage(i18next.t("battlerTags:powderOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + /** + * Applies Powder's effects before the tag owner uses a Fire-type move. + * Also causes the tag to expire at the end of turn. + * @param pokemon {@linkcode Pokemon} the owner of this tag + * @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out + * @returns `true` if the tag should not expire after this lapse; `false` otherwise. + */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType === BattlerTagLapseType.PRE_MOVE) { + const movePhase = pokemon.scene.getCurrentPhase(); + if (movePhase instanceof MovePhase) { + const move = movePhase.move.getMove(); + if (pokemon.getMoveType(move) === Type.FIRE) { + movePhase.cancel(); + + pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.POWDER)); + + const cancelDamage = new BooleanHolder(false); + applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage); + if (!cancelDamage.value) { + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER); + } + + // "When the flame touched the powder\non the Pokémon, it exploded!" + pokemon.scene.queueMessage(i18next.t("battlerTags:powderLapse", { moveName: move.name })); + } + } + return true; + } else { + return super.lapse(pokemon, lapseType); + } + } +} + export class NightmareTag extends BattlerTag { constructor() { super(BattlerTagType.NIGHTMARE, BattlerTagLapseType.TURN_END, 1, Moves.NIGHTMARE); @@ -2955,6 +3006,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new InfatuatedTag(sourceMove, sourceId); case BattlerTagType.SEEDED: return new SeedTag(sourceId); + case BattlerTagType.POWDER: + return new PowderTag(); case BattlerTagType.NIGHTMARE: return new NightmareTag(); case BattlerTagType.FRENZY: diff --git a/src/data/move.ts b/src/data/move.ts index 166058178f5..27f7829a920 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6728,6 +6728,126 @@ export class CopyMoveAttr extends OverrideMoveEffectAttr { } } +/** + * Attribute used for moves that causes the target to repeat their last used move. + * + * Used for [Instruct](https://bulbapedia.bulbagarden.net/wiki/Instruct_(move)). +*/ +export class RepeatMoveAttr extends MoveEffectAttr { + constructor() { + super(false, { trigger: MoveEffectTrigger.POST_APPLY }); // needed to ensure correct protect interaction + } + + /** + * Forces the target to re-use their last used move again + * + * @param user {@linkcode Pokemon} that used the attack + * @param target {@linkcode Pokemon} targeted by the attack + * @param move N/A + * @param args N/A + * @returns `true` if the move succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + // get the last move used (excluding status based failures) as well as the corresponding moveset slot + const lastMove = target.getLastXMoves(-1).find(m => m.move !== Moves.NONE)!; + const movesetMove = target.getMoveset().find(m => m?.moveId === lastMove.move)!; + const moveTargets = lastMove.targets ?? []; + + user.scene.queueMessage(i18next.t("moveTriggers:instructingMove", { + userPokemonName: getPokemonNameWithAffix(user), + targetPokemonName: getPokemonNameWithAffix(target) + })); + target.getMoveQueue().unshift({ move: lastMove.move, targets: moveTargets, ignorePP: false }); + target.turnData.extraTurns++; + target.scene.appendToPhase(new MovePhase(target.scene, target, moveTargets, movesetMove), MoveEndPhase); + return true; + } + + getCondition(): MoveConditionFunc { + return (user, target, move) => { + // TODO: Confirm behavior of instructing move known by target but called by another move + const lastMove = target.getLastXMoves(-1).find(m => m.move !== Moves.NONE); + const movesetMove = target.getMoveset().find(m => m?.moveId === lastMove?.move); + const moveTargets = lastMove?.targets ?? []; + // TODO: Add a way of adding moves to list procedurally rather than a pre-defined blacklist + const unrepeatablemoves = [ + // Locking/Continually Executed moves + Moves.OUTRAGE, + Moves.RAGING_FURY, + Moves.ROLLOUT, + Moves.PETAL_DANCE, + Moves.THRASH, + Moves.ICE_BALL, + // Multi-turn Moves + Moves.BIDE, + Moves.SHELL_TRAP, + Moves.BEAK_BLAST, + Moves.FOCUS_PUNCH, + // "First Turn Only" moves + Moves.FAKE_OUT, + Moves.FIRST_IMPRESSION, + Moves.MAT_BLOCK, + // Moves with a recharge turn + Moves.HYPER_BEAM, + Moves.ETERNABEAM, + Moves.FRENZY_PLANT, + Moves.BLAST_BURN, + Moves.HYDRO_CANNON, + Moves.GIGA_IMPACT, + Moves.PRISMATIC_LASER, + Moves.ROAR_OF_TIME, + Moves.ROCK_WRECKER, + Moves.METEOR_ASSAULT, + // Charging & 2-turn moves + Moves.DIG, + Moves.FLY, + Moves.BOUNCE, + Moves.SHADOW_FORCE, + Moves.PHANTOM_FORCE, + Moves.DIVE, + Moves.ELECTRO_SHOT, + Moves.ICE_BURN, + Moves.GEOMANCY, + Moves.FREEZE_SHOCK, + Moves.SKY_DROP, + Moves.SKY_ATTACK, + Moves.SKULL_BASH, + Moves.SOLAR_BEAM, + Moves.SOLAR_BLADE, + Moves.METEOR_BEAM, + // Other moves + Moves.INSTRUCT, + Moves.KINGS_SHIELD, + Moves.SKETCH, + Moves.TRANSFORM, + Moves.MIMIC, + Moves.STRUGGLE, + // TODO: Add Max/G-Move blockage if or when they are implemented + ]; + + if (!movesetMove // called move not in target's moveset (dancer, forgetting the move, etc.) + || movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp + || allMoves[lastMove?.move ?? Moves.NONE].isChargingMove() // called move is a charging/recharging move + || !moveTargets.length // called move has no targets + || unrepeatablemoves.includes(lastMove?.move ?? Moves.NONE)) { // called move is explicitly in the banlist + return false; + } + return true; + }; + } + + getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { + // TODO: Make the AI acutally use instruct + /* Ideally, the AI would score instruct based on the scorings of the on-field pokemons' + * last used moves at the time of using Instruct (by the time the instructor gets to act) + * with respect to the user's side. + * In 99.9% of cases, this would be the pokemon's ally (unless the target had last + * used a move like Decorate on the user or its ally) + */ + return 2; + } +} + /** * Attribute used for moves that reduce PP of the target's last used move. * Used for Spite. @@ -8546,7 +8666,7 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.SPDEF ], -1) .ballBombMove(), new AttackMove(Moves.FUTURE_SIGHT, Type.PSYCHIC, MoveCategory.SPECIAL, 120, 100, 10, -1, 0, 2) - .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc + .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc, should not apply abilities or held items if user is off the field .ignoresProtect() .attr(DelayedAttackAttr, ArenaTagType.FUTURE_SIGHT, ChargeAnim.FUTURE_SIGHT_CHARGING, i18next.t("moveTriggers:foresawAnAttack", { pokemonName: "{USER}" })), new AttackMove(Moves.ROCK_SMASH, Type.FIGHTING, MoveCategory.PHYSICAL, 40, 100, 15, 50, 0, 2) @@ -8856,7 +8976,7 @@ export function initMoves() { .attr(ConfuseAttr) .pulseMove(), new AttackMove(Moves.DOOM_DESIRE, Type.STEEL, MoveCategory.SPECIAL, 140, 100, 5, -1, 0, 3) - .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc + .partial() // cannot be used on multiple Pokemon on the same side in a double battle, hits immediately when called by Metronome/etc, should not apply abilities or held items if user is off the field .ignoresProtect() .attr(DelayedAttackAttr, ArenaTagType.DOOM_DESIRE, ChargeAnim.DOOM_DESIRE_CHARGING, i18next.t("moveTriggers:choseDoomDesireAsDestiny", { pokemonName: "{USER}" })), new AttackMove(Moves.PSYCHO_BOOST, Type.PSYCHIC, MoveCategory.SPECIAL, 140, 90, 5, -1, 0, 3) @@ -9612,9 +9732,10 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], -1, false, { condition: (user, target, move) => target.status?.effect === StatusEffect.POISON || target.status?.effect === StatusEffect.TOXIC }) .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) + .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .ignoresSubstitute() .powderMove() - .unimplemented(), + .edgeCase(), // does not cancel Fire-type moves generated by Dancer new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) @@ -9892,7 +10013,8 @@ export function initMoves() { .attr(StatStageChangeAttr, [ Stat.ATK ], -1), new StatusMove(Moves.INSTRUCT, Type.PSYCHIC, -1, 15, -1, 0, 7) .ignoresSubstitute() - .unimplemented(), + .attr(RepeatMoveAttr) + .edgeCase(), // incorrect interactions with Gigaton Hammer, Blood Moon & Torment new AttackMove(Moves.BEAK_BLAST, Type.FLYING, MoveCategory.PHYSICAL, 100, 100, 15, -1, -3, 7) .attr(BeakBlastHeaderAttr) .ballBombMove() diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 934fc1b805b..fa445d75d4f 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -582,7 +582,13 @@ function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon receivedPokemonTintSprite.setTintFill(getPokeballTintColor(receivedPokemon.pokeball)); [ tradedPokemonSprite, tradedPokemonTintSprite ].map(sprite => { - sprite.play(tradedPokemon.getSpriteKey(true)); + const spriteKey = tradedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey()); @@ -597,7 +603,13 @@ function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon }); [ receivedPokemonSprite, receivedPokemonTintSprite ].map(sprite => { - sprite.play(receivedPokemon.getSpriteKey(true)); + const spriteKey = receivedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey()); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index ab6517e97af..877deee66b7 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -34,6 +34,7 @@ export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) .withEncounterTier(MysteryEncounterTier.COMMON) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) + .withScenePartySizeRequirement(2, 6, true) .withAutoHideIntroVisuals(false) .withCatchAllowed(true) .withIntroSpriteConfigs([ diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 7adcfdc4974..072b5e5b160 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -290,7 +290,10 @@ export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, if (damage <= 0) { console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead."); } - + // If a Pokemon would faint from the damage applied, its HP is instead set to 1. + if (pokemon.isAllowedInBattle() && pokemon.hp - damage <= 0) { + damage = pokemon.hp - 1; + } applyHpChangeToPokemon(scene, pokemon, -damage); } diff --git a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts index fcadb101817..424ba15f811 100644 --- a/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts +++ b/src/data/mystery-encounters/utils/encounter-transformation-sequence.ts @@ -54,7 +54,13 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke pokemonEvoTintSprite.setTintFill(0xFFFFFF); [ pokemonSprite, pokemonTintSprite, pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { - sprite.play(previousPokemon.getSpriteKey(true)); + const spriteKey = previousPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(previousPokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", previousPokemon.getSpriteKey()); @@ -69,7 +75,13 @@ export function doPokemonTransformationSequence(scene: BattleScene, previousPoke }); [ pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => { - sprite.play(transformPokemon.getSpriteKey(true)); + const spriteKey = transformPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", transformPokemon.getSpriteKey()); sprite.setPipelineData("shiny", transformPokemon.shiny); diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index bb969386630..f28ac37ae27 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -93,4 +93,5 @@ export enum BattlerTagType { GRUDGE = "GRUDGE", PSYCHO_SHIFT = "PSYCHO_SHIFT", ENDURE_TOKEN = "ENDURE_TOKEN", + POWDER = "POWDER", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index c22c755fc6e..fcfc2ff7536 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3501,12 +3501,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE); } - canSetStatus(effect: StatusEffect | undefined, quiet: boolean = false, overrideStatus: boolean = false, sourcePokemon: Pokemon | null = null): boolean { + /** + * Checks if a status effect can be applied to the Pokemon. + * + * @param effect The {@linkcode StatusEffect} whose applicability is being checked + * @param quiet Whether in-battle messages should trigger or not + * @param overrideStatus Whether the Pokemon's current status can be overriden + * @param sourcePokemon The Pokemon that is setting the status effect + * @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered + */ + canSetStatus(effect: StatusEffect | undefined, quiet: boolean = false, overrideStatus: boolean = false, sourcePokemon: Pokemon | null = null, ignoreField: boolean = false): boolean { if (effect !== StatusEffect.FAINT) { if (overrideStatus ? this.status?.effect === effect : this.status) { return false; } - if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY) { + if (this.isGrounded() && (!ignoreField && this.scene.arena.terrain?.terrainType === TerrainType.MISTY)) { return false; } } @@ -3556,7 +3565,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } break; case StatusEffect.FREEZE: - if (this.isOfType(Type.ICE) || (this.scene?.arena?.weather?.weatherType && [ WeatherType.SUNNY, WeatherType.HARSH_SUN ].includes(this.scene.arena.weather.weatherType))) { + if (this.isOfType(Type.ICE) || (!ignoreField && (this.scene?.arena?.weather?.weatherType && [ WeatherType.SUNNY, WeatherType.HARSH_SUN ].includes(this.scene.arena.weather.weatherType)))) { return false; } break; @@ -3741,8 +3750,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { setFrameRate(frameRate: integer) { this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; - this.getSprite().play(this.getBattleSpriteKey()); - this.getTintSprite()?.play(this.getBattleSpriteKey()); + try { + this.getSprite().play(this.getBattleSpriteKey()); + } catch (err: unknown) { + console.error(`Failed to play animation for ${this.getBattleSpriteKey()}`, err); + } + try { + this.getTintSprite()?.play(this.getBattleSpriteKey()); + } catch (err: unknown) { + console.error(`Failed to play animation for ${this.getBattleSpriteKey()}`, err); + } } tint(color: number, alpha?: number, duration?: integer, ease?: string) { @@ -5277,6 +5294,7 @@ export class PokemonBattleSummonData { export class PokemonTurnData { public flinched: boolean = false; public acted: boolean = false; + /** How many times the move should hit the target(s) */ public hitCount: number = 0; /** * - `-1` = Calculate how many hits are left @@ -5295,6 +5313,11 @@ export class PokemonTurnData { public switchedInThisTurn: boolean = false; public failedRunAway: boolean = false; public joinedRound: boolean = false; + /** + * Used to make sure multi-hits occur properly when the user is + * forced to act again in the same turn + */ + public extraTurns: number = 0; } export enum AiType { diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 04776afc624..57b3ced1813 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1751,19 +1751,59 @@ const modifierPool: ModifierPool = { || (p.isFusion() && checkedSpecies.includes(p.getFusionSpeciesForm(true).speciesId)))) ? 12 : 0; }, 12), new WeightedModifierType(modifierTypes.TOXIC_ORB, (party: Pokemon[]) => { - const checkedAbilities = [ Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.TOXIC_BOOST, Abilities.POISON_HEAL, Abilities.MAGIC_GUARD ]; - const checkedMoves = [ Moves.FACADE, Moves.TRICK, Moves.FLING, Moves.SWITCHEROO, Moves.PSYCHO_SHIFT ]; - // If a party member doesn't already have one of these two orbs and has one of the above moves or abilities, the orb can appear - return party.some(p => !p.getHeldItems().some(i => i instanceof TurnStatusEffectModifier) - && (checkedAbilities.some(a => p.hasAbility(a, false, true)) - || p.getMoveset(true).some(m => m && checkedMoves.includes(m.moveId)))) ? 10 : 0; + return party.some(p => { + const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId); + + const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); + const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ] + .some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves from comment they are implemented + const hasItemMoves = [ /* Moves.TRICK, Moves.FLING, Moves.SWITCHEROO */ ] + .some(m => moveset.includes(m)); + // Abilities that take advantage of obtaining the actual status effect + const hasRelevantAbilities = [ Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.TOXIC_BOOST, Abilities.POISON_HEAL, Abilities.MAGIC_GUARD ] + .some(a => p.hasAbility(a, false, true)); + + if (!isHoldingOrb) { + if (canSetStatus) { + return hasRelevantAbilities || hasStatusMoves; + } else { + return hasItemMoves; + } + } + return false; + }) ? 10 : 0; }, 10), new WeightedModifierType(modifierTypes.FLAME_ORB, (party: Pokemon[]) => { - const checkedAbilities = [ Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.FLARE_BOOST, Abilities.MAGIC_GUARD ]; - const checkedMoves = [ Moves.FACADE, Moves.TRICK, Moves.FLING, Moves.SWITCHEROO, Moves.PSYCHO_SHIFT ]; - // If a party member doesn't already have one of these two orbs and has one of the above moves or abilities, the orb can appear - return party.some(p => !p.getHeldItems().some(i => i instanceof TurnStatusEffectModifier) - && (checkedAbilities.some(a => p.hasAbility(a, false, true)) || p.getMoveset(true).some(m => m && checkedMoves.includes(m.moveId)))) ? 10 : 0; + return party.some(p => { + const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId); + const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); + const isHoldingOrb = p.getHeldItems().some(i => i.type.id === "FLAME_ORB" || i.type.id === "TOXIC_ORB"); + + // Moves that take advantage of obtaining the actual status effect + const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ] + .some(m => moveset.includes(m)); + // Moves that take advantage of being able to give the target a status orb + // TODO: Take moves from comment they are implemented + const hasItemMoves = [ /* Moves.TRICK, Moves.FLING, Moves.SWITCHEROO */ ] + .some(m => moveset.includes(m)); + // Abilities that take advantage of obtaining the actual status effect + const hasRelevantAbilities = [ Abilities.QUICK_FEET, Abilities.GUTS, Abilities.MARVEL_SCALE, Abilities.FLARE_BOOST, Abilities.MAGIC_GUARD ] + .some(a => p.hasAbility(a, false, true)); + + if (!isHoldingOrb) { + if (canSetStatus) { + return hasRelevantAbilities || hasStatusMoves; + } else { + return hasItemMoves; + } + } + return false; + }) ? 10 : 0; }, 10), new WeightedModifierType(modifierTypes.WHITE_HERB, (party: Pokemon[]) => { const checkedAbilities = [ Abilities.WEAK_ARMOR, Abilities.CONTRARY, Abilities.MOODY, Abilities.ANGER_SHELL, Abilities.COMPETITIVE, Abilities.DEFIANT ]; diff --git a/src/overrides.ts b/src/overrides.ts index dc166307314..85be47d95cc 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -177,7 +177,11 @@ class DefaultOverrides { // MYSTERY ENCOUNTER OVERRIDES // ------------------------- - /** 1 to 256, set to null to ignore */ + /** + * `1` (almost never) to `256` (always), set to `null` to disable the override + * + * Note: Make sure `STARTING_WAVE_OVERRIDE > 10`, otherwise MEs won't trigger + */ readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = null; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier | null = null; readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = null; diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index d45c580228c..803fd478fd4 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -330,7 +330,12 @@ export class EggHatchPhase extends Phase { this.scene.validateAchv(achvs.HATCH_SHINY); } this.eggContainer.setVisible(false); - this.pokemonSprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + this.pokemonSprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } this.pokemonSprite.setPipelineData("ignoreTimeTint", true); this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); this.pokemonSprite.setPipelineData("shiny", this.pokemon.shiny); diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 36bd3b7bd81..76e521c9b3d 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -105,7 +105,13 @@ export class EvolutionPhase extends Phase { this.scene.ui.add(this.evolutionOverlay); [ this.pokemonSprite, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) }); sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey()); @@ -130,7 +136,13 @@ export class EvolutionPhase extends Phase { this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { [ this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(evolvedPokemon.getSpriteKey(true)); + const spriteKey = evolvedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", evolvedPokemon.getSpriteKey()); sprite.setPipelineData("shiny", evolvedPokemon.shiny); diff --git a/src/phases/form-change-phase.ts b/src/phases/form-change-phase.ts index 410163a70e4..b042cd98294 100644 --- a/src/phases/form-change-phase.ts +++ b/src/phases/form-change-phase.ts @@ -39,7 +39,13 @@ export class FormChangePhase extends EvolutionPhase { this.pokemon.getPossibleForm(this.formChange).then(transformedPokemon => { [ this.pokemonEvoSprite, this.pokemonEvoTintSprite ].map(sprite => { - sprite.play(transformedPokemon.getSpriteKey(true)); + const spriteKey = transformedPokemon.getSpriteKey(true); + try { + sprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } + sprite.setPipelineData("ignoreTimeTint", true); sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey()); sprite.setPipelineData("shiny", transformedPokemon.shiny); diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 84a4a4e8ef9..52d0996b946 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -23,6 +23,12 @@ import * as Utils from "#app/utils"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; +import { SessionSaveData } from "#app/system/game-data"; +import PersistentModifierData from "#app/system/modifier-data"; +import PokemonData from "#app/system/pokemon-data"; +import ChallengeData from "#app/system/challenge-data"; +import TrainerData from "#app/system/trainer-data"; +import ArenaData from "#app/system/arena-data"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; export class GameOverPhase extends BattlePhase { @@ -109,7 +115,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.isVictory); + const fadeDuration = this.isVictory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); @@ -135,8 +141,11 @@ export class GameOverPhase extends BattlePhase { this.scene.unshiftPhase(new GameOverModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PREMIUM)); } } - this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase)); - this.end(); + this.getRunHistoryEntry().then(runHistoryEntry => { + this.scene.gameData.saveRunHistory(this.scene, runHistoryEntry, this.isVictory); + this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase)); + this.end(); + }); }; if (this.isVictory && this.scene.gameMode.isClassic) { @@ -212,5 +221,34 @@ export class GameOverPhase extends BattlePhase { this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter))); } } + + /** + * Slightly modified version of {@linkcode GameData.getSessionSaveData}. + * @returns A promise containing the {@linkcode SessionSaveData} + */ + private async getRunHistoryEntry(): Promise { + const preWaveSessionData = await this.scene.gameData.getSession(this.scene.sessionSlotId); + return { + seed: this.scene.seed, + playTime: this.scene.sessionPlayTime, + gameMode: this.scene.gameMode.modeId, + party: this.scene.getPlayerParty().map(p => new PokemonData(p)), + enemyParty: this.scene.getEnemyParty().map(p => new PokemonData(p)), + modifiers: preWaveSessionData ? preWaveSessionData.modifiers : this.scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), + enemyModifiers: preWaveSessionData ? preWaveSessionData.enemyModifiers : this.scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), + arena: new ArenaData(this.scene.arena), + pokeballCounts: this.scene.pokeballCounts, + money: Math.floor(this.scene.money), + score: this.scene.score, + waveIndex: this.scene.currentBattle.waveIndex, + battleType: this.scene.currentBattle.battleType, + trainer: this.scene.currentBattle.trainer ? new TrainerData(this.scene.currentBattle.trainer) : null, + gameVersion: this.scene.game.config.gameVersion, + timestamp: new Date().getTime(), + challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), + mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1, + mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData + } as SessionSaveData; + } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index ea45dc2b9e2..d08fc46e563 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -56,7 +56,7 @@ import { PokemonMultiHitModifier, } from "#app/modifier/modifier"; import { PokemonPhase } from "#app/phases/pokemon-phase"; -import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; +import { BooleanHolder, executeIf, isNullOrUndefined, NumberHolder } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import i18next from "i18next"; @@ -106,7 +106,9 @@ export class MoveEffectPhase extends PokemonPhase { */ return super.end(); } - user.resetTurnData(); + if (isNullOrUndefined(user.turnData)) { + user.resetTurnData(); + } } } @@ -127,6 +129,14 @@ export class MoveEffectPhase extends PokemonPhase { user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); + // If the user is acting again (such as due to Instruct), reset hitsLeft/hitCount so that + // the move executes correctly (ensures all hits of a multi-hit are properly calculated) + if (user.turnData.hitsLeft === 0 && user.turnData.hitCount > 0 && user.turnData.extraTurns > 0) { + user.turnData.hitsLeft = -1; + user.turnData.hitCount = 0; + user.turnData.extraTurns--; + } + /** * If this phase is for the first hit of the invoked move, * resolve the move's total hit count. This block combines the @@ -313,7 +323,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. + * Create a Promise that applies *all* effects from the invoked move's MoveEffectAttrs. * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * type requires different conditions to be met with respect to the move's hit result. */ diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index 6c84c0d1a8a..c9e5bec845d 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -43,7 +43,12 @@ export class QuietFormChangePhase extends BattlePhase { const getPokemonSprite = () => { const sprite = this.scene.addPokemonSprite(this.pokemon, this.pokemon.x + this.pokemon.getSprite().x, this.pokemon.y + this.pokemon.getSprite().y, "pkmn__sub"); sprite.setOrigin(0.5, 1); - sprite.play(this.pokemon.getBattleSpriteKey()).stop(); + const spriteKey = this.pokemon.getBattleSpriteKey(); + try { + sprite.play(spriteKey).stop(); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } sprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) }); [ "spriteColors", "fusionSpriteColors" ].map(k => { if (this.pokemon.summonData?.speciesForm) { @@ -81,7 +86,12 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.setVisible(false); this.pokemon.changeForm(this.formChange).then(() => { pokemonFormTintSprite.setScale(0.01); - pokemonFormTintSprite.play(this.pokemon.getBattleSpriteKey()).stop(); + const spriteKey = this.pokemon.getBattleSpriteKey(); + try { + pokemonFormTintSprite.play(spriteKey).stop(); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } pokemonFormTintSprite.setVisible(true); this.scene.tweens.add({ targets: pokemonTintSprite, diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index 6790e7e632c..c2f54fa4cfc 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -1,5 +1,5 @@ import { Type } from "#enums/type"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; import { toDmgValue } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -8,7 +8,7 @@ import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Parental Bond", () => { @@ -470,4 +470,25 @@ describe("Abilities - Parental Bond", () => { expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1); } ); + + it("should not allow Future Sight to hit infinitely many times if the user switches out", async () => { + game.override.enemyLevel(1000) + .moveset(Moves.FUTURE_SIGHT); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "damageAndUpdate"); + + game.move.select(Moves.FUTURE_SIGHT); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.doSwitchPokemon(2); + await game.toNextTurn(); + + // TODO: Update hit count to 1 once Future Sight is fixed to not activate abilities if user is off the field + expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index a19b5c82546..6f9b2195a9c 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -25,10 +25,13 @@ describe("Abilities - Serene Grace", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override + .disableCrits() .battleType("single") .ability(Abilities.SERENE_GRACE) - .moveset([ Moves.AIR_SLASH, Moves.TACKLE ]) + .moveset([ Moves.AIR_SLASH ]) + .enemySpecies(Species.ALOLA_GEODUDE) .enemyLevel(10) + .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset([ Moves.SPLASH ]); }); diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts index f4b4c5712ee..bd586878fce 100644 --- a/src/test/items/multi_lens.test.ts +++ b/src/test/items/multi_lens.test.ts @@ -24,7 +24,7 @@ describe("Items - Multi Lens", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([ Moves.TACKLE, Moves.TRAILBLAZE, Moves.TACHYON_CUTTER ]) + .moveset([ Moves.TACKLE, Moves.TRAILBLAZE, Moves.TACHYON_CUTTER, Moves.FUTURE_SIGHT ]) .ability(Abilities.BALL_FETCH) .startingHeldItems([{ name: "MULTI_LENS" }]) .battleType("single") @@ -170,6 +170,7 @@ describe("Items - Multi Lens", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5); }); + it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => { game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) .moveset(Moves.SUPER_FANG) @@ -188,4 +189,24 @@ describe("Items - Multi Lens", () => { await game.phaseInterceptor.to("MoveEndPhase"); expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 5); }); + + it("should not allow Future Sight to hit infinitely many times if the user switches out", async () => { + game.override.enemyLevel(1000); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "damageAndUpdate"); + + game.move.select(Moves.FUTURE_SIGHT); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.doSwitchPokemon(2); + await game.toNextTurn(); + + // TODO: Update hit count to 1 once Future Sight is fixed to not activate held items if user is off the field + expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(2); + }); }); diff --git a/src/test/moves/instruct.test.ts b/src/test/moves/instruct.test.ts new file mode 100644 index 00000000000..0e227ef6a3f --- /dev/null +++ b/src/test/moves/instruct.test.ts @@ -0,0 +1,315 @@ +import { BattlerIndex } from "#app/battle"; +import type Pokemon from "#app/field/pokemon"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Instruct", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + function instructSuccess(pokemon: Pokemon, move: Moves): void { + expect(pokemon.getLastXMoves(-1)[0].move).toBe(move); + expect(pokemon.getLastXMoves(-1)[1].move).toBe(pokemon.getLastXMoves()[0].move); + expect(pokemon.getMoveset().find(m => m?.moveId === move)?.ppUsed).toBe(2); + } + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .battleType("single") + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.NO_GUARD) + .enemyLevel(100) + .startingLevel(100) + .ability(Abilities.BALL_FETCH) + .moveset([ Moves.INSTRUCT, Moves.SONIC_BOOM, Moves.SPLASH, Moves.TORMENT ]) + .disableCrits(); + }); + + it("should repeat enemy's attack move when moving last", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + const enemy = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemy, Moves.SONIC_BOOM); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); + instructSuccess(enemy, Moves.SONIC_BOOM); + }); + + it("should repeat enemy's move through substitute", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + const enemy = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemy, [ Moves.SONIC_BOOM, Moves.SUBSTITUTE ]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SUBSTITUTE); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.toNextTurn(); + + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.SONIC_BOOM); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); + instructSuccess(game.scene.getEnemyPokemon()!, Moves.SONIC_BOOM); + }); + + it("should repeat ally's attack on enemy", async () => { + game.override + .battleType("double") + .moveset([]); + await game.classicMode.startBattle([ Species.AMOONGUSS, Species.SHUCKLE ]); + + const [ amoonguss, shuckle ] = game.scene.getPlayerField(); + game.move.changeMoveset(amoonguss, Moves.INSTRUCT); + game.move.changeMoveset(shuckle, Moves.SONIC_BOOM); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + game.move.select(Moves.SONIC_BOOM, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getEnemyField()[0].getInverseHp()).toBe(40); + instructSuccess(shuckle, Moves.SONIC_BOOM); + }); + + // TODO: Enable test case once gigaton hammer (and blood moon) is fixed + it.todo("should repeat enemy's Gigaton Hammer", async () => { + game.override + .enemyLevel(5); + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + const enemy = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemy, Moves.GIGATON_HAMMER); + + game.move.select(Moves.INSTRUCT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + instructSuccess(enemy, Moves.GIGATON_HAMMER); + }); + + it("should respect enemy's status condition", async () => { + game.override + .moveset([ Moves.THUNDER_WAVE, Moves.INSTRUCT ]) + .enemyMoveset([ Moves.SPLASH, Moves.SONIC_BOOM ]); + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + game.move.select(Moves.THUNDER_WAVE); + await game.forceEnemyMove(Moves.SONIC_BOOM); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.move.forceStatusActivation(true); + await game.phaseInterceptor.to("MovePhase"); + await game.move.forceStatusActivation(false); + await game.phaseInterceptor.to("TurnEndPhase", false); + + const moveHistory = game.scene.getEnemyPokemon()!.getMoveHistory(); + expect(moveHistory.length).toBe(3); + expect(moveHistory[0].move).toBe(Moves.SONIC_BOOM); + expect(moveHistory[1].move).toBe(Moves.NONE); + expect(moveHistory[2].move).toBe(Moves.SONIC_BOOM); + }); + + it("should not repeat enemy's out of pp move", async () => { + game.override.enemySpecies(Species.UNOWN); + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemyPokemon, Moves.HIDDEN_POWER); + const moveUsed = enemyPokemon.moveset.find(m => m?.moveId === Moves.HIDDEN_POWER)!; + moveUsed.ppUsed = moveUsed.getMovePp() - 1; + + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.HIDDEN_POWER); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + const playerMove = game.scene.getPlayerPokemon()!.getLastXMoves()!; + expect(playerMove[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.getMoveHistory().length).toBe(1); + }); + + it("should fail if no move has yet been used by target", async () => { + game.override.enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should attempt to call enemy's disabled move, but move use itself should fail", async () => { + game.override + .moveset([ Moves.INSTRUCT, Moves.DISABLE ]) + .battleType("double"); + await game.classicMode.startBattle([ Species.AMOONGUSS, Species.DROWZEE ]); + + const [ enemy1, enemy2 ] = game.scene.getEnemyField(); + game.move.changeMoveset(enemy1, Moves.SONIC_BOOM); + game.move.changeMoveset(enemy2, Moves.SPLASH); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.DISABLE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getPlayerField()[0].getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + const enemyMove = game.scene.getEnemyPokemon()!.getLastXMoves()[0]; + expect(enemyMove.result).toBe(MoveResult.FAIL); + expect(game.scene.getEnemyPokemon()!.getMoveset().find(m => m?.moveId === Moves.SONIC_BOOM)?.ppUsed).toBe(1); + + }); + + it("should not repeat enemy's move through protect", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + + const MoveToUse = Moves.PROTECT; + const enemyPokemon = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemyPokemon, MoveToUse); + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.PROTECT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(enemyPokemon.getLastXMoves(-1)[0].move).toBe(Moves.PROTECT); + expect(enemyPokemon.getLastXMoves(-1)[1]).toBeUndefined(); // undefined because protect failed + expect(enemyPokemon.getMoveset().find(m => m?.moveId === Moves.PROTECT)?.ppUsed).toBe(1); + }); + + it("should not repeat enemy's charging move", async () => { + game.override + .enemyMoveset([ Moves.SONIC_BOOM, Moves.HYPER_BEAM ]) + .enemyLevel(5); + await game.classicMode.startBattle([ Species.SHUCKLE ]); + + const player = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.battleSummonData.moveHistory = [{ move: Moves.SONIC_BOOM, targets: [ BattlerIndex.PLAYER ], result: MoveResult.SUCCESS, virtual: false }]; + + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.HYPER_BEAM); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + + game.move.select(Moves.INSTRUCT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should not repeat dance move not known by target", async () => { + game.override + .battleType("double") + .moveset([ Moves.INSTRUCT, Moves.FIERY_DANCE ]) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.DANCER); + await game.classicMode.startBattle([ Species.SHUCKLE, Species.SHUCKLE ]); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + expect(game.scene.getPlayerField()[0].getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should cause multi-hit moves to hit the appropriate number of times in singles", async () => { + game.override + .enemyAbility(Abilities.SKILL_LINK) + .enemyMoveset(Moves.BULLET_SEED); + await game.classicMode.startBattle([ Species.BULBASAUR ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + + game.move.select(Moves.INSTRUCT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.turnData.attacksReceived.length).toBe(10); + + await game.toNextTurn(); + game.move.select(Moves.INSTRUCT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.turnData.attacksReceived.length).toBe(10); + }); + + it("should cause multi-hit moves to hit the appropriate number of times in doubles", async () => { + game.override + .battleType("double") + .enemyAbility(Abilities.SKILL_LINK) + .enemyMoveset([ Moves.BULLET_SEED, Moves.SPLASH ]) + .enemyLevel(5); + await game.classicMode.startBattle([ Species.BULBASAUR, Species.IVYSAUR ]); + + const [ , ivysaur ] = game.scene.getPlayerField(); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.BULLET_SEED, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + game.move.select(Moves.INSTRUCT, 0, BattlerIndex.ENEMY); + game.move.select(Moves.INSTRUCT, 1, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.BULLET_SEED, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(ivysaur.turnData.attacksReceived.length).toBe(15); + + await game.toNextTurn(); + game.move.select(Moves.INSTRUCT, 0, BattlerIndex.ENEMY); + game.move.select(Moves.INSTRUCT, 1, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.BULLET_SEED, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(ivysaur.turnData.attacksReceived.length).toBe(15); + }); +}); diff --git a/src/test/moves/powder.test.ts b/src/test/moves/powder.test.ts new file mode 100644 index 00000000000..5c0f318d620 --- /dev/null +++ b/src/test/moves/powder.test.ts @@ -0,0 +1,205 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { MoveResult } from "#app/field/pokemon"; +import { Type } from "#enums/type"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { StatusEffect } from "#enums/status-effect"; + +describe("Moves - Powder", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override.battleType("single"); + + game.override + .enemySpecies(Species.SNORLAX) + .enemyLevel(100) + .enemyMoveset(Moves.EMBER) + .enemyAbility(Abilities.INSOMNIA) + .startingLevel(100) + .moveset([ Moves.POWDER, Moves.SPLASH, Moves.FIERY_DANCE ]); + }); + + it( + "should cancel the target's Fire-type move and damage the target", + async () => { + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + + await game.toNextTurn(); + + game.move.select(Moves.SPLASH); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should have no effect against Grass-type Pokemon", + async () => { + game.override.enemySpecies(Species.AMOONGUSS); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }); + + it( + "should have no effect against Pokemon with Overcoat", + async () => { + game.override.enemyAbility(Abilities.OVERCOAT); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }); + + it( + "should not damage the target if the target has Magic Guard", + async () => { + game.override.enemyAbility(Abilities.MAGIC_GUARD); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }); + + it( + "should not prevent the target from thawing out with Flame Wheel", + async () => { + game.override + .enemyMoveset(Array(4).fill(Moves.FLAME_WHEEL)) + .enemyStatusEffect(StatusEffect.FREEZE); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.FREEZE); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + } + ); + + it( + "should not allow a target with Protean to change to Fire type", + async () => { + game.override.enemyAbility(Abilities.PROTEAN); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(enemyPokemon.summonData?.types).not.toBe(Type.FIRE); + }); + + // TODO: Implement this interaction to pass this test + it.skip( + "should cancel Fire-type moves generated by the target's Dancer ability", + async () => { + game.override + .enemySpecies(Species.BLASTOISE) + .enemyAbility(Abilities.DANCER); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.FIERY_DANCE); + + await game.phaseInterceptor.to(MoveEffectPhase); + const enemyStartingHp = enemyPokemon.hp; + + await game.phaseInterceptor.to(BerryPhase, false); + // player should not take damage + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + // enemy should have taken damage from player's Fiery Dance + 2 Powder procs + expect(enemyPokemon.hp).toBe(enemyStartingHp - 2 * Math.floor(enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should cancel Revelation Dance if it becomes a Fire-type move", + async () => { + game.override + .enemySpecies(Species.CHARIZARD) + .enemyMoveset(Array(4).fill(Moves.REVELATION_DANCE)); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should cancel Shell Trap and damage the target, even if the move would fail", + async () => { + game.override.enemyMoveset(Array(4).fill(Moves.SHELL_TRAP)); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }); +}); diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index 73fe63395fd..68d3b3d51d7 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -1,4 +1,6 @@ import { BattlerIndex } from "#app/battle"; +import type Pokemon from "#app/field/pokemon"; +import { PokemonMove } from "#app/field/pokemon"; import Overrides from "#app/overrides"; import { CommandPhase } from "#app/phases/command-phase"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; @@ -71,4 +73,21 @@ export class MoveHelper extends GameManagerHelper { await this.game.phaseInterceptor.to("MovePhase"); vi.spyOn(Overrides, "STATUS_ACTIVATION_OVERRIDE", "get").mockReturnValue(null); } + + /** + * Used when the normal moveset override can't be used (such as when it's necessary to check updated properties of the moveset). + * @param pokemon - The pokemon being modified + * @param moveset - The moveset to use + */ + public changeMoveset(pokemon: Pokemon, moveset: Moves | Moves[]): void { + if (!Array.isArray(moveset)) { + moveset = [ moveset ]; + } + pokemon.moveset = []; + moveset.forEach((move) => { + pokemon.moveset.push(new PokemonMove(move)); + }); + const movesetStr = moveset.map((moveId) => Moves[moveId]).join(", "); + console.log(`Pokemon ${pokemon.species.name}'s moveset manually set to ${movesetStr} (=[${moveset.join(", ")}])!`); + } } diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts index ace9d956dd9..071690aee54 100644 --- a/src/ui/run-info-ui-handler.ts +++ b/src/ui/run-info-ui-handler.ts @@ -118,6 +118,7 @@ export default class RunInfoUiHandler extends UiHandler { this.runResultContainer = this.scene.add.container(0, 24); const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth - 11, 65); runResultWindow.setOrigin(0, 0); + runResultWindow.setName("Run_Result_Window"); this.runResultContainer.add(runResultWindow); if (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) { this.parseRunResult(); @@ -254,8 +255,6 @@ export default class RunInfoUiHandler extends UiHandler { * Mystery Encounters contain sprites associated with MEs + the title of the specific ME. */ private parseRunStatus() { - const runStatusText = addTextObject(this.scene, 6, 5, `${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex} - ${getBiomeName(this.runInfo.arena.biome)}`, TextStyle.WINDOW, { fontSize : "65px", lineSpacing: 0.1 }); - const enemyContainer = this.scene.add.container(0, 0); this.runResultContainer.add(enemyContainer); if (this.runInfo.battleType === BattleType.WILD) { @@ -271,7 +270,7 @@ export default class RunInfoUiHandler extends UiHandler { const pokeball = this.scene.add.sprite(0, 0, "pb"); pokeball.setFrame(getPokeballAtlasKey(p.pokeball)); pokeball.setScale(0.5); - pokeball.setPosition(52 + ((i % row_limit) * 8), (i <= 2) ? 18 : 25); + pokeball.setPosition(58 + ((i % row_limit) * 8), (i <= 2) ? 18 : 25); enemyContainer.add(pokeball); }); const trainerObj = this.runInfo.trainer.toTrainer(this.scene); @@ -286,7 +285,7 @@ export default class RunInfoUiHandler extends UiHandler { const descContainer = this.scene.add.container(0, 0); const textBox = addTextObject(this.scene, 0, 0, boxString, TextStyle.WINDOW, { fontSize : "35px", wordWrap: { width: 200 }}); descContainer.add(textBox); - descContainer.setPosition(52, 29); + descContainer.setPosition(55, 32); this.runResultContainer.add(descContainer); } else if (this.runInfo.battleType === BattleType.MYSTERY_ENCOUNTER) { const encounterExclaim = this.scene.add.sprite(0, 0, "encounter_exclaim"); @@ -303,7 +302,17 @@ export default class RunInfoUiHandler extends UiHandler { this.runResultContainer.add([ encounterExclaim, subSprite, descContainer ]); } - this.runResultContainer.add(runStatusText); + const runResultWindow = this.runResultContainer.getByName("Run_Result_Window") as Phaser.GameObjects.Image; + const windowCenterX = runResultWindow.getTopCenter().x; + const windowBottomY = runResultWindow.getBottomCenter().y; + + const runStatusText = addTextObject(this.scene, windowCenterX, 5, `${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, TextStyle.WINDOW, { fontSize : "60px", lineSpacing: 0.1 }); + runStatusText.setOrigin(0.5, 0); + + const currentBiomeText = addTextObject(this.scene, windowCenterX, windowBottomY - 5, `${getBiomeName(this.runInfo.arena.biome)}`, TextStyle.WINDOW, { fontSize: "60px" }); + currentBiomeText.setOrigin(0.5, 1); + + this.runResultContainer.add([ runStatusText, currentBiomeText ]); this.runContainer.add(this.runResultContainer); } @@ -387,12 +396,12 @@ export default class RunInfoUiHandler extends UiHandler { tObjSprite.setPosition(-9, -3); tObjPartnerSprite.setScale(0.55); doubleContainer.add([ tObjSprite, tObjPartnerSprite ]); - doubleContainer.setPosition(28, 40); + doubleContainer.setPosition(28, 34); } enemyContainer.add(doubleContainer); } else { - const scale = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? 0.35 : 0.65; - const position = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? [ 12, 28 ] : [ 32, 36 ]; + const scale = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? 0.35 : 0.55; + const position = (this.runDisplayMode === RunDisplayMode.RUN_HISTORY) ? [ 12, 28 ] : [ 30, 32 ]; tObjSprite.setScale(scale, scale); tObjSprite.setPosition(position[0], position[1]); enemyContainer.add(tObjSprite); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 2e05f3de4c1..63ef6155fbc 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -321,8 +321,12 @@ export default class SummaryUiHandler extends UiHandler { this.numberText.setText(Utils.padInt(this.pokemon.species.speciesId, 4)); this.numberText.setColor(this.getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD)); this.numberText.setShadowColor(this.getTextColor(!this.pokemon.isShiny() ? TextStyle.SUMMARY : TextStyle.SUMMARY_GOLD, true)); - - this.pokemonSprite.play(this.pokemon.getSpriteKey(true)); + const spriteKey = this.pokemon.getSpriteKey(true); + try { + this.pokemonSprite.play(spriteKey); + } catch (err: unknown) { + console.error(`Failed to play animation for ${spriteKey}`, err); + } this.pokemonSprite.setPipelineData("teraColor", getTypeRgb(this.pokemon.getTeraType())); this.pokemonSprite.setPipelineData("ignoreTimeTint", true); this.pokemonSprite.setPipelineData("spriteKey", this.pokemon.getSpriteKey());