From 83c70889fc69a55e48fa7d743425cecbefff7306 Mon Sep 17 00:00:00 2001 From: Flashfyre Date: Wed, 18 Oct 2023 18:01:15 -0400 Subject: [PATCH] Implement trainer battles and various changes Implement trainer battles; add dialogue functionality; add random session seed for predictable random results; remove capitalization from text; add full party heal after every 10 waves --- index.html | 1 + public/audio/bgm/bw/heal.mp3 | Bin 0 -> 53520 bytes public/audio/se/{pb.wav => pb_tray_ball.wav} | Bin public/audio/se/pb_tray_empty.wav | Bin 0 -> 5940 bytes public/audio/se/pb_tray_enter.wav | Bin 0 -> 47932 bytes .../trainer/{rival_m.json => player.json} | 2 +- .../trainer/{rival_m.png => player.png} | Bin .../trainer/{rival_f.json => rival.json} | 2 +- .../images/trainer/{rival_f.png => rival.png} | Bin public/images/ui/namebox.png | Bin 0 -> 278 bytes public/images/ui/pb_tray_ball.json | 104 +++++ public/images/ui/pb_tray_ball.png | Bin 0 -> 205 bytes public/images/ui/pb_tray_overlay_enemy.png | Bin 0 -> 157 bytes public/images/ui/pb_tray_overlay_player.png | Bin 0 -> 156 bytes public/images/ui/summary_moves_effect.png | Bin 902 -> 4827 bytes src/arena.ts | 92 ++++- src/battle-phases.ts | 249 +++++++++--- src/battle-scene.ts | 118 +++++- src/battle.ts | 86 ++++- src/data/ability.ts | 228 +++++------ src/data/battle-stat.ts | 14 +- src/data/berry.ts | 5 +- src/data/biome.ts | 12 +- src/data/move.ts | 16 +- src/data/pokeball.ts | 3 +- src/data/pokemon-species.ts | 9 +- src/data/pokemon-stat.ts | 4 +- src/data/trainer-type.ts | 357 ++++++++++++++++-- src/data/weather.ts | 2 +- src/debug.js | 6 + src/main.ts | 3 +- src/modifier/modifier-type.ts | 142 +++---- src/pokemon.ts | 7 +- src/system/game-data.ts | 11 +- src/system/game-speed.ts | 23 ++ src/system/trainer-data.ts | 4 +- src/system/unlockables.ts | 2 +- src/trainer.ts | 142 ++++++- src/ui/ball-ui-handler.ts | 2 +- src/ui/battle-message-ui-handler.ts | 43 ++- src/ui/command-ui-handler.ts | 3 +- src/ui/message-ui-handler.ts | 8 + src/ui/modifier-select-ui-handler.ts | 2 +- src/ui/party-ui-handler.ts | 12 +- src/ui/pokeball-tray.ts | 116 ++++++ src/ui/starter-select-ui-handler.ts | 6 +- src/ui/summary-ui-handler.ts | 15 +- src/ui/text.ts | 1 - src/ui/ui.ts | 8 + src/utils.ts | 39 +- 50 files changed, 1500 insertions(+), 399 deletions(-) create mode 100644 public/audio/bgm/bw/heal.mp3 rename public/audio/se/{pb.wav => pb_tray_ball.wav} (100%) create mode 100644 public/audio/se/pb_tray_empty.wav create mode 100644 public/audio/se/pb_tray_enter.wav rename public/images/trainer/{rival_m.json => player.json} (99%) rename public/images/trainer/{rival_m.png => player.png} (100%) rename public/images/trainer/{rival_f.json => rival.json} (99%) rename public/images/trainer/{rival_f.png => rival.png} (100%) create mode 100644 public/images/ui/namebox.png create mode 100644 public/images/ui/pb_tray_ball.json create mode 100644 public/images/ui/pb_tray_ball.png create mode 100644 public/images/ui/pb_tray_overlay_enemy.png create mode 100644 public/images/ui/pb_tray_overlay_player.png create mode 100644 src/debug.js create mode 100644 src/ui/pokeball-tray.ts diff --git a/index.html b/index.html index 4516b63d6f7..c8d0fa6b964 100644 --- a/index.html +++ b/index.html @@ -31,6 +31,7 @@
+ \ No newline at end of file diff --git a/public/audio/bgm/bw/heal.mp3 b/public/audio/bgm/bw/heal.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..22b2d9fb8a3045ea01714e10d46b0731d455ce27 GIT binary patch literal 53520 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 literal 0 HcmV?d00001 diff --git a/public/audio/se/pb.wav b/public/audio/se/pb_tray_ball.wav similarity index 100% rename from public/audio/se/pb.wav rename to public/audio/se/pb_tray_ball.wav diff --git a/public/audio/se/pb_tray_empty.wav b/public/audio/se/pb_tray_empty.wav new file mode 100644 index 0000000000000000000000000000000000000000..ca14f42e699b53700cdfcff8b17765c96820004e GIT binary patch literal 5940 zcmbW*y^2;>7>42PU|~63+8lrzW)ArUDvDY;d=k=x*oqjT(7Uk>(j@tAK(0!S^s|g^YeTE z`uY2*++WSFdzWrz`Eqb-@^EhIolyRzzIW~D(c#_BuG^j)%G<=Jzxe!gnx)O1U8dS@ zyV~7ucjT(MPkVkVY_8qum#-T?-L|G*efM^EcfNi-Z@^PVN3L(Vb!E!kFYV|;J$#{! zdQ;<_aJOT_Te{8iil(1`pb?o!HMcW~5m!I0o`gr9_t6uIduI|j+gFBxFXO=#(@9}7h_v%*4uA}%d0>6%*pO>*KL2@cya1Hdr%JUenY#>$~+7n>b<+se0AH^ zWb3)@%l`_tyX({7-KNYAr`Db2^;=%KTW9f^!DV2(def6B%D_{1`Fd`6(rwOM-)L8! zR^-_#*6Xd`?9ygt@|n@M$H#a5ooAD#l% zJ9X^xXWky%<$1F!%g4Qi*RR%G_2zePo_aR;-Ko}F#ydNt*~fRdwCeNbSND4$&wEg} hxoYjT?A$;$8mvFWu+={{=ouc+mg= literal 0 HcmV?d00001 diff --git a/public/audio/se/pb_tray_enter.wav b/public/audio/se/pb_tray_enter.wav new file mode 100644 index 0000000000000000000000000000000000000000..5cfdd4dd961ad1e5a99d9eee5d2b84d9947b0858 GIT binary patch literal 47932 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 r!U78mEG)3Fz`_Cx3oIqS literal 0 HcmV?d00001 diff --git a/public/images/trainer/rival_m.json b/public/images/trainer/player.json similarity index 99% rename from public/images/trainer/rival_m.json rename to public/images/trainer/player.json index 7a54d1f008b..cb65f57d909 100644 --- a/public/images/trainer/rival_m.json +++ b/public/images/trainer/player.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "rival_m.png", + "image": "player.png", "format": "RGBA8888", "size": { "w": 196, diff --git a/public/images/trainer/rival_m.png b/public/images/trainer/player.png similarity index 100% rename from public/images/trainer/rival_m.png rename to public/images/trainer/player.png diff --git a/public/images/trainer/rival_f.json b/public/images/trainer/rival.json similarity index 99% rename from public/images/trainer/rival_f.json rename to public/images/trainer/rival.json index c2dd781d311..6c80e5382eb 100644 --- a/public/images/trainer/rival_f.json +++ b/public/images/trainer/rival.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "rival_f.png", + "image": "rival.png", "format": "RGBA8888", "size": { "w": 267, diff --git a/public/images/trainer/rival_f.png b/public/images/trainer/rival.png similarity index 100% rename from public/images/trainer/rival_f.png rename to public/images/trainer/rival.png diff --git a/public/images/ui/namebox.png b/public/images/ui/namebox.png new file mode 100644 index 0000000000000000000000000000000000000000..ed43bc4aa5b3deeee6405495a264e637b5485723 GIT binary patch literal 278 zcmeAS@N?(olHy`uVBq!ia0vp^A|TAc1|)ksWqEGkH+-3duO1&8;1nzm57#M!a=VOtYhm!cGZog0d zU$=P%7BFv=+;&j0;bChFOW1VJJrxH93KE#ZoF)6E%6Araue9UK<8kVMsGBEgX WRtHbcnz0?|Q3g*}KbLh*2~7a(mut`f literal 0 HcmV?d00001 diff --git a/public/images/ui/pb_tray_ball.json b/public/images/ui/pb_tray_ball.json new file mode 100644 index 00000000000..0fee95aeff5 --- /dev/null +++ b/public/images/ui/pb_tray_ball.json @@ -0,0 +1,104 @@ +{ + "textures": [ + { + "image": "pb_tray_ball.png", + "format": "RGBA8888", + "size": { + "w": 28, + "h": 7 + }, + "scale": 1, + "frames": [ + { + "filename": "ball", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 7, + "h": 7 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 7, + "h": 7 + }, + "frame": { + "x": 0, + "y": 0, + "w": 7, + "h": 7 + } + }, + { + "filename": "empty", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 7, + "h": 7 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 7, + "h": 7 + }, + "frame": { + "x": 7, + "y": 0, + "w": 7, + "h": 7 + } + }, + { + "filename": "faint", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 7, + "h": 7 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 7, + "h": 7 + }, + "frame": { + "x": 14, + "y": 0, + "w": 7, + "h": 7 + } + }, + { + "filename": "status", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 7, + "h": 7 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 7, + "h": 7 + }, + "frame": { + "x": 21, + "y": 0, + "w": 7, + "h": 7 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:f7a37dfea18d90e97d6fe0b1f2e1f69c:6823410fc296184fa84e87f6ca79158a:097b8eb81a77f8ed73a75d6d94f48dac$" + } +} diff --git a/public/images/ui/pb_tray_ball.png b/public/images/ui/pb_tray_ball.png new file mode 100644 index 0000000000000000000000000000000000000000..112184f8593529ae800aed2ee06982a8b481d63c GIT binary patch literal 205 zcmeAS@N?(olHy`uVBq!ia0vp^GC<7E!3-pI4&D6+q@)9ULR^8gms3zopr09T?(U;qFB literal 0 HcmV?d00001 diff --git a/public/images/ui/pb_tray_overlay_enemy.png b/public/images/ui/pb_tray_overlay_enemy.png new file mode 100644 index 0000000000000000000000000000000000000000..c557ac736fc23ea939679c73e8520d9ecf15d1ad GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^89>a!!VDxQu%vGWQlbGqA+CApB@6~^ik1vP*~!&Y zS}cQ{fHLRPr5k`0V@Z%-FoVOh8)-m}nWu|m2*>s01O-72;|YvY9CpPt9AM$-X;hHl zaB6fA<(N{yl-i+|uE3CK#L6^bnSw&Vp~c(`jZcI>PTH6j2{eJh)78&qol`;+0O9l~ A&Hw-a literal 0 HcmV?d00001 diff --git a/public/images/ui/pb_tray_overlay_player.png b/public/images/ui/pb_tray_overlay_player.png new file mode 100644 index 0000000000000000000000000000000000000000..4a065de5d7d2ed1b7896ede42b9361be9c85cf15 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^89>a!!VDxQu%vGWQlbGqA+9Bv`MHVdLD|WQmJF6b zPSsOdfHEQ){SSZ?V@Z%-FoVOh8)-m}si%u$2*>s0goK2EGzD1=>B)=P)eG2|#1#S< ynN$@xn3@6<4lF-@gN4Ik)|>{0C7z-{2E(B=vGUtWBHBO$7(8A5T-G@yGywqKIVXSs literal 0 HcmV?d00001 diff --git a/public/images/ui/summary_moves_effect.png b/public/images/ui/summary_moves_effect.png index ca3a9ed9cba5ec4e2a1002fd60391422ec05d6f1..ed422d1b439069a3b74ac2c45a1c2241bac0906d 100644 GIT binary patch literal 4827 zcmV<15+v=3P) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3&scI!G0g#U9Dy#&r6m&5U#-a#+FKTxtW5BI(o zC$=L?B!)tvAe;5S|8Daieq#0B#3UuxoGm}Ga;Wj9*z2dBXJ_MiK0i6$pSickO<4B_ zojwwUHe6*Kv1SXzFcIu0>v( z+V6$N!@?p5tXf(7zCx9zV~*d5%ZpXf?dpQ0~PD z$Q$<_uhzZ}(3>DX-kJB&uU`b;xBHCnE|&R@5pO>Iz@>M`!*hTCdgJi4$iMx-M(01c zbN}r<%iinquEk75^NpwzZ1-4(6FVz~b6Msg{3YJYc~l<7Hakf9w8M5D!&T>r{x&YU z>6+Vao!8+8CPu$JIq}{PAI`Btj;bF)^Az>rwwF-D424_{zYM91!9Q(rXWx4E8?LbP zbe!rPi#aa&@?pOF@J}9Q@1Y@b8cb5}t^|7xIA&<(^qI3rh}(DG(h2b6ai`zD0v1pu zgXx61;9#@;TB0ZSf~|1!95^rWdX*F@)@m_~LbMt`T?6^~qj>S)u?FrkT=r_3_zY|EJiQ>?hs%1c&Rb+rxFcGz*J zoww|=>u%4i-C6zc_=BvuJ8S-AO3#yL);MZJ9!of-lcYH#V?H`EE}8)V?KNk%xEQ@= zPIG3fvRv{QnKU{hmu<&xk>tGgs2;H*4MUt#C8=aS}P z_q)uv8VBC4AaUopN75@(U&_kW%^ug@J=cwcruo@=X?-QKGku9o)|kEfys>%1*Kx*C zSFkPk*7=A}@|rt@(Kg}OKmx;!?%QYWy9?$z*<-sI1DmA5b77AO-_7iGUt;4*+_=y4 zk~n%+F3z(U%ux=igT~_KsGNyqaSo|7yE*$FqD#zHS0p`EApPw%xl&$5z+55dK343Z zUI%Srbj_EKxwKR(q1Z`P$7-?8IxHB%UKVPV;I>_wB&*~fdn=$1u$9praFY}(`hjZ) zt6GG_n0Au`+SPGTBdm0@ZOvxRb*8s8`D_m&zpm4YIQo8p6m2eF*A*;uL8 z>egJ*=1@xLRC@riWJCp!m!5)biq>urU{d~Q=2R<11KKA^tPglE@w>UUv+rd)Ft^t9 zNVG+5nMqgW_?$SAZEK)~^6y6YOMPsPm-+0z<9Pcf*rDVUly)!5nih2xWapf18>{_Grl?CI`fmXY_-jk|8eo z0s>>7HslnWg05Tq-V_=NVj?*VkY$a{NgfWR0${k6piw6^7Fom)KFG0*t>OcYnKe7g z*@;r(6qu&TUI;Bfl{{?L&_F`=MxPPQ@*%=~yCJ3SR61m~X|vC*Y02i*)@9`7l+(uo z0&QV@298Mc1bAgrRustw;z+G^}}sdX;hM#Gk-T`{~=@+oAfivat*6z>X+>hyWp zq%Xu`cko4x??Rhp}OfP-f`hW}CMTHH$yTc3`M(=@pa!O!Gea zbSSQ$NgeMj)JK>##=1YQSl%x9{d(qJ@Uh;?cWi9YW4sA*Zh$d!1I)WYL|_SN&{1Q8 z@TG}SHL8!aky@qLo+435G*k#fYRhXhDk-kW2m^~Z8q(o^{(5WLOsG&%Qx`)G)&O0y zv?@TqOFjgN~_xr-37a>cC0V3y1ou{&fconrUtbk zGvEx!ixw1T-X1;0@LdCX2j^faR$nLpY@ilPC8`C4;{|C&vB7-Q0RvP>i^K_ycdMoW zz%dVSJT&4Nlj>ldh2(-!O53$l<;js;j}|hW+NGjpRk|OQO(?6x*KR$URn>}#)*B>Er)aH@I^PA zf@RcY*CIvna58dL#Sgz3&1mU^{14pIPOFPLra^M>$+$kPyBnHr2S1a9uc+>UCQ6uK z(+9@ds9+qxoj5W|7oG-7I<)D;jfn6~=*Te|y0|iZBe)hl4PGg*qUlOXtfD1=F2@Ao zmAn+k?B9?M@6c@&-WR%Ko66&@%obMOFdv4pAY`Ac!kZ%#B)AOi0gs`1PzDjNCNdoq8^?Hrw zLPw@DPIZ!Ks-H9|4?z*9B>@mRGF>^N%n6Y#Z5j9~k@GS_UX;4kv1`+10VT>5X+sFt zrrV?7>0FRm#1-g5Z)kvY(Nd8TOoCx4)RcAJA=;C%iI^*}+vy^`o-n zT6^GozI9D)lry+Vu1D2>Rh}X-B@(TvtSBrB3UynkOCg zScCR2oPlSQsul+pP_xvlh7U;$BcCN{KsqMVqboX_VG7yTg&okMu8_WX+h}haB`xjZ z$P(we&LCGp7$CW3ag87;;UoVNmn1}9eI01qE2zj4#2Is&RwON-m^q!2X+jo^>g^|o zfx|E+>wPgKnc6rW-g)E);zM3-(!brre76fs{c#T_W3HIbFbibm(lYJnGtkJ;4zhjq zm*lhuXpF#TZ(`(bP2%nl-uMxs&D5IUBXYp_&ZpJB_UAKiZeR2^rN5-<(;f5TkKP|} zC$T^Iba~VsQVu#J*nxtw%B?k{>@#*OXfcRK!N?_`v|jq_G-TRAX=x|Ukmq&hP}phMCpK^ zf{qbaI2dYhj|q~41>h4k>s1styqY>#@vGkjeE&*{yeeU%3V27x&J$*Dm6p%EL0hMB zGg{wz`Ek;#Q$UTLblMP#&HDkEmajtC7JU+l4X z{t-WO)azQ)iOntqsX0cbjXHP*A4Yny-@@nlYQBY!g6BPS9>95r8}rw|c?Sve4ildM z=OelfaNeQD{3UEYyqZ_AQP8}Hi~{B~t0!Q-z>E1~ziisL$i&ZNq{-w#AzQI`3L3oCQjz}to(hP%#THe=8xSYPu5Pe zN=Xc9Na0dE17#lt8BlN=KB^4oE=;r}V~|dyl?g&(=?kkUqlVy4h;w12)UQVdgQm`r zYogO9a-%X!iYcCk&r(D0OfH}64opR&_kZU#L<}4Bh7%|pJ*4JV*Px&n56vtDH$YY# zxuD7vk$!4S(3;nDF__m7Cj&flWH84)Ezs{X2D>1d$HiCPsPf5tkeI2mT`W~_770+M^Ib=Hm)9;y z9nhiCyK%n%O;T)r@2^pZ5&x~%L~GoY<|9o-%S^8Llo>dfJr#CW=KP%7Ud>b2Uol+6 zk}2iPH}}?xiECoLwmz>lwZdj=l=&!Li8j2LfU14Pd+UJnuWi`8vzyl?6N3dW`jSjD6j>u+lv< zrRbL*VDmVA@N})O-y83%9bH@K^y=^Fwf6|Vz!*P76GJR(NJ2?belWhy4)k* zuh)H>>u(TlzRR*|vuB<)%3k?rR`Y*A^nmr@EM|fL00D$)LqkwWLqi~Na&Km7Y-Iod zc$|HaJxIeq9K~N#rBW(I>>$!1Lv^wsD&iR*@9q zbK)_BE=c^yb=l=N&Si%Mo*6N6nK|MJu~_b6xrH9AV~VO#zL0lW;k?CJt<_ok zp8SQOqPCpnI?X7OSVRhGh>%f76*X9h)2fkTB18KL5C5>^Pm@a~R~3vL^Von2$?=2# z!S8O((&U7j6iNWSFSh+L0t9w}R>QWxk8Qhk0{EYSE3M<-XaX~zq>R><9>N0~gmF zP2K}8cYvWMT{0v`^3xPbCE)#xzNr8V-U7i@x3~5_P9K0Ab+vQ@92^3pWy)Ujcz0iS zZ~vZY_xA&ky>hc|U;_dG000JJOGiWi{{R3005d0i^8f$<32;bRa{vGf6951U69E94 zoEQKA00(qQO+^Ri3K0({2)e@5s{jB2rAb6VRCwC$oH0^^Koo}mI2LF{nUs4AwX$?* zy#ie*IR+(mTp=yBl^%m~Q(9>{Er?7YNs)0e?B@F#=Dj2v*pK&Mo1ID0)8pN@U=qxb zF)gllgWK14W@IWthB6k~w8yB8%{HF@#!-JDW8=NS^DI?SR!;KERnhy8L%HE+r_i*Q zr}g&OsJ%eDjjf{puaLizXX$8pW8+@FC@bYkrEDiYGM=|B%LRLPW;sqiSK9yHvEO;d zmCM13vXZf}jE(j3oIB4C#rr<_Ubn1^e9Ic8WjW3dpDCeLiBlm(S#`anYPl-92l=5Y3Pn)2Qr}&NERqkIWE0e#XOb^JE99YEdvo%qkeG#kuf1($ zgeSw>bx3G!>f#rMfDnWLLJ$H70fZm~5P}du2toiM2myouLJ$H7K?ooOA%GBs073vE z2myp31Q3D{KnOwrAqWA4074J~2tf!S1R-!nAx#}IBU2J$>sU>F!>pW&1EYcD80C<%w0u=i@yya_$O|*o=Ql`~at1qRqu}`7Hnd002ovPDHLkV1jaT BKu`bx delta 880 zcmV-$1CRXMC58u(BYy);Nkl8vuaEhx>m(vBlg1>>dx^ zPQOR!9t*;QaP>Ufb%D{X zPhnLpfVDed?GAo?FTL}1;6qn_&@L;l?BVuhR}q#jEy4(h&$K)Eo6^a|`_f^0>-QgzFc&)4Hff4&eR>G-m7QM}D! zOs-7k$!nJy#3VB{Qo6mdHWmQ_Z7vcKAVdTR0YXH8L=K3pcEIpW=QM|da_$oHqpL&o zqHk^5KZ=wat1SozTb;#;Ar(SJOI(q1A0ga9ETKnM_`tJNWAdjIr4Lwhqx7e=dQ8x=lIFBPCjeCac(Pz<{=1L za(_rt^N=Y8W8Y$z;o|=%%ujIl0O9k=6{4nqptzc$7fdN zvVInKaE$QtOik8?Wg)Va5Us>Q{ycmoja=J80O(`= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; @@ -47,7 +49,7 @@ export class Arena { if (!tierPool.length) ret = this.scene.randomSpecies(waveIndex, level); else { - const entry = tierPool[Utils.randInt(tierPool.length)]; + const entry = tierPool[Utils.randSeedInt(tierPool.length)]; let species: Species; if (typeof entry === 'number') species = entry as Species; @@ -58,7 +60,7 @@ export class Arena { if (level >= levelThreshold) { const speciesIds = entry[levelThreshold]; if (speciesIds.length > 1) - species = speciesIds[Utils.randInt(speciesIds.length)]; + species = speciesIds[Utils.randSeedInt(speciesIds.length)]; else species = speciesIds[0]; break; @@ -99,9 +101,25 @@ export class Arena { return ret; } + randomTrainerType(waveIndex: integer): TrainerType { + const isBoss = waveIndex % 10 === 0 && !!this.trainerPool[BiomePoolTier.BOSS].length + && (this.biomeType !== Biome.END || this.scene.gameMode !== GameMode.ENDLESS || waveIndex % 250 === 0); + const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64); + let tier = !isBoss + ? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE + : tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; + console.log(BiomePoolTier[tier]); + while (tier && !this.trainerPool[tier].length) { + console.log(`Downgraded trainer rarity tier from ${BiomePoolTier[tier]} to ${BiomePoolTier[tier - 1]}`); + tier--; + } + const tierPool = this.trainerPool[tier] || []; + return !tierPool.length ? TrainerType.BREEDER : tierPool[Utils.randSeedInt(tierPool.length)]; + } + getFormIndex(species: PokemonSpecies) { if (!species.canChangeForm && species.forms?.length) - return Utils.randInt(species.forms.length); // TODO: Base on biome + return Utils.randSeedInt(species.forms.length); // TODO: Base on biome return 0; } @@ -182,6 +200,45 @@ export class Arena { return this.weather.getAttackTypeMultiplier(attackType); } + getTrainerChance(): integer { + switch (this.biomeType) { + case Biome.CITY: + case Biome.BEACH: + case Biome.DOJO: + case Biome.CONSTRUCTION_SITE: + return 4; + case Biome.PLAINS: + case Biome.GRASS: + case Biome.LAKE: + case Biome.CAVE: + return 6; + case Biome.TALL_GRASS: + case Biome.FOREST: + case Biome.SEA: + case Biome.SWAMP: + case Biome.MOUNTAIN: + case Biome.BADLANDS: + case Biome.DESERT: + case Biome.MEADOW: + case Biome.POWER_PLANT: + case Biome.GRAVEYARD: + case Biome.FACTORY: + return 8; + case Biome.ICE_CAVE: + case Biome.VOLCANO: + case Biome.RUINS: + case Biome.WASTELAND: + case Biome.JUNGLE: + return 12; + case Biome.SEABED: + case Biome.ABYSS: + case Biome.SPACE: + return 16; + default: + return 0; + } + } + isDaytime(): boolean { switch (this.biomeType) { case Biome.TOWN: @@ -195,6 +252,7 @@ export class Arena { case Biome.DESERT: case Biome.MEADOW: case Biome.DOJO: + case Biome.CONSTRUCTION_SITE: return true; } } @@ -349,12 +407,12 @@ export class ArenaBase extends Phaser.GameObjects.Container { this.player = player; - this.base = scene.add.sprite(0, 0, `plains_a`); + this.base = scene.add.sprite(0, 0, 'plains_a'); this.base.setOrigin(0, 0); this.props = !player ? new Array(3).fill(null).map(() => { - const ret = scene.add.sprite(0, 0, `plains_b`); + const ret = scene.add.sprite(0, 0, 'plains_b'); ret.setOrigin(0, 0); ret.setVisible(false); return ret; @@ -372,14 +430,16 @@ export class ArenaBase extends Phaser.GameObjects.Container { this.add(this.base); if (!this.player) { - this.propValue = propValue === undefined - ? hasProps ? Utils.randInt(8) : 0 - : propValue; - this.props.forEach((prop, p) => { - prop.setTexture(`${biomeKey}_b${hasProps ? `_${p + 1}` : ''}`); - prop.setVisible(hasProps && !!(this.propValue & (1 << p))); - this.add(prop); - }); + (this.scene as BattleScene).executeWithSeedOffset(() => { + this.propValue = propValue === undefined + ? hasProps ? Utils.randSeedInt(8) : 0 + : propValue; + this.props.forEach((prop, p) => { + prop.setTexture(`${biomeKey}_b${hasProps ? `_${p + 1}` : ''}`); + prop.setVisible(hasProps && !!(this.propValue & (1 << p))); + this.add(prop); + }); + }, (this.scene as BattleScene).currentBattle?.waveIndex || 0); } } } \ No newline at end of file diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 55f3a048871..bec3b3e51ba 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -80,7 +80,7 @@ export class CheckLoadPhase extends BattlePhase { this.scene.pushPhase(new SummonPhase(this.scene, 0)); if (this.scene.currentBattle.double && availablePartyMembers > 1) this.scene.pushPhase(new SummonPhase(this.scene, 1)); - if (this.scene.currentBattle.waveIndex > 1) { + if (this.scene.currentBattle.waveIndex > 1 && this.scene.currentBattle.battleType !== BattleType.TRAINER) { this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double)); if (this.scene.currentBattle.double && availablePartyMembers > 1) this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double)); @@ -237,11 +237,14 @@ export class EncounterPhase extends BattlePhase { const battle = this.scene.currentBattle; battle.enemyLevels.forEach((level, e) => { - const enemySpecies = battle.battleType === BattleType.TRAINER - ? this.scene.currentBattle.trainer.genPartyMemberSpecies(level) - : this.scene.randomSpecies(battle.waveIndex, level, null, true); - if (!this.loaded) - battle.enemyParty[e] = new EnemyPokemon(this.scene, enemySpecies, level); + if (!this.loaded) { + if (battle.battleType === BattleType.TRAINER) + battle.enemyParty[e] = battle.trainer.genPartyMember(e); + else { + const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, null, true); + battle.enemyParty[e] = new EnemyPokemon(this.scene, enemySpecies, level); + } + } const enemyPokemon = this.scene.getEnemyParty()[e]; if (e < (battle.double ? 2 : 1)) { enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); @@ -255,7 +258,7 @@ export class EncounterPhase extends BattlePhase { }); if (battle.battleType === BattleType.TRAINER) - loadEnemyAssets.push(battle.trainer.loadAssets()); + loadEnemyAssets.push(battle.trainer.loadAssets().then(() => battle.trainer.initSprite())); Promise.all(loadEnemyAssets).then(() => { battle.enemyParty.forEach((enemyPokemon, e) => { @@ -321,23 +324,45 @@ export class EncounterPhase extends BattlePhase { : `A wild ${enemyField[0].name}\nand ${enemyField[1].name} appeared!`; this.scene.ui.showText(text, null, () => this.end(), 1500); } else if (this.scene.currentBattle.battleType === BattleType.TRAINER) { - this.scene.currentBattle.trainer.untint(100, 'Sine.easeOut'); - this.scene.currentBattle.trainer.playAnim(); - const text = `${this.scene.currentBattle.trainer.getName()}\nwould like to battle!`; - this.scene.ui.showText(text, null, () => { - this.scene.tweens.add({ - targets: this.scene.currentBattle.trainer, - x: '+=16', - y: '-=16', - alpha: 0, - ease: 'Sine.easeInOut', - duration: 750 - }); - this.scene.unshiftPhase(new SummonPhase(this.scene, 0, false)); - if (this.scene.currentBattle.double) - this.scene.unshiftPhase(new SummonPhase(this.scene, 1, false)); - this.end(); - }, 1500); + const trainer = this.scene.currentBattle.trainer; + trainer.untint(100, 'Sine.easeOut'); + trainer.playAnim(); + + const doSummon = () => { + this.scene.currentBattle.started = true; + this.scene.playBgm(undefined); + this.scene.pbTray.showPbTray(this.scene.getParty()); + this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty()); + const text = `${this.scene.currentBattle.trainer.getName()}\nwould like to battle!`; + this.scene.ui.showText(text, null, () => { + this.scene.tweens.add({ + targets: this.scene.currentBattle.trainer, + x: '+=16', + y: '-=16', + alpha: 0, + ease: 'Sine.easeInOut', + duration: 750 + }); + this.scene.unshiftPhase(new SummonPhase(this.scene, 0, false)); + if (this.scene.currentBattle.double) + this.scene.unshiftPhase(new SummonPhase(this.scene, 1, false)); + this.end(); + }, 1500, true); + }; + + if (!trainer.config.encounterMessages.length) + doSummon(); + else { + let message: string; + this.scene.executeWithSeedOffset(() => message = Phaser.Math.RND.pick(this.scene.currentBattle.trainer.config.encounterMessages), this.scene.currentBattle.waveIndex); + const messagePages = message.split(/\$/g).map(m => m.trim()); + let showMessageAndSummon = () => doSummon(); + for (let p = messagePages.length - 1; p >= 0; p--) { + const originalFunc = showMessageAndSummon; + showMessageAndSummon = () => this.scene.ui.showDialogue(messagePages[p], trainer.getName(), null, originalFunc, null, true); + } + showMessageAndSummon(); + } } } @@ -368,7 +393,7 @@ export class NextEncounterPhase extends EncounterPhase { const enemyField = this.scene.getEnemyField(); this.scene.tweens.add({ - targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField ].flat(), + targets: [ this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer ].flat(), x: '+=300', duration: 2000, onComplete: () => { @@ -430,6 +455,8 @@ export class SelectBiomePhase extends BattlePhase { const currentBiome = this.scene.arena.biomeType; const setNextBiome = (nextBiome: Biome) => { + if (this.scene.gameMode === GameMode.CLASSIC) + this.scene.unshiftPhase(new PartyHealPhase(this.scene, false)); this.scene.unshiftPhase(new SwitchBiomePhase(this.scene, nextBiome)); this.end(); }; @@ -441,7 +468,7 @@ export class SelectBiomePhase extends BattlePhase { setNextBiome(Biome.END); else { const allBiomes = Utils.getEnumValues(Biome); - setNextBiome(allBiomes[Utils.randInt(allBiomes.length - 2, 1)]); + setNextBiome(allBiomes[Utils.randSeedInt(allBiomes.length - 2, 1)]); } } else if (Array.isArray(biomeLinks[currentBiome])) { const biomes = biomeLinks[currentBiome] as Biome[]; @@ -451,7 +478,7 @@ export class SelectBiomePhase extends BattlePhase { setNextBiome(biomes[biomeIndex]); }); } else - setNextBiome(biomes[Utils.randInt(biomes.length)]); + setNextBiome(biomes[Utils.randSeedInt(biomes.length)]); } else setNextBiome(biomeLinks[currentBiome] as Biome); } @@ -469,6 +496,9 @@ export class SwitchBiomePhase extends BattlePhase { start() { super.start(); + if (this.nextBiome === undefined) + return this.end(); + this.scene.tweens.add({ targets: this.scene.arenaEnemy, x: '+=300', @@ -536,7 +566,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (this.player) { this.scene.ui.showText(`Go! ${this.getPokemon().name}!`); - if (this.player) + if (this.player) + this.scene.pbTray.hide(); this.scene.trainer.play('trainer_m_pb'); this.scene.tweens.add({ targets: this.scene.trainer, @@ -544,8 +575,10 @@ export class SummonPhase extends PartyMemberPokemonPhase { duration: 1000 }); this.scene.time.delayedCall(750, () => this.summon()); - } else + } else { + this.scene.pbTrayEnemy.hide(); this.scene.ui.showText(`${this.scene.currentBattle.trainer.getName()} sent out\n${this.getPokemon().name}!`, null, () => this.summon()); + } } summon(): void { @@ -740,6 +773,25 @@ export class ReturnPhase extends SwitchSummonPhase { } } +export class ShowTrainerPhase extends BattlePhase { + constructor(scene: BattleScene) { + super(scene); + } + + start() { + super.start(); + + this.scene.trainer.setTexture('trainer_m'); + + this.scene.tweens.add({ + targets: this.scene.trainer, + x: 106, + duration: 1000, + onComplete: () => this.end() + }); + } +} + export class ToggleDoublePositionPhase extends BattlePhase { private double: boolean; @@ -796,7 +848,7 @@ export class CheckSwitchPhase extends BattlePhase { return; } - this.scene.ui.showText(`Will you switch\n${this.useName ? pokemon.name : 'POKéMON'}?`, null, () => { + this.scene.ui.showText(`Will you switch\n${this.useName ? pokemon.name : 'Pokémon'}?`, null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.MESSAGE); this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true)); @@ -931,7 +983,14 @@ export class CommandPhase extends FieldPhase { if (this.scene.arena.biomeType === Biome.END) { this.scene.ui.setMode(Mode.COMMAND); this.scene.ui.setMode(Mode.MESSAGE); - this.scene.ui.showText(`A strange force\nprevents using POKé BALLS.`, null, () => { + this.scene.ui.showText(`A strange force\nprevents using Poké Balls.`, null, () => { + this.scene.ui.showText(null, 0); + this.scene.ui.setMode(Mode.COMMAND); + }, null, true); + } else if (this.scene.currentBattle.battleType === BattleType.TRAINER) { + this.scene.ui.setMode(Mode.COMMAND); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(`You can't catch\nanother trainer's Pokémon!`, null, () => { this.scene.ui.showText(null, 0); this.scene.ui.setMode(Mode.COMMAND); }, null, true); @@ -949,20 +1008,33 @@ export class CommandPhase extends FieldPhase { case Command.POKEMON: case Command.RUN: const isSwitch = command === Command.POKEMON; - const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; - const trapped = new Utils.BooleanHolder(false); - const batonPass = isSwitch && args[0] as boolean; - if (!batonPass) - enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped)); - if (batonPass || (!trapTag && !trapped.value)) { - this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch - ? { command: Command.POKEMON, cursor: cursor, args: args } - : { command: Command.RUN }; - success = true; - } else if (trapTag) - this.scene.ui.showText(`${this.scene.getPokemonById(trapTag.sourceId).name}'s ${trapTag.getMoveName()}\nprevents ${isSwitch ? 'switching' : 'fleeing'}!`, null, () => { + if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) { + this.scene.ui.setMode(Mode.COMMAND); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(`You can't run\nfrom a trainer battle!`, null, () => { this.scene.ui.showText(null, 0); + this.scene.ui.setMode(Mode.COMMAND); }, null, true); + } else { + const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag; + const trapped = new Utils.BooleanHolder(false); + const batonPass = isSwitch && args[0] as boolean; + if (!batonPass) + enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped)); + if (batonPass || (!trapTag && !trapped.value)) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch + ? { command: Command.POKEMON, cursor: cursor, args: args } + : { command: Command.RUN }; + success = true; + } else if (trapTag) { + this.scene.ui.setMode(Mode.COMMAND); + this.scene.ui.setMode(Mode.MESSAGE); + this.scene.ui.showText(`${this.scene.getPokemonById(trapTag.sourceId).name}'s ${trapTag.getMoveName()}\nprevents ${isSwitch ? 'switching' : 'fleeing'}!`, null, () => { + this.scene.ui.showText(null, 0); + this.scene.ui.setMode(Mode.COMMAND); + }, null, true); + } + } break; } @@ -1786,6 +1858,12 @@ export class MessagePhase extends BattlePhase { start() { super.start(); + if (this.text.indexOf('$') > -1) { + const pageIndex = this.text.indexOf('$'); + this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay)); + this.text = this.text.slice(0, pageIndex).trim(); + } + this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay); } @@ -1972,6 +2050,8 @@ export class VictoryPhase extends PokemonPhase { if (!this.scene.getEnemyParty().filter(p => !p?.isFainted(true)).length) { this.scene.pushPhase(new BattleEndPhase(this.scene)); + if (this.scene.currentBattle.battleType === BattleType.TRAINER) + this.scene.pushPhase(new TrainerVictoryPhase(this.scene)); if (this.scene.gameMode === GameMode.ENDLESS || this.scene.currentBattle.waveIndex < this.scene.finalWave) { this.scene.pushPhase(new SelectModifierPhase(this.scene)); this.scene.pushPhase(new NewBattlePhase(this.scene)); @@ -1983,6 +2063,41 @@ export class VictoryPhase extends PokemonPhase { } } +export class TrainerVictoryPhase extends BattlePhase { + constructor(scene: BattleScene) { + super(scene); + } + + start() { + this.scene.playBgm('victory'); + + this.scene.ui.showText(`You defeated\n${this.scene.currentBattle.trainer.getName()}!`, null, () => { + const defeatMessages = this.scene.currentBattle.trainer.config.defeatMessages; + let showMessageAndEnd = () => this.end();//this.scene.ui.showText(`You got ₽0\nfor winning!`, null, () => this.end(), null, true); + if (defeatMessages.length) { + let message: string; + this.scene.executeWithSeedOffset(() => message = Phaser.Math.RND.pick(this.scene.currentBattle.trainer.config.defeatMessages), this.scene.currentBattle.waveIndex); + const messagePages = message.split(/\$/g).map(m => m.trim()); + + for (let p = messagePages.length - 1; p >= 0; p--) { + const originalFunc = showMessageAndEnd; + showMessageAndEnd = () => this.scene.ui.showDialogue(messagePages[p], this.scene.currentBattle.trainer.getName(), null, originalFunc, null, true); + } + } + showMessageAndEnd(); + }, null, true); + + this.scene.tweens.add({ + targets: this.scene.currentBattle.trainer, + x: '-=16', + y: '+=16', + alpha: 1, + ease: 'Sine.easeInOut', + duration: 750 + }); + } +} + export class GameOverPhase extends BattlePhase { private victory: boolean; @@ -2166,7 +2281,7 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { pokemon.calculateStats(); pokemon.updateInfo(); this.scene.playSoundWithoutBgm('level_up_fanfare', 1500); - this.scene.ui.showText(`${this.getPokemon().name} grew to\nLV. ${this.level}!`, null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false, () => this.end()), null, true); + this.scene.ui.showText(`${this.getPokemon().name} grew to\nLv. ${this.level}!`, null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false, () => this.end()), null, true); if (this.level <= 100) { const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1); for (let lm of levelMoves) @@ -2215,7 +2330,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { .then(() => { this.scene.ui.setMode(messageMode).then(() => { this.scene.playSoundWithoutBgm('level_up_fanfare', 1500); - this.scene.ui.showText(`${pokemon.name} learned\n${Utils.toPokemonUpperCase(move.name)}!`, null, () => this.end(), messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true); + this.scene.ui.showText(`${pokemon.name} learned\n${move.name}!`, null, () => this.end(), messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true); }); }); }); @@ -2506,7 +2621,7 @@ export class AttemptCapturePhase extends PokemonPhase { Promise.all([ pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon) ]).then(() => { if (this.scene.getParty().length === 6) { const promptRelease = () => { - this.scene.ui.showText(`Your party is full.\nRelease a POKéMON to make room for ${pokemon.name}?`, null, () => { + this.scene.ui.showText(`Your party is full.\nRelease a Pokémon to make room for ${pokemon.name}?`, null, () => { this.scene.ui.setMode(Mode.CONFIRM, () => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => { this.scene.ui.setMode(Mode.MESSAGE).then(() => { @@ -2591,6 +2706,8 @@ export class SelectModifierPhase extends BattlePhase { start() { super.start(); + this.scene.resetSeed(); + const party = this.scene.getParty(); regenerateModifierPoolThresholds(party); const modifierCount = new Utils.IntegerHolder(3); @@ -2658,6 +2775,40 @@ export class SelectModifierPhase extends BattlePhase { } } +export class PartyHealPhase extends BattlePhase { + private resumeBgm: boolean; + + constructor(scene: BattleScene, resumeBgm: boolean) { + super(scene); + + this.resumeBgm = resumeBgm; + } + + start() { + super.start(); + + const bgmPlaying = this.scene.isBgmPlaying(); + if (bgmPlaying) + this.scene.fadeOutBgm(1000, false); + this.scene.ui.fadeOut(1000).then(() => { + for (let pokemon of this.scene.getParty()) { + pokemon.hp = pokemon.getMaxHp(); + pokemon.resetStatus(); + for (let move of pokemon.moveset) + move.ppUsed = 0; + } + const healSong = this.scene.sound.add('heal'); + healSong.play(); + this.scene.time.delayedCall(healSong.totalDuration * 1000, () => { + healSong.destroy(); + if (this.resumeBgm && bgmPlaying) + this.scene.playBgm(); + this.scene.ui.fadeIn(500).then(() => this.end()); + }); + }); + } +} + export class ShinySparklePhase extends PokemonPhase { constructor(scene: BattleScene, battlerIndex: BattlerIndex) { super(scene, battlerIndex); @@ -2669,4 +2820,10 @@ export class ShinySparklePhase extends PokemonPhase { this.getPokemon().sparkle(); this.scene.time.delayedCall(1000, () => this.end()); } +} + +export class TestMessagePhase extends MessagePhase { + constructor(scene: BattleScene, message: string) { + super(scene, message, null, true); + } } \ No newline at end of file diff --git a/src/battle-scene.ts b/src/battle-scene.ts index fa2670e2ef5..7cfda5cd8cc 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1,7 +1,7 @@ import Phaser from 'phaser'; import { Biome } from './data/biome'; import UI from './ui/ui'; -import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, ToggleDoublePositionPhase, CheckSwitchPhase, LevelCapPhase } from './battle-phases'; +import { EncounterPhase, SummonPhase, NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, CheckLoadPhase, TurnInitPhase, ReturnPhase, ToggleDoublePositionPhase, CheckSwitchPhase, LevelCapPhase, TestMessagePhase, ShowTrainerPhase, PartyHealPhase } from './battle-phases'; import Pokemon, { PlayerPokemon, EnemyPokemon } from './pokemon'; import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies, initSpecies } from './data/pokemon-species'; import * as Utils from './utils'; @@ -19,7 +19,7 @@ import { Moves, initMoves } from './data/move'; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave } from './modifier/modifier-type'; import AbilityBar from './ui/ability-bar'; import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, applyAbAttrs, initAbilities } from './data/ability'; -import Battle, { BattleType } from './battle'; +import Battle, { BattleType, FixedBattleConfig, fixedBattles } from './battle'; import { GameMode } from './game-mode'; import SpritePipeline from './pipelines/sprite'; import PartyExpBar from './ui/party-exp-bar'; @@ -28,6 +28,7 @@ import Trainer from './trainer'; import TrainerData from './system/trainer-data'; import SoundFade from 'phaser3-rex-plugins/plugins/soundfade'; import { pokemonPrevolutions } from './data/pokemon-evolutions'; +import PokeballTray from './ui/pokeball-tray'; const enableAuto = true; const quickStart = false; @@ -70,6 +71,8 @@ export default class BattleScene extends Phaser.Scene { private currentPhase: BattlePhase; public field: Phaser.GameObjects.Container; public fieldUI: Phaser.GameObjects.Container; + public pbTray: PokeballTray; + public pbTrayEnemy: PokeballTray; public abilityBar: AbilityBar; public partyExpBar: PartyExpBar; public arenaBg: Phaser.GameObjects.Sprite; @@ -81,6 +84,7 @@ export default class BattleScene extends Phaser.Scene { public arena: Arena; public gameMode: GameMode; public trainer: Phaser.GameObjects.Sprite; + public lastEnemyTrainer: Trainer; public currentBattle: Battle; public pokeballCounts: PokeballCounts; private party: PlayerPokemon[]; @@ -92,6 +96,9 @@ export default class BattleScene extends Phaser.Scene { public uiContainer: Phaser.GameObjects.Container; public ui: UI; + public seed: string; + public waveSeed: string; + public spritePipeline: SpritePipeline; private bgm: Phaser.Sound.BaseSound; @@ -163,6 +170,7 @@ export default class BattleScene extends Phaser.Scene { this.loadAtlas('prompt', 'ui'); this.loadImage('cursor', 'ui'); this.loadImage('window', 'ui'); + this.loadImage('namebox', 'ui'); this.loadImage('pbinfo_player', 'ui'); this.loadImage('pbinfo_player_mini', 'ui'); this.loadImage('pbinfo_enemy_mini', 'ui'); @@ -176,6 +184,10 @@ export default class BattleScene extends Phaser.Scene { this.loadImage('party_exp_bar', 'ui'); this.loadImage('shiny_star', 'ui', 'shiny.png'); + this.loadImage('pb_tray_overlay_player', 'ui'); + this.loadImage('pb_tray_overlay_enemy', 'ui'); + this.loadAtlas('pb_tray_ball', 'ui'); + this.loadImage('party_bg', 'ui'); this.loadImage('party_bg_double', 'ui'); this.loadAtlas('party_slot_main', 'ui'); @@ -281,7 +293,6 @@ export default class BattleScene extends Phaser.Scene { this.loadSe('upgrade'); this.loadSe('error'); - this.loadSe('pb'); this.loadSe('pb_rel'); this.loadSe('pb_throw'); this.loadSe('pb_bounce_1'); @@ -290,9 +301,15 @@ export default class BattleScene extends Phaser.Scene { this.loadSe('pb_catch'); this.loadSe('pb_lock'); + this.loadSe('pb_tray_enter'); + this.loadSe('pb_tray_ball'); + this.loadSe('pb_tray_empty'); + this.loadBgm('menu'); this.loadBgm('level_up_fanfare', 'bw/level_up_fanfare.mp3'); + this.loadBgm('heal', 'bw/heal.mp3'); + this.loadBgm('victory', 'bw/victory.mp3'); this.loadBgm('evolution', 'bw/evolution.mp3'); this.loadBgm('evolution_fanfare', 'bw/evolution_fanfare.mp3'); @@ -300,6 +317,9 @@ export default class BattleScene extends Phaser.Scene { } create() { + this.seed = this.game.config.seed[0]; + console.log('Seed:', this.seed); + initGameSpeed.apply(this); this.setupControls(); @@ -345,6 +365,15 @@ export default class BattleScene extends Phaser.Scene { this.add.existing(this.enemyModifierBar); uiContainer.add(this.enemyModifierBar); + this.pbTray = new PokeballTray(this, true); + this.pbTray.setup(); + + this.pbTrayEnemy = new PokeballTray(this, false); + this.pbTrayEnemy.setup(); + + this.fieldUI.add(this.pbTray); + this.fieldUI.add(this.pbTrayEnemy); + this.abilityBar = new AbilityBar(this); this.abilityBar.setup(); this.fieldUI.add(this.abilityBar); @@ -541,38 +570,65 @@ export default class BattleScene extends Phaser.Scene { let newBattleType: BattleType; let newTrainer: Trainer; - if (battleType === undefined) - newBattleType = BattleType.WILD; - else - newBattleType = battleType; + let battleConfig: FixedBattleConfig = null; - if (newBattleType === BattleType.TRAINER) { - newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, TrainerType.SWIMMER, !!Utils.randInt(2)); - this.field.add(newTrainer); + this.resetSeed(newWaveIndex); + + if (fixedBattles.hasOwnProperty(newWaveIndex)) { + battleConfig = fixedBattles[newWaveIndex]; + newDouble = battleConfig.double; + newBattleType = battleConfig.battleType; + newTrainer = battleConfig.getTrainer(this); + if (newTrainer) + this.field.add(newTrainer); + } else { + if (battleType === undefined) { + const trainerChance = this.arena.getTrainerChance(); + newBattleType = trainerChance && !Utils.randSeedInt(trainerChance) ? BattleType.TRAINER : BattleType.WILD; + } else + newBattleType = battleType; + + if (newBattleType === BattleType.TRAINER) { + newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, this.arena.randomTrainerType(newWaveIndex), !!Utils.randSeedInt(2)); + this.field.add(newTrainer); + } } + const playerField = this.getPlayerField(); + if (double === undefined && newWaveIndex > 1) { if (newBattleType === BattleType.WILD) { const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8); this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance); - this.getPlayerField().forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance)); - newDouble = !Utils.randInt(doubleChance.value); + playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance)); + newDouble = !Utils.randSeedInt(doubleChance.value); } else if (newBattleType === BattleType.TRAINER) newDouble = newTrainer.config.isDouble; - } else + } else if (!battleConfig) newDouble = !!double; const lastBattle = this.currentBattle; const maxExpLevel = this.getMaxExpLevel(); + this.lastEnemyTrainer = lastBattle?.trainer ?? null; + this.currentBattle = new Battle(newWaveIndex, newBattleType, newTrainer, newDouble); this.currentBattle.incrementTurn(this); + //this.pushPhase(new TestMessagePhase(this, trainerConfigs[TrainerType.RIVAL].encounterMessages[0])) + if (!waveIndex) { + const isNewBiome = !lastBattle || !(lastBattle.waveIndex % 10); + const showTrainer = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER; + const availablePartyMemberCount = this.getParty().filter(p => !p.isFainted()).length; if (lastBattle) { this.getEnemyField().forEach(enemyPokemon => enemyPokemon.destroy()); - if (this.gameMode === GameMode.CLASSIC && lastBattle.waveIndex % 10) + if (showTrainer) { + playerField.forEach((_, p) => this.unshiftPhase(new ReturnPhase(this, p))); + this.unshiftPhase(new ShowTrainerPhase(this)); + } + if (this.gameMode === GameMode.CLASSIC && !isNewBiome) this.pushPhase(new NextEncounterPhase(this)); else { this.pushPhase(new SelectBiomePhase(this)); @@ -582,6 +638,11 @@ export default class BattleScene extends Phaser.Scene { if (newMaxExpLevel > maxExpLevel) this.pushPhase(new LevelCapPhase(this)); } + if (showTrainer) { + this.pushPhase(new SummonPhase(this, 0)); + if (this.currentBattle.double && availablePartyMemberCount > 1) + this.pushPhase(new SummonPhase(this, 1)); + } } else { if (!this.quickStart) this.pushPhase(new CheckLoadPhase(this)); @@ -591,8 +652,7 @@ export default class BattleScene extends Phaser.Scene { } } - if ((lastBattle?.double || false) !== newDouble) { - const availablePartyMemberCount = this.getParty().filter(p => !p.isFainted()).length; + if (!showTrainer && (lastBattle?.double || false) !== newDouble) { if (newDouble) { if (availablePartyMemberCount > 1) { this.pushPhase(new ToggleDoublePositionPhase(this, true)); @@ -605,7 +665,7 @@ export default class BattleScene extends Phaser.Scene { } } - if (lastBattle) { + if (lastBattle && this.currentBattle.battleType !== BattleType.TRAINER) { this.pushPhase(new CheckSwitchPhase(this, 0, newDouble)); if (newDouble) this.pushPhase(new CheckSwitchPhase(this, 1, newDouble)); @@ -632,6 +692,20 @@ export default class BattleScene extends Phaser.Scene { return this.arena; } + resetSeed(waveIndex?: integer): void { + this.waveSeed = Utils.shiftCharCodes(this.seed, waveIndex || this.currentBattle.waveIndex); + Phaser.Math.RND.sow([ this.waveSeed ]); + } + + executeWithSeedOffset(func: Function, offset: integer): void { + if (!func) + return; + const state = Phaser.Math.RND.state(); + Phaser.Math.RND.sow([ Utils.shiftCharCodes(this.seed, offset) ]); + func(); + Phaser.Math.RND.state(state); + } + updateWaveCountText(): void { const isBoss = !(this.currentBattle.waveIndex % 10); this.waveCountText.setText(this.currentBattle.waveIndex.toString()); @@ -659,7 +733,7 @@ export default class BattleScene extends Phaser.Scene { s = getPokemonSpecies(pokemonPrevolutions[s.speciesId]); return s; }))] : allSpecies.slice(0, -1); - return getPokemonSpecies(filteredSpecies[Utils.randInt(filteredSpecies.length)].getSpeciesForLevel(level, true)); + return getPokemonSpecies(filteredSpecies[Utils.randSeedInt(filteredSpecies.length)].getSpeciesForLevel(level, true)); } checkInput(): boolean { @@ -721,11 +795,15 @@ export default class BattleScene extends Phaser.Scene { return this.buttonKeys[button].filter(k => k.isDown).length >= 1; } + isBgmPlaying(): boolean { + return this.bgm && this.bgm.isPlaying; + } + playBgm(bgmName?: string, fadeOut?: boolean): void { if (bgmName === undefined) bgmName = this.currentBattle.getBgmOverride() || this.arena.bgm; if (this.bgm && bgmName === this.bgm.key) { - if (!this.bgm.isPlaying || this.bgm.pendingRemove) { + if (!this.bgm.isPlaying) { this.bgm.play({ volume: 1 }); @@ -771,7 +849,7 @@ export default class BattleScene extends Phaser.Scene { } pauseBgm(): void { - if (this.bgm) + if (this.bgm && this.bgm.isPlaying) this.bgm.pause(); } diff --git a/src/battle.ts b/src/battle.ts index 430aab9533f..b42233c50dd 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -5,7 +5,7 @@ import * as Utils from "./utils"; import Trainer from "./trainer"; import { Species } from "./data/species"; import { Moves } from "./data/move"; -import { TrainerType } from "./data/trainer-type"; +import { TrainerConfig, TrainerType } from "./data/trainer-type"; export enum BattleType { WILD, @@ -38,6 +38,7 @@ export default class Battle { public enemyLevels: integer[]; public enemyParty: EnemyPokemon[]; public double: boolean; + public started: boolean; public turn: integer; public turnCommands: TurnCommands; public turnPokeballCounts: PokeballCounts; @@ -49,10 +50,13 @@ export default class Battle { this.waveIndex = waveIndex; this.battleType = battleType; this.trainer = trainer; - this.enemyLevels = new Array(battleType !== BattleType.TRAINER ? double ? 2 : 1 : trainer.config.genPartySize()).fill(null).map(() => this.getLevelForWave()); + this.enemyLevels = battleType !== BattleType.TRAINER + ? new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave()) + : trainer.getPartyLevels(this.waveIndex); this.enemyParty = []; this.double = double; this.turn = 0; + this.started = false; } private getLevelForWave(): integer { @@ -89,23 +93,85 @@ export default class Battle { getBgmOverride(): string { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); + if (this.battleType === BattleType.TRAINER) { + if (!this.started && this.trainer.config.encounterMessages.length) + return `encounter_${this.trainer.getEncounterBgm()}`; + return this.trainer.getBattleBgm(); + } for (let pokemon of battlers) { - if (this.battleType === BattleType.TRAINER) { - if (this.trainer.config.trainerType === TrainerType.RIVAL) - return 'battle_rival'; - return 'battle_trainer'; - } if (pokemon.species.speciesId === Species.ETERNATUS) return 'battle_final'; - if (pokemon.species.legendary) { - if (pokemon.species.speciesId === Species.RESHIRAM || pokemon.species.speciesId === Species.ZEKROM) - return 'battle_legendary_rz'; + if (pokemon.species.legendary || pokemon.species.pseudoLegendary || pokemon.species.mythical) { if (pokemon.species.speciesId === Species.KYUREM) return 'battle_legendary_z'; + if (pokemon.species.legendary) + return 'battle_legendary_rz'; return 'battle_legendary'; } } + if (this.waveIndex <= 3) + return 'battle_wild'; + return null; } +} + +export class FixedBattle extends Battle { + constructor(scene: BattleScene, waveIndex: integer, config: FixedBattleConfig) { + super(waveIndex, config.battleType, config.battleType === BattleType.TRAINER ? config.getTrainer(scene) : null, config.double); + if (config.getEnemyParty) + this.enemyParty = config.getEnemyParty(scene); + } +} + +type GetTrainerFunc = (scene: BattleScene) => Trainer; +type GetEnemyPartyFunc = (scene: BattleScene) => EnemyPokemon[]; + +export class FixedBattleConfig { + public battleType: BattleType; + public double: boolean; + public getTrainer: GetTrainerFunc; + public getEnemyParty: GetEnemyPartyFunc; + + setBattleType(battleType: BattleType): FixedBattleConfig { + this.battleType = battleType; + return this; + } + + setDouble(double: boolean): FixedBattleConfig { + this.double = double; + return this; + } + + setGetTrainerFunc(getTrainerFunc: GetTrainerFunc): FixedBattleConfig { + this.getTrainer = getTrainerFunc; + return this; + } + + setGetEnemyPartyFunc(getEnemyPartyFunc: GetEnemyPartyFunc): FixedBattleConfig { + this.getEnemyParty = getEnemyPartyFunc; + return this; + } +} + +interface FixedBattleConfigs { + [key: integer]: FixedBattleConfig +} + +export const fixedBattles: FixedBattleConfigs = { + [4]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.YOUNGSTER, !!Utils.randInt(2))), + [5]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL, true)), + [25]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_2, true)), + [55]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_3, true)), + [95]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_4, true)), + [145]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_5, true)), + [199]: new FixedBattleConfig().setBattleType(BattleType.TRAINER) + .setGetTrainerFunc(scene => new Trainer(scene, TrainerType.RIVAL_6, true)) } \ No newline at end of file diff --git a/src/data/ability.ts b/src/data/ability.ts index 53f02ae7180..be9df8af1b7 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1,13 +1,13 @@ -import Pokemon, { HitResult, MoveResult, PokemonMove } from "../pokemon"; +import Pokemon, { HitResult, PokemonMove } from "../pokemon"; import { Type } from "./type"; import * as Utils from "../utils"; import { BattleStat, getBattleStatName } from "./battle-stat"; import { DamagePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../battle-phases"; import { getPokemonMessage } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, BattlerTagType, TrappedTag } from "./battler-tag"; +import { BattlerTag, BattlerTagType } from "./battler-tag"; import { StatusEffect, getStatusEffectDescriptor } from "./status-effect"; -import { MoveFlags, Moves, RecoilAttr, allMoves } from "./move"; +import { MoveFlags, Moves, RecoilAttr } from "./move"; import { ArenaTagType } from "./arena-tag"; export class Ability { @@ -20,7 +20,7 @@ export class Ability { constructor(id: Abilities, name: string, description: string, generation: integer) { this.id = id; - this.name = name.toUpperCase(); + this.name = name; this.description = description; this.generation = generation; this.attrs = []; @@ -265,7 +265,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr { } getTriggerMessage(pokemon: Pokemon, ...args: any[]): string { - return getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nmade it the ${Type[pokemon.getTypes()[0]]} type!`); + return getPokemonMessage(pokemon, `'s ${pokemon.getAbility().name}\nmade it the ${Utils.toReadableString(Type[pokemon.getTypes()[0]])} type!`); } } @@ -1196,142 +1196,142 @@ export function initAbilities() { .attr(SuppressWeatherEffectAbAttr, true), new Ability(Abilities.ARENA_TRAP, "Arena Trap", "Prevents the foe from fleeing.", 3) .attr(ArenaTrapAbAttr), - new Ability(Abilities.BATTLE_ARMOR, "Battle Armor", "The POKéMON is protected against critical hits.", 3) + new Ability(Abilities.BATTLE_ARMOR, "Battle Armor", "The Pokémon is protected against critical hits.", 3) .attr(BlockCritAbAttr), - new Ability(Abilities.BLAZE, "Blaze", "Powers up FIRE-type moves in a pinch.", 3) + new Ability(Abilities.BLAZE, "Blaze", "Powers up Fire-type moves in a pinch.", 3) .attr(LowHpMoveTypePowerBoostAbAttr, Type.FIRE), - new Ability(Abilities.CHLOROPHYLL, "Chlorophyll", "Boosts the POKéMON's SPEED in sunshine.", 3) + new Ability(Abilities.CHLOROPHYLL, "Chlorophyll", "Boosts the Pokémon's Speed in sunshine.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), // TODO: Show ability bar on weather change and summon - new Ability(Abilities.CLEAR_BODY, "Clear Body", "Prevents other POKéMON from lowering its stats.", 3) + new Ability(Abilities.CLEAR_BODY, "Clear Body", "Prevents other Pokémon from lowering its stats.", 3) .attr(ProtectStatAbAttr), new Ability(Abilities.CLOUD_NINE, "Cloud Nine", "Eliminates the effects of non-severe weather.", 3) .attr(SuppressWeatherEffectAbAttr), - new Ability(Abilities.COLOR_CHANGE, "Color Change", "Changes the POKéMON's type to the foe's move.", 3) + new Ability(Abilities.COLOR_CHANGE, "Color Change", "Changes the Pokémon's type to the foe's move.", 3) .attr(PostDefendTypeChangeAbAttr), - new Ability(Abilities.COMPOUND_EYES, "Compound Eyes", "The POKéMON's accuracy is boosted.", 3) + new Ability(Abilities.COMPOUND_EYES, "Compound Eyes", "The Pokémon's Accuracy is boosted.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.ACC, 1.3), - new Ability(Abilities.CUTE_CHARM, "Cute Charm", "Contact with the POKéMON may cause infatuation.", 3) + new Ability(Abilities.CUTE_CHARM, "Cute Charm", "Contact with the Pokémon may cause infatuation.", 3) .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), new Ability(Abilities.DAMP, "Damp (N)", "Prevents the use of self-destructing moves.", 3), - new Ability(Abilities.DRIZZLE, "Drizzle", "The POKéMON makes it rain when it enters a battle.", 3) + new Ability(Abilities.DRIZZLE, "Drizzle", "The Pokémon makes it rain when it enters a battle.", 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.RAIN), - new Ability(Abilities.DROUGHT, "Drought", "Turns the sunlight harsh when the POKéMON enters a battle.", 3) + new Ability(Abilities.DROUGHT, "Drought", "Turns the sunlight harsh when the Pokémon enters a battle.", 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY), - new Ability(Abilities.EARLY_BIRD, "Early Bird (N)", "The POKéMON awakens quickly from sleep.", 3), + new Ability(Abilities.EARLY_BIRD, "Early Bird (N)", "The Pokémon awakens quickly from sleep.", 3), new Ability(Abilities.EFFECT_SPORE, "Effect Spore", "Contact may poison or cause paralysis or sleep.", 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 10, StatusEffect.POISON, StatusEffect.PARALYSIS, StatusEffect.SLEEP), - new Ability(Abilities.FLAME_BODY, "Flame Body", "Contact with the POKéMON may burn the attacker.", 3) + new Ability(Abilities.FLAME_BODY, "Flame Body", "Contact with the Pokémon may burn the attacker.", 3) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.BURN), - new Ability(Abilities.FLASH_FIRE, "Flash Fire", "It powers up FIRE-type moves if it's hit by one.", 3) + new Ability(Abilities.FLASH_FIRE, "Flash Fire", "It powers up Fire-type moves if it's hit by one.", 3) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1, (pokemon: Pokemon) => !pokemon.status || pokemon.status.effect !== StatusEffect.FREEZE), new Ability(Abilities.FORECAST, "Forecast (N)", "Castform transforms with the weather.", 3), - new Ability(Abilities.GUTS, "Guts (N)", "Boosts ATTACK if there is a status problem.", 3), - new Ability(Abilities.HUGE_POWER, "Huge Power", "Raises the POKéMON's ATTACK stat.", 3) + new Ability(Abilities.GUTS, "Guts (N)", "Boosts Attack if there is a status problem.", 3), + new Ability(Abilities.HUGE_POWER, "Huge Power", "Raises the Pokémon's Attack stat.", 3) .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, 1, true), - new Ability(Abilities.HUSTLE, "Hustle (N)", "Boosts the ATTACK stat, but lowers accuracy.", 3), - new Ability(Abilities.HYPER_CUTTER, "Hyper Cutter", "Prevents other POKéMON from lowering ATTACK stat.", 3) + new Ability(Abilities.HUSTLE, "Hustle (N)", "Boosts the Attack stat, but lowers Accuracy.", 3), + new Ability(Abilities.HYPER_CUTTER, "Hyper Cutter", "Prevents other Pokémon from lowering Attack stat.", 3) .attr(ProtectStatAbAttr, BattleStat.ATK), new Ability(Abilities.ILLUMINATE, "Illuminate", "Raises the likelihood of an encounter being a double battle.", 3) .attr(DoubleBattleChanceAbAttr), - new Ability(Abilities.IMMUNITY, "Immunity", "Prevents the POKéMON from getting poisoned.", 3) + new Ability(Abilities.IMMUNITY, "Immunity", "Prevents the Pokémon from getting poisoned.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.POISON), - new Ability(Abilities.INNER_FOCUS, "Inner Focus", "The POKéMON is protected from flinching.", 3) + new Ability(Abilities.INNER_FOCUS, "Inner Focus", "The Pokémon is protected from flinching.", 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.FLINCHED), - new Ability(Abilities.INSOMNIA, "Insomnia", "Prevents the POKéMON from falling asleep.", 3) + new Ability(Abilities.INSOMNIA, "Insomnia", "Prevents the Pokémon from falling asleep.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY), - new Ability(Abilities.INTIMIDATE, "Intimidate", "Lowers the foe's ATTACK stat.", 3) + new Ability(Abilities.INTIMIDATE, "Intimidate", "Lowers the foe's Attack stat.", 3) .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, -1), - new Ability(Abilities.KEEN_EYE, "Keen Eye", "Prevents other POKéMON from lowering accuracy.", 3) + new Ability(Abilities.KEEN_EYE, "Keen Eye", "Prevents other Pokémon from lowering Accuracy.", 3) .attr(ProtectStatAbAttr, BattleStat.ACC), - new Ability(Abilities.LEVITATE, "Levitate", "Gives immunity to GROUND-type moves.", 3) + new Ability(Abilities.LEVITATE, "Levitate", "Gives immunity to Ground-type moves.", 3) .attr(TypeImmunityAbAttr, Type.GROUND, (pokemon: Pokemon) => !pokemon.getTag(BattlerTagType.IGNORE_FLYING) && !pokemon.scene.arena.getTag(ArenaTagType.GRAVITY)), - new Ability(Abilities.LIGHTNING_ROD, "Lightning Rod", "Draws in all ELECTRIC-type moves to up SP. ATK.", 3) + new Ability(Abilities.LIGHTNING_ROD, "Lightning Rod", "Draws in all Electric-type moves to up Sp. Atk.", 3) .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPATK, 1), - new Ability(Abilities.LIMBER, "Limber", "The POKéMON is protected from paralysis.", 3) + new Ability(Abilities.LIMBER, "Limber", "The Pokémon is protected from paralysis.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS), new Ability(Abilities.LIQUID_OOZE, "Liquid Ooze (N)", "Damages attackers using any draining move.", 3), - new Ability(Abilities.MAGMA_ARMOR, "Magma Armor", "Prevents the POKéMON from becoming frozen.", 3) + new Ability(Abilities.MAGMA_ARMOR, "Magma Armor", "Prevents the Pokémon from becoming frozen.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.FREEZE), - new Ability(Abilities.MAGNET_PULL, "Magnet Pull", "Prevents STEEL-type POKéMON from escaping.", 3) + new Ability(Abilities.MAGNET_PULL, "Magnet Pull", "Prevents Steel-type Pokémon from escaping.", 3) /*.attr(ArenaTrapAbAttr) .condition((pokemon: Pokemon) => pokemon.getOpponent()?.isOfType(Type.STEEL))*/, // TODO: Rework - new Ability(Abilities.MARVEL_SCALE, "Marvel Scale (N)", "Ups DEFENSE if there is a status problem.", 3), - new Ability(Abilities.MINUS, "Minus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3), + new Ability(Abilities.MARVEL_SCALE, "Marvel Scale (N)", "Ups Defense if there is a status problem.", 3), + new Ability(Abilities.MINUS, "Minus (N)", "Ups Sp. Atk if another Pokémon has Plus or Minus.", 3), new Ability(Abilities.NATURAL_CURE, "Natural Cure (N)", "All status problems heal when it switches out.", 3), new Ability(Abilities.OBLIVIOUS, "Oblivious", "Prevents it from becoming infatuated.", 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.INFATUATED), - new Ability(Abilities.OVERGROW, "Overgrow", "Powers up GRASS-type moves in a pinch.", 3) + new Ability(Abilities.OVERGROW, "Overgrow", "Powers up Grass-type moves in a pinch.", 3) .attr(LowHpMoveTypePowerBoostAbAttr, Type.GRASS), - new Ability(Abilities.OWN_TEMPO, "Own Tempo", "Prevents the POKéMON from becoming confused.", 3) + new Ability(Abilities.OWN_TEMPO, "Own Tempo", "Prevents the Pokémon from becoming confused.", 3) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED), - new Ability(Abilities.PICKUP, "Pickup (N)", "The POKéMON may pick up items.", 3), - new Ability(Abilities.PLUS, "Plus (N)", "Ups SP. ATK if another POKéMON has PLUS or MINUS.", 3), - new Ability(Abilities.POISON_POINT, "Poison Point", "Contact with the POKéMON may poison the attacker.", 3) + new Ability(Abilities.PICKUP, "Pickup (N)", "The Pokémon may pick up items.", 3), + new Ability(Abilities.PLUS, "Plus (N)", "Ups Sp. Atk if another Pokémon has PLUS or MINUS.", 3), + new Ability(Abilities.POISON_POINT, "Poison Point", "Contact with the Pokémon may poison the attacker.", 3) .attr(PostDefendContactApplyStatusEffectAbAttr, StatusEffect.POISON), - new Ability(Abilities.PRESSURE, "Pressure (N)", "The POKéMON raises the foe's PP usage.", 3), - new Ability(Abilities.PURE_POWER, "Pure Power", "Raises the POKéMON's ATTACK stat.", 3) + new Ability(Abilities.PRESSURE, "Pressure (N)", "The Pokémon raises the foe's PP usage.", 3), + new Ability(Abilities.PURE_POWER, "Pure Power", "Raises the Pokémon's Attack stat.", 3) .attr(PostSummonStatChangeAbAttr, BattleStat.ATK, 1, true), - new Ability(Abilities.RAIN_DISH, "Rain Dish", "The POKéMON gradually regains HP in rain.", 3) + new Ability(Abilities.RAIN_DISH, "Rain Dish", "The Pokémon gradually regains HP in rain.", 3) .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.RAIN, WeatherType.HEAVY_RAIN), - new Ability(Abilities.ROCK_HEAD, "Rock Head", "Protects the POKéMON from recoil damage.", 3) + new Ability(Abilities.ROCK_HEAD, "Rock Head", "Protects the Pokémon from recoil damage.", 3) .attr(BlockRecoilDamageAttr), new Ability(Abilities.ROUGH_SKIN, "Rough Skin (N)", "Inflicts damage to the attacker on contact.", 3), - new Ability(Abilities.RUN_AWAY, "Run Away (N)", "Enables a sure getaway from wild POKéMON.", 3), - new Ability(Abilities.SAND_STREAM, "Sand Stream", "The POKéMON summons a sandstorm in battle.", 3) + new Ability(Abilities.RUN_AWAY, "Run Away (N)", "Enables a sure getaway from wild Pokémon.", 3), + new Ability(Abilities.SAND_STREAM, "Sand Stream", "The Pokémon summons a sandstorm in battle.", 3) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SANDSTORM), - new Ability(Abilities.SAND_VEIL, "Sand Veil", "Boosts the POKéMON's evasion in a sandstorm.", 3) + new Ability(Abilities.SAND_VEIL, "Sand Veil", "Boosts the Pokémon's Evasiveness in a sandstorm.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.SANDSTORM) .condition(getWeatherCondition(WeatherType.SANDSTORM)), new Ability(Abilities.SERENE_GRACE, "Serene Grace (N)", "Boosts the likelihood of added effects appearing.", 3), new Ability(Abilities.SHADOW_TAG, "Shadow Tag", "Prevents the foe from escaping.", 3) .attr(ArenaTrapAbAttr), - new Ability(Abilities.SHED_SKIN, "Shed Skin (N)", "The POKéMON may heal its own status problems.", 3), - new Ability(Abilities.SHELL_ARMOR, "Shell Armor", "The POKéMON is protected against critical hits.", 3) + new Ability(Abilities.SHED_SKIN, "Shed Skin (N)", "The Pokémon may heal its own status problems.", 3), + new Ability(Abilities.SHELL_ARMOR, "Shell Armor", "The Pokémon is protected against critical hits.", 3) .attr(BlockCritAbAttr), new Ability(Abilities.SHIELD_DUST, "Shield Dust (N)", "Blocks the added effects of attacks taken.", 3), new Ability(Abilities.SOUNDPROOF, "Soundproof (N)", "Gives immunity to sound-based moves.", 3), - new Ability(Abilities.SPEED_BOOST, "Speed Boost", "Its SPEED stat is gradually boosted.", 3) + new Ability(Abilities.SPEED_BOOST, "Speed Boost", "Its Speed stat is gradually boosted.", 3) .attr(PostTurnSpeedBoostAbAttr), - new Ability(Abilities.STATIC, "Static", "Contact with the POKéMON may cause paralysis.", 3) + new Ability(Abilities.STATIC, "Static", "Contact with the Pokémon may cause paralysis.", 3) .attr(PostDefendContactApplyStatusEffectAbAttr, StatusEffect.PARALYSIS), new Ability(Abilities.STENCH, "Stench (N)", "The stench may cause the target to flinch.", 3), - new Ability(Abilities.STICKY_HOLD, "Sticky Hold", "Protects the POKéMON from item theft.", 3) + new Ability(Abilities.STICKY_HOLD, "Sticky Hold", "Protects the Pokémon from item theft.", 3) .attr(BlockItemTheftAbAttr), new Ability(Abilities.STURDY, "Sturdy (N)", "It cannot be knocked out with one hit.", 3), new Ability(Abilities.SUCTION_CUPS, "Suction Cups (N)", "Negates all moves that force switching out.", 3), - new Ability(Abilities.SWARM, "Swarm", "Powers up BUG-type moves in a pinch.", 3) + new Ability(Abilities.SWARM, "Swarm", "Powers up Bug-type moves in a pinch.", 3) .attr(LowHpMoveTypePowerBoostAbAttr, Type.BUG), - new Ability(Abilities.SWIFT_SWIM, "Swift Swim", "Boosts the POKéMON's SPEED in rain.", 3) + new Ability(Abilities.SWIFT_SWIM, "Swift Swim", "Boosts the Pokémon's Speed in rain.", 3) .attr(BattleStatMultiplierAbAttr, BattleStat.SPD, 2) .condition(getWeatherCondition(WeatherType.RAIN, WeatherType.HEAVY_RAIN)), // TODO: Show ability bar on weather change and summon new Ability(Abilities.SYNCHRONIZE, "Synchronize (N)", "Passes a burn, poison, or paralysis to the foe.", 3), - new Ability(Abilities.THICK_FAT, "Thick Fat", "Ups resistance to Fire- and ICE-type moves.", 3) + new Ability(Abilities.THICK_FAT, "Thick Fat", "Ups resistance to Fire-type and Ice-type moves.", 3) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.ICE, 0.5), - new Ability(Abilities.TORRENT, "Torrent", "Powers up WATER-type moves in a pinch.", 3) + new Ability(Abilities.TORRENT, "Torrent", "Powers up Water-type moves in a pinch.", 3) .attr(LowHpMoveTypePowerBoostAbAttr, Type.WATER), - new Ability(Abilities.TRACE, "Trace (N)", "The POKéMON copies a foe's Ability.", 3), - new Ability(Abilities.TRUANT, "Truant", "POKéMON can't attack on consecutive turns.", 3) + new Ability(Abilities.TRACE, "Trace (N)", "The Pokémon copies a foe's Ability.", 3), + new Ability(Abilities.TRUANT, "Truant", "Pokémon can't attack on consecutive turns.", 3) .attr(PostSummonAddBattlerTagAbAttr, BattlerTagType.TRUANT, 1), - new Ability(Abilities.VITAL_SPIRIT, "Vital Spirit", "Prevents the POKéMON from falling asleep.", 3) + new Ability(Abilities.VITAL_SPIRIT, "Vital Spirit", "Prevents the Pokémon from falling asleep.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.SLEEP) .attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY), - new Ability(Abilities.VOLT_ABSORB, "Volt Absorb", "Restores HP if hit by an ELECTRIC-type move.", 3) + new Ability(Abilities.VOLT_ABSORB, "Volt Absorb", "Restores HP if hit by an Electric-type move.", 3) .attr(TypeImmunityHealAbAttr, Type.ELECTRIC), - new Ability(Abilities.WATER_ABSORB, "Water Absorb", "Restores HP if hit by a WATER-type move.", 3) + new Ability(Abilities.WATER_ABSORB, "Water Absorb", "Restores HP if hit by a Water-type move.", 3) .attr(TypeImmunityHealAbAttr, Type.WATER), - new Ability(Abilities.WATER_VEIL, "Water Veil", "Prevents the POKéMON from getting a burn.", 3) + new Ability(Abilities.WATER_VEIL, "Water Veil", "Prevents the Pokémon from getting a burn.", 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.BURN), - new Ability(Abilities.WHITE_SMOKE, "White Smoke", "Prevents other POKéMON from lowering its stats.", 3) + new Ability(Abilities.WHITE_SMOKE, "White Smoke", "Prevents other Pokémon from lowering its stats.", 3) .attr(ProtectStatAbAttr), new Ability(Abilities.WONDER_GUARD, "Wonder Guard", "Only super effective moves will hit.", 3) .attr(NonSuperEffectiveImmunityAbAttr), new Ability(Abilities.ADAPTABILITY, "Adaptability (N)", "Powers up moves of the same type.", 4), new Ability(Abilities.AFTERMATH, "Aftermath (N)", "Damages the attacker landing the finishing hit.", 4), - new Ability(Abilities.ANGER_POINT, "Anger Point (N)", "Maxes ATTACK after taking a critical hit.", 4), + new Ability(Abilities.ANGER_POINT, "Anger Point (N)", "Maxes Attack after taking a critical hit.", 4), new Ability(Abilities.ANTICIPATION, "Anticipation (N)", "Senses a foe's dangerous moves.", 4), new Ability(Abilities.BAD_DREAMS, "Bad Dreams (N)", "Reduces a sleeping foe's HP.", 4), new Ability(Abilities.DOWNLOAD, "Download (N)", "Adjusts power according to a foe's defenses.", 4), @@ -1341,105 +1341,105 @@ export function initAbilities() { .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 1.25) .attr(TypeImmunityHealAbAttr, Type.WATER), new Ability(Abilities.FILTER, "Filter (N)", "Reduces damage from super-effective attacks.", 4), - new Ability(Abilities.FLOWER_GIFT, "Flower Gift (N)", "Powers up party POKéMON when it is sunny.", 4), + new Ability(Abilities.FLOWER_GIFT, "Flower Gift (N)", "Powers up party Pokémon when it is sunny.", 4), new Ability(Abilities.FOREWARN, "Forewarn (N)", "Determines what moves a foe has.", 4), - new Ability(Abilities.FRISK, "Frisk (N)", "The POKéMON can check a foe's held item.", 4), + new Ability(Abilities.FRISK, "Frisk (N)", "The Pokémon can check a foe's held item.", 4), new Ability(Abilities.GLUTTONY, "Gluttony (N)", "Encourages the early use of a held Berry.", 4), - new Ability(Abilities.HEATPROOF, "Heatproof", "Weakens the power of FIRE-type moves.", 4) + new Ability(Abilities.HEATPROOF, "Heatproof", "Weakens the power of Fire-type moves.", 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5), - new Ability(Abilities.HONEY_GATHER, "Honey Gather (N)", "The POKéMON may gather Honey from somewhere.", 4), + new Ability(Abilities.HONEY_GATHER, "Honey Gather (N)", "The Pokémon may gather Honey from somewhere.", 4), new Ability(Abilities.HYDRATION, "Hydration (N)", "Heals status problems if it is raining.", 4), - new Ability(Abilities.ICE_BODY, "Ice Body", "The POKéMON gradually regains HP in a hailstorm.", 4) + new Ability(Abilities.ICE_BODY, "Ice Body", "The Pokémon gradually regains HP in a hailstorm.", 4) .attr(PostWeatherLapseHealAbAttr, 1, WeatherType.HAIL), new Ability(Abilities.IRON_FIST, "Iron Fist (N)", "Boosts the power of punching moves.", 4), - new Ability(Abilities.KLUTZ, "Klutz (N)", "The POKéMON can't use any held items.", 4), + new Ability(Abilities.KLUTZ, "Klutz (N)", "The Pokémon can't use any held items.", 4), new Ability(Abilities.LEAF_GUARD, "Leaf Guard", "Prevents problems with status in sunny weather.", 4) .attr(StatusEffectImmunityAbAttr) .condition(getWeatherCondition(WeatherType.SUNNY, WeatherType.HARSH_SUN)), - new Ability(Abilities.MAGIC_GUARD, "Magic Guard (N)", "Protects the POKéMON from indirect damage.", 4), + new Ability(Abilities.MAGIC_GUARD, "Magic Guard (N)", "Protects the Pokémon from indirect damage.", 4), new Ability(Abilities.MOLD_BREAKER, "Mold Breaker (N)", "Moves can be used regardless of Abilities.", 4), - new Ability(Abilities.MOTOR_DRIVE, "Motor Drive", "Raises SPEED if hit by an ELECTRIC-type move.", 4) + new Ability(Abilities.MOTOR_DRIVE, "Motor Drive", "Raises Speed if hit by an Electric-type move.", 4) .attr(TypeImmunityStatChangeAbAttr, Type.ELECTRIC, BattleStat.SPD, 1), new Ability(Abilities.MULTITYPE, "Multitype (N)", "Changes type to match the held Plate.", 4), - new Ability(Abilities.NO_GUARD, "No Guard (N)", "Ensures attacks by or against the POKéMON land.", 4), - new Ability(Abilities.NORMALIZE, "Normalize (N)", "All the POKéMON's moves become the NORMAL type.", 4), - new Ability(Abilities.POISON_HEAL, "Poison Heal (N)", "Restores HP if the POKéMON is poisoned.", 4), - new Ability(Abilities.QUICK_FEET, "Quick Feet (N)", "Boosts SPEED if there is a status problem.", 4), + new Ability(Abilities.NO_GUARD, "No Guard (N)", "Ensures attacks by or against the Pokémon land.", 4), + new Ability(Abilities.NORMALIZE, "Normalize (N)", "All the Pokémon's moves become Normal-type.", 4), + new Ability(Abilities.POISON_HEAL, "Poison Heal (N)", "Restores HP if the Pokémon is poisoned.", 4), + new Ability(Abilities.QUICK_FEET, "Quick Feet (N)", "Boosts Speed if there is a status problem.", 4), new Ability(Abilities.RECKLESS, "Reckless", "Powers up moves that have recoil damage.", 4) .attr(RecoilMovePowerBoostAbAttr), - new Ability(Abilities.RIVALRY, "Rivalry (N)", "Deals more damage to a POKéMON of same gender.", 4), - new Ability(Abilities.SCRAPPY, "Scrappy (N)", "Enables moves to hit GHOST-type POKéMON.", 4), + new Ability(Abilities.RIVALRY, "Rivalry (N)", "Deals more damage to a Pokémon of same gender.", 4), + new Ability(Abilities.SCRAPPY, "Scrappy (N)", "Enables moves to hit Ghost-type Pokémon.", 4), new Ability(Abilities.SIMPLE, "Simple (N)", "Doubles all stat changes.", 4), new Ability(Abilities.SKILL_LINK, "Skill Link (N)", "Increases the frequency of multi-strike moves.", 4), - new Ability(Abilities.SLOW_START, "Slow Start (N)", "Temporarily halves ATTACK and SPEED.", 4), + new Ability(Abilities.SLOW_START, "Slow Start (N)", "Temporarily halves Attack and Speed.", 4), new Ability(Abilities.SNIPER, "Sniper (N)", "Powers up moves if they become critical hits.", 4), - new Ability(Abilities.SNOW_CLOAK, "Snow Cloak", "Raises evasion in a hailstorm.", 4) + new Ability(Abilities.SNOW_CLOAK, "Snow Cloak", "Raises Evasiveness in a hailstorm.", 4) .attr(BattleStatMultiplierAbAttr, BattleStat.EVA, 1.2) .attr(BlockWeatherDamageAttr, WeatherType.HAIL), - new Ability(Abilities.SNOW_WARNING, "Snow Warning", "The POKéMON summons a hailstorm in battle.", 4) + new Ability(Abilities.SNOW_WARNING, "Snow Warning", "The Pokémon summons a hailstorm in battle.", 4) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HAIL), - new Ability(Abilities.SOLAR_POWER, "Solar Power (N)", "In sunshine, SP. ATK is boosted but HP decreases.", 4), + new Ability(Abilities.SOLAR_POWER, "Solar Power (N)", "In sunshine, Sp. Atk is boosted but HP decreases.", 4), new Ability(Abilities.SOLID_ROCK, "Solid Rock (N)", "Reduces damage from super-effective attacks.", 4), - new Ability(Abilities.STALL, "Stall (N)", "The POKéMON moves after all other POKéMON do.", 4), - new Ability(Abilities.STEADFAST, "Steadfast (N)", "Raises SPEED each time the POKéMON flinches.", 4), - new Ability(Abilities.STORM_DRAIN, "Storm Drain", "Draws in all WATER-type moves to up SP. ATK.", 4) + new Ability(Abilities.STALL, "Stall (N)", "The Pokémon moves after all other Pokémon do.", 4), + new Ability(Abilities.STEADFAST, "Steadfast (N)", "Raises Speed each time the Pokémon flinches.", 4), + new Ability(Abilities.STORM_DRAIN, "Storm Drain", "Draws in all Water-type moves to up Sp. Atk.", 4) .attr(TypeImmunityStatChangeAbAttr, Type.WATER, BattleStat.SPATK, 1), new Ability(Abilities.SUPER_LUCK, "Super Luck (N)", "Heightens the critical-hit ratios of moves.", 4), - new Ability(Abilities.TANGLED_FEET, "Tangled Feet (N)", "Raises evasion if the POKéMON is confused.", 4), - new Ability(Abilities.TECHNICIAN, "Technician (N)", "Powers up the POKéMON's weaker moves.", 4), + new Ability(Abilities.TANGLED_FEET, "Tangled Feet (N)", "Raises Evasiveness if the Pokémon is confused.", 4), + new Ability(Abilities.TECHNICIAN, "Technician (N)", "Powers up the Pokémon's weaker moves.", 4), new Ability(Abilities.TINTED_LENS, "Tinted Lens (N)", "Powers up \"not very effective\" moves.", 4), - new Ability(Abilities.UNAWARE, "Unaware (N)", "Ignores any stat changes in the POKéMON.", 4), - new Ability(Abilities.UNBURDEN, "Unburden (N)", "Raises SPEED if a held item is used.", 4), - new Ability(Abilities.ANALYTIC, "Analytic (N)", "Boosts move power when the POKéMON moves last.", 5), - new Ability(Abilities.BIG_PECKS, "Big Pecks", "Protects the POKéMON from DEFENSE-lowering attacks.", 5) + new Ability(Abilities.UNAWARE, "Unaware (N)", "Ignores any stat changes in the Pokémon.", 4), + new Ability(Abilities.UNBURDEN, "Unburden (N)", "Raises Speed if a held item is used.", 4), + new Ability(Abilities.ANALYTIC, "Analytic (N)", "Boosts move power when the Pokémon moves last.", 5), + new Ability(Abilities.BIG_PECKS, "Big Pecks", "Protects the Pokémon from Defense-lowering attacks.", 5) .attr(ProtectStatAbAttr, BattleStat.DEF), new Ability(Abilities.CONTRARY, "Contrary (N)", "Makes stat changes have an opposite effect.", 5), - new Ability(Abilities.CURSED_BODY, "Cursed Body (N)", "May disable a move used on the POKéMON.", 5), + new Ability(Abilities.CURSED_BODY, "Cursed Body (N)", "May disable a move used on the Pokémon.", 5), new Ability(Abilities.DEFEATIST, "Defeatist (N)", "Lowers stats when HP drops below half.", 5), - new Ability(Abilities.DEFIANT, "Defiant (N)", "Sharply raises ATTACK when the POKéMON's stats are lowered.", 5), + new Ability(Abilities.DEFIANT, "Defiant (N)", "Sharply raises Attack when the Pokémon's stats are lowered.", 5), new Ability(Abilities.FLARE_BOOST, "Flare Boost (N)", "Powers up special attacks when burned.", 5), new Ability(Abilities.FRIEND_GUARD, "Friend Guard (N)", "Reduces damage done to allies.", 5), new Ability(Abilities.HARVEST, "Harvest (N)", "May create another Berry after one is used.", 5), new Ability(Abilities.HEALER, "Healer (N)", "May heal an ally's status conditions.", 5), - new Ability(Abilities.HEAVY_METAL, "Heavy Metal (N)", "Doubles the POKéMON's weight.", 5), - new Ability(Abilities.ILLUSION, "Illusion (N)", "Enters battle disguised as the last POKéMON in the party.", 5), - new Ability(Abilities.IMPOSTER, "Imposter (N)", "It transforms itself into the POKéMON it is facing.", 5), + new Ability(Abilities.HEAVY_METAL, "Heavy Metal (N)", "Doubles the Pokémon's weight.", 5), + new Ability(Abilities.ILLUSION, "Illusion (N)", "Enters battle disguised as the last Pokémon in the party.", 5), + new Ability(Abilities.IMPOSTER, "Imposter (N)", "It transforms itself into the Pokémon it is facing.", 5), new Ability(Abilities.INFILTRATOR, "Infiltrator (N)", "Passes through the foe's barrier and strikes.", 5), - new Ability(Abilities.IRON_BARBS, "Iron Barbs (N)", "Inflicts damage to the POKéMON on contact.", 5), - new Ability(Abilities.JUSTIFIED, "Justified (N)", "Raises ATTACK when hit by a DARK-type move.", 5), - new Ability(Abilities.LIGHT_METAL, "Light Metal (N)", "Halves the POKéMON's weight.", 5), + new Ability(Abilities.IRON_BARBS, "Iron Barbs (N)", "Inflicts damage to the Pokémon on contact.", 5), + new Ability(Abilities.JUSTIFIED, "Justified (N)", "Raises Attack when hit by a Dark-type move.", 5), + new Ability(Abilities.LIGHT_METAL, "Light Metal (N)", "Halves the Pokémon's weight.", 5), new Ability(Abilities.MAGIC_BOUNCE, "Magic Bounce (N)", "Reflects status- changing moves.", 5), new Ability(Abilities.MOODY, "Moody (N)", "Raises one stat and lowers another.", 5), - new Ability(Abilities.MOXIE, "Moxie (N)", "Boosts ATTACK after knocking out any POKéMON.", 5), + new Ability(Abilities.MOXIE, "Moxie (N)", "Boosts Attack after knocking out any Pokémon.", 5), new Ability(Abilities.MULTISCALE, "Multiscale (N)", "Reduces damage when HP is full.", 5), - new Ability(Abilities.MUMMY, "Mummy (N)", "Contact with this POKéMON spreads this Ability.", 5), - new Ability(Abilities.OVERCOAT, "Overcoat", "Protects the POKéMON from weather damage.", 5) + new Ability(Abilities.MUMMY, "Mummy (N)", "Contact with this Pokémon spreads this Ability.", 5), + new Ability(Abilities.OVERCOAT, "Overcoat", "Protects the Pokémon from weather damage.", 5) .attr(BlockWeatherDamageAttr), - new Ability(Abilities.PICKPOCKET, "Pickpocket (N)", "Once per battle, steals an item when hit by another POKéMON.", 5), - new Ability(Abilities.POISON_TOUCH, "Poison Touch", "May poison targets when a POKéMON makes contact.", 5) + new Ability(Abilities.PICKPOCKET, "Pickpocket (N)", "Once per battle, steals an item when hit by another Pokémon.", 5), + new Ability(Abilities.POISON_TOUCH, "Poison Touch", "May poison targets when a Pokémon makes contact.", 5) .attr(PostDefendContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON), new Ability(Abilities.PRANKSTER, "Prankster (N)", "Gives priority to a status move.", 5), - new Ability(Abilities.RATTLED, "Rattled (N)", "BUG, GHOST or DARK type moves scare it and boost its SPEED.", 5), + new Ability(Abilities.RATTLED, "Rattled (N)", "BUG, GHOST or DARK type moves scare it and boost its Speed.", 5), new Ability(Abilities.REGENERATOR, "Regenerator (N)", "Restores a little HP when withdrawn from battle.", 5), new Ability(Abilities.SAND_FORCE, "Sand Force (N)", "Boosts certain moves' power in a sandstorm.", 5), - new Ability(Abilities.SAND_RUSH, "Sand Rush (N)", "Boosts the POKéMON's SPEED in a sandstorm.", 5), - new Ability(Abilities.SAP_SIPPER, "Sap Sipper", "Boosts ATTACK when hit by a GRASS-type move.", 5) + new Ability(Abilities.SAND_RUSH, "Sand Rush (N)", "Boosts the Pokémon's Speed in a sandstorm.", 5), + new Ability(Abilities.SAP_SIPPER, "Sap Sipper", "Boosts Attack when hit by a Grass-type move.", 5) .attr(TypeImmunityStatChangeAbAttr, Type.GRASS, BattleStat.ATK, 1), new Ability(Abilities.SHEER_FORCE, "Sheer Force (N)", "Removes added effects to increase move damage.", 5), - new Ability(Abilities.TELEPATHY, "Telepathy (N)", "Anticipates an ally's ATTACK and dodges it.", 5), + new Ability(Abilities.TELEPATHY, "Telepathy (N)", "Anticipates an ally's Attack and dodges it.", 5), new Ability(Abilities.TERAVOLT, "Teravolt (N)", "Moves can be used regardless of Abilities.", 5), new Ability(Abilities.TOXIC_BOOST, "Toxic Boost (N)", "Powers up physical attacks when poisoned.", 5), new Ability(Abilities.TURBOBLAZE, "Turboblaze (N)", "Moves can be used regardless of Abilities.", 5), new Ability(Abilities.UNNERVE, "Unnerve (N)", "Makes the foe nervous and unable to eat Berries.", 5), - new Ability(Abilities.VICTORY_STAR, "Victory Star (N)", "Boosts the accuracy of its allies and itself.", 5), - new Ability(Abilities.WEAK_ARMOR, "Weak Armor (N)", "Physical attacks lower DEFENSE and raise SPEED.", 5), + new Ability(Abilities.VICTORY_STAR, "Victory Star (N)", "Boosts the Accuracy of its allies and itself.", 5), + new Ability(Abilities.WEAK_ARMOR, "Weak Armor (N)", "Physical attacks lower Defense and raise Speed.", 5), new Ability(Abilities.WONDER_SKIN, "Wonder Skin (N)", "Makes status-changing moves more likely to miss.", 5), new Ability(Abilities.ZEN_MODE, "Zen Mode (N)", "Changes form when HP drops below half.", 5), - new Ability(Abilities.COMPETITIVE, "Competitive (N)", "Sharply raises SP. ATK when the POKéMON's stats are lowered.", 6), - new Ability(Abilities.DARK_AURA, "Dark Aura (N)", "Raises power of DARK type moves for all POKéMON in battle.", 6), - new Ability(Abilities.FAIRY_AURA, "Fairy Aura (N)", "Raises power of FAIRY type moves for all POKéMON in battle.", 6), - new Ability(Abilities.PROTEAN, "Protean (N)", "Changes the POKéMON's type to its last used move.", 6), - new Ability(Abilities.SLUSH_RUSH, "Slush Rush (N)", "Boosts the POKéMON's SPEED stat in a hailstorm.", 7), - new Ability(Abilities.NEUTRALIZING_GAS, "Neutralizing Gas (N)", "Neutralizes abilities of all POKéMON in battle.", 8) + new Ability(Abilities.COMPETITIVE, "Competitive (N)", "Sharply raises Sp. Atk when the Pokémon's stats are lowered.", 6), + new Ability(Abilities.DARK_AURA, "Dark Aura (N)", "Raises power of DARK type moves for all Pokémon in battle.", 6), + new Ability(Abilities.FAIRY_AURA, "Fairy Aura (N)", "Raises power of FAIRY type moves for all Pokémon in battle.", 6), + new Ability(Abilities.PROTEAN, "Protean (N)", "Changes the Pokémon's type to its last used move.", 6), + new Ability(Abilities.SLUSH_RUSH, "Slush Rush (N)", "Boosts the Pokémon's Speed stat in a hailstorm.", 7), + new Ability(Abilities.NEUTRALIZING_GAS, "Neutralizing Gas (N)", "Neutralizes abilities of all Pokémon in battle.", 8) ); } \ No newline at end of file diff --git a/src/data/battle-stat.ts b/src/data/battle-stat.ts index 82ad68f58ec..e0cb6fb53f0 100644 --- a/src/data/battle-stat.ts +++ b/src/data/battle-stat.ts @@ -12,19 +12,19 @@ export enum BattleStat { export function getBattleStatName(stat: BattleStat) { switch (stat) { case BattleStat.ATK: - return 'ATTACK'; + return 'Attack'; case BattleStat.DEF: - return 'DEFENSE'; + return 'Defense'; case BattleStat.SPATK: - return 'SP. ATK'; + return 'Sp. Atk'; case BattleStat.SPDEF: - return 'SP. DEF'; + return 'Sp. Def'; case BattleStat.SPD: - return 'SPEED'; + return 'Speed'; case BattleStat.ACC: - return 'accuracy'; + return 'Accuracy'; case BattleStat.EVA: - return 'evasiveness'; + return 'Evasiveness'; default: return '???'; } diff --git a/src/data/berry.ts b/src/data/berry.ts index d75a6d5f49d..64dc6a25ba4 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -1,10 +1,11 @@ import { PokemonHealPhase, StatChangePhase } from "../battle-phases"; import { getPokemonMessage } from "../messages"; -import Pokemon, { HitResult, MoveResult } from "../pokemon"; +import Pokemon, { HitResult } from "../pokemon"; import { getBattleStatName } from "./battle-stat"; import { BattleStat } from "./battle-stat"; import { BattlerTagType } from "./battler-tag"; import { getStatusEffectHealText } from "./status-effect"; +import * as Utils from "../utils"; export enum BerryType { SITRUS, @@ -20,7 +21,7 @@ export enum BerryType { } export function getBerryName(berryType: BerryType) { - return `${BerryType[berryType]} BERRY`; + return `${Utils.toReadableString(BerryType[berryType])} Berry`; } export function getBerryEffectDescription(berryType: BerryType) { diff --git a/src/data/biome.ts b/src/data/biome.ts index bda8abac66c..a333abc9be2 100644 --- a/src/data/biome.ts +++ b/src/data/biome.ts @@ -41,17 +41,17 @@ export enum Biome { export function getBiomeName(biome: Biome) { switch (biome) { case Biome.GRASS: - return 'GRASSY FIELD'; + return 'Grassy Field'; case Biome.RUINS: - return 'ANCIENT RUINS'; + return 'Ancient Ruins'; case Biome.ABYSS: - return 'THE ABYSS'; + return 'The Abyss'; case Biome.SPACE: - return 'STRATOSPHERE'; + return 'Stratosphere'; case Biome.END: - return 'FINAL DESTINATION'; + return 'Final Destination'; default: - return Biome[biome].replace(/\_/g, ' '); + return Utils.toReadableString(Biome[biome]); } } diff --git a/src/data/move.ts b/src/data/move.ts index d184f3fa6e8..17fc1e5fbcc 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -70,7 +70,7 @@ export default class Move { constructor(id: Moves, name: string, type: Type, category: MoveCategory, defaultMoveTarget: MoveTarget, power: integer, accuracy: integer, pp: integer, tm: integer, effect: string, chance: integer, priority: integer, generation: integer) { this.id = id; - this.name = name.toUpperCase(); + this.name = name; this.type = type; this.category = category; this.moveTarget = defaultMoveTarget; @@ -1926,7 +1926,7 @@ export class CopyBiomeTypeAttr extends MoveEffectAttr { user.summonData.types = [ biomeType ]; - user.scene.queueMessage(getPokemonMessage(user, ` transformed\ninto the ${Type[biomeType].toUpperCase()} type!`)); + user.scene.queueMessage(getPokemonMessage(user, ` transformed\ninto the ${Utils.toReadableString(Type[biomeType])} type!`)); return true; } @@ -2237,7 +2237,7 @@ export function initMoves() { new SelfStatusMove(Moves.SWORDS_DANCE, "Swords Dance", Type.NORMAL, -1, 20, 88, "Sharply raises user's Attack.", -1, 0, 1) .attr(StatChangeAttr, BattleStat.ATK, 2, true), new AttackMove(Moves.CUT, "Cut", Type.NORMAL, MoveCategory.PHYSICAL, 50, 95, 30, -1, "", -1, 0, 1), - new AttackMove(Moves.GUST, "Gust", Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, "Hits Pokémon using FLY/BOUNCE/SKY DROP with double power.", -1, 0, 1) + new AttackMove(Moves.GUST, "Gust", Type.FLYING, MoveCategory.SPECIAL, 40, 100, 35, -1, "Hits Pokémon using Fly/Bounce/Sky Drop with double power.", -1, 0, 1) .attr(HitsTagAttr, BattlerTagType.FLYING, true) .target(MoveTarget.OTHER), new AttackMove(Moves.WING_ATTACK, "Wing Attack", Type.FLYING, MoveCategory.PHYSICAL, 60, 100, 35, -1, "", -1, 0, 1) @@ -2649,7 +2649,7 @@ export function initMoves() { .target(MoveTarget.ENEMY_SIDE), new AttackMove(Moves.ZAP_CANNON, "Zap Cannon", Type.ELECTRIC, MoveCategory.SPECIAL, 120, 50, 5, -1, "Paralyzes opponent.", 100, 0, 2) .attr(StatusEffectAttr, StatusEffect.PARALYSIS), - new StatusMove(Moves.FORESIGHT, "Foresight (N)", Type.NORMAL, -1, 40, -1, "Resets opponent's Evasiveness, and allows Normal- and Fighting-type attacks to hit Ghosts.", -1, 0, 2), // TODO + new StatusMove(Moves.FORESIGHT, "Foresight (N)", Type.NORMAL, -1, 40, -1, "Resets opponent's Evasiveness, and allows Normal-type and Fighting-type attacks to hit Ghosts.", -1, 0, 2), // TODO new StatusMove(Moves.DESTINY_BOND, "Destiny Bond (N)", Type.GHOST, -1, 5, -1, "If the user faints, the opponent also faints.", -1, 0, 2) .ignoresProtect(), new StatusMove(Moves.PERISH_SONG, "Perish Song (N)", Type.NORMAL, -1, 5, -1, "Any Pokémon in play when this attack is used faints in 3 turns.", -1, 0, 2) @@ -2744,7 +2744,7 @@ export function initMoves() { new AttackMove(Moves.HIDDEN_POWER, "Hidden Power (N)", Type.NORMAL, MoveCategory.SPECIAL, 60, 100, 15, -1, "Type and power depends on user's IVs.", -1, 0, 2), new AttackMove(Moves.CROSS_CHOP, "Cross Chop", Type.FIGHTING, MoveCategory.PHYSICAL, 100, 80, 5, -1, "High critical hit ratio.", -1, 0, 2) .attr(HighCritAttr), - new AttackMove(Moves.TWISTER, "Twister", Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, -1, "May cause flinching. Hits Pokémon using FLY/BOUNCE with double power.", 20, 0, 2) + new AttackMove(Moves.TWISTER, "Twister", Type.DRAGON, MoveCategory.SPECIAL, 40, 100, 20, -1, "May cause flinching. Hits Pokémon using Fly/Bounce/Sky Drop with double power.", 20, 0, 2) .attr(HitsTagAttr, BattlerTagType.FLYING, true) .attr(FlinchAttr) .target(MoveTarget.ALL_NEAR_ENEMIES), // TODO @@ -2989,7 +2989,7 @@ export function initMoves() { new SelfStatusMove(Moves.ROOST, "Roost", Type.FLYING, -1, 5, -1, "User recovers half of its max HP and loses the FLYING type temporarily.", -1, 0, 4) .attr(HealAttr) .attr(AddBattlerTagAttr, BattlerTagType.IGNORE_FLYING, true, 1), - new SelfStatusMove(Moves.GRAVITY, "Gravity", Type.PSYCHIC, -1, 5, -1, "Prevents moves like FLY and BOUNCE and the Ability LEVITATE for 5 turns.", -1, 0, 4) + new SelfStatusMove(Moves.GRAVITY, "Gravity", Type.PSYCHIC, -1, 5, -1, "Prevents moves like Fly and Bounce and the Ability Levitate for 5 turns.", -1, 0, 4) .attr(AddArenaTagAttr, ArenaTagType.GRAVITY, 5) .target(MoveTarget.BOTH_SIDES), new StatusMove(Moves.MIRACLE_EYE, "Miracle Eye (N)", Type.PSYCHIC, -1, 40, -1, "Resets opponent's Evasiveness, removes DARK's PSYCHIC immunity.", -1, 0, 4), @@ -3005,7 +3005,7 @@ export function initMoves() { .attr(MovePowerMultiplierAttr, (user: Pokemon, target: Pokemon, move: Move) => target.getHpRatio() < 0.5 ? 2 : 1), new AttackMove(Moves.NATURAL_GIFT, "Natural Gift (N)", Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, "Power and type depend on the user's held berry.", -1, 0, 4) .makesContact(false), - new AttackMove(Moves.FEINT, "Feint", Type.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, "Only hits if opponent uses PROTECT or DETECT in the same turn.", -1, 2, 4) + new AttackMove(Moves.FEINT, "Feint", Type.NORMAL, MoveCategory.PHYSICAL, 30, 100, 10, -1, "Only hits if opponent uses Protect or Detect in the same turn.", -1, 2, 4) .condition((user: Pokemon, target: Pokemon, move: Move) => !!target.getTag(BattlerTagType.PROTECTED)) .makesContact(false) .ignoresProtect(), @@ -3050,7 +3050,7 @@ export function initMoves() { new AttackMove(Moves.PUNISHMENT, "Punishment (N)", Type.DARK, MoveCategory.PHYSICAL, -1, 100, 5, -1, "Power increases when opponent's stats have been raised.", -1, 0, 4), new AttackMove(Moves.LAST_RESORT, "Last Resort", Type.NORMAL, MoveCategory.PHYSICAL, 140, 100, 5, -1, "Can only be used after all other moves are used.", -1, 0, 4) .condition((user: Pokemon, target: Pokemon, move: Move) => !user.getMoveset().filter(m => m.moveId !== move.id && m.getPpRatio() > 0).length), - new StatusMove(Moves.WORRY_SEED, "Worry Seed (N)", Type.GRASS, 100, 10, -1, "Changes the opponent's Ability to INSOMNIA.", -1, 0, 4), + new StatusMove(Moves.WORRY_SEED, "Worry Seed (N)", Type.GRASS, 100, 10, -1, "Changes the opponent's Ability to Insomnia.", -1, 0, 4), new AttackMove(Moves.SUCKER_PUNCH, "Sucker Punch (N)", Type.DARK, MoveCategory.PHYSICAL, 70, 100, 5, -1, "User attacks first, but only works if opponent is readying an attack.", -1, 0, 4), new StatusMove(Moves.TOXIC_SPIKES, "Toxic Spikes", Type.POISON, -1, 20, 91, "Poisons opponents when they switch into battle.", -1, 0, 4) .attr(AddArenaTrapTagAttr, ArenaTagType.TOXIC_SPIKES) diff --git a/src/data/pokeball.ts b/src/data/pokeball.ts index bf3e06bedb0..3b12da756b1 100644 --- a/src/data/pokeball.ts +++ b/src/data/pokeball.ts @@ -1,5 +1,4 @@ import BattleScene from "../battle-scene"; -import { toPokemonUpperCase } from "../utils"; export enum PokeballType { POKEBALL, @@ -43,7 +42,7 @@ export function getPokeballName(type: PokeballType): string { ret = 'Luxury Ball'; break; } - return toPokemonUpperCase(ret); + return ret; } export function getPokeballCatchMultiplier(type: PokeballType): number { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 2454e8bb909..9e073fd8b86 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -902,7 +902,7 @@ export function initSpecies() { new PokemonForm("Origin Forme", "origin", Type.GHOST, Type.DRAGON, 6.9, 650, Abilities.LEVITATE, Abilities.NONE, Abilities.NONE, 680, 150, 120, 100, 120, 100, 90, 3, 0, 306, GrowthRate.SLOW, "Undiscovered", null, null, 120, false) ), new PokemonSpecies(Species.CRESSELIA, "Cresselia", 4, true, false, false, "Lunar Pokémon", Type.PSYCHIC, null, 1.5, 85.6, Abilities.LEVITATE, Abilities.NONE, Abilities.NONE, 600, 120, 70, 120, 75, 130, 85, 3, 100, 270, GrowthRate.SLOW, "Undiscovered", null, 0, 120, false), - new PokemonSpecies(Species.PHIONE, "Phione", 4, false, false, true, "Sea Drifter Pokémon", Type.WATER, null, 0.4, 3.1, Abilities.HYDRATION, Abilities.NONE, Abilities.NONE, 480, 80, 80, 80, 80, 80, 80, 30, 70, 216, GrowthRate.SLOW, "Fairy", "Water 1", null, 40, false), + new PokemonSpecies(Species.PHIONE, "Phione", 4, false, false, false, "Sea Drifter Pokémon", Type.WATER, null, 0.4, 3.1, Abilities.HYDRATION, Abilities.NONE, Abilities.NONE, 480, 80, 80, 80, 80, 80, 80, 30, 70, 216, GrowthRate.SLOW, "Fairy", "Water 1", null, 40, false), new PokemonSpecies(Species.MANAPHY, "Manaphy", 4, false, false, true, "Seafaring Pokémon", Type.WATER, null, 0.3, 1.4, Abilities.HYDRATION, Abilities.NONE, Abilities.NONE, 600, 100, 100, 100, 100, 100, 100, 3, 70, 270, GrowthRate.SLOW, "Fairy", "Water 1", null, 10, false), new PokemonSpecies(Species.DARKRAI, "Darkrai", 4, false, false, true, "Pitch-Black Pokémon", Type.DARK, null, 1.5, 50.5, Abilities.BAD_DREAMS, Abilities.NONE, Abilities.NONE, 600, 70, 90, 90, 135, 90, 125, 3, 0, 270, GrowthRate.SLOW, "Undiscovered", null, null, 120, false), new PokemonSpecies(Species.SHAYMIN, "Shaymin", 4, false, false, true, "Gratitude Pokémon", Type.GRASS, null, 0.2, 2.1, Abilities.NATURAL_CURE, Abilities.NONE, Abilities.NONE, 600, 100, 100, 100, 100, 100, 100, 45, 100, 270, GrowthRate.MEDIUM_SLOW, "Undiscovered", null, null, 120, false, true, @@ -1117,5 +1117,12 @@ export function initSpecies() { return s; }))].map(s => s.name)); } + + const speciesFilter = (species: PokemonSpecies) => !species.legendary && !species.pseudoLegendary && !species.mythical && species.baseTotal >= 540; + console.log(!speciesFilter ? 'all' : [...new Set(allSpecies.slice(0, -1).filter(speciesFilter).map(s => { + while (pokemonPrevolutions.hasOwnProperty(s.speciesId)) + s = getPokemonSpecies(pokemonPrevolutions[s.speciesId]); + return s; + }))].map(s => s.name)); }, 1000); }*/ \ No newline at end of file diff --git a/src/data/pokemon-stat.ts b/src/data/pokemon-stat.ts index 31f5bab3ebc..d7ca92b43c7 100644 --- a/src/data/pokemon-stat.ts +++ b/src/data/pokemon-stat.ts @@ -1,5 +1,3 @@ -import { toPokemonUpperCase } from "../utils"; - export enum Stat { HP = 0, ATK, @@ -31,5 +29,5 @@ export function getStatName(stat: Stat) { ret = 'Speed'; break; } - return toPokemonUpperCase(ret); + return ret; } \ No newline at end of file diff --git a/src/data/trainer-type.ts b/src/data/trainer-type.ts index 0d33807cc87..9c4ca851d09 100644 --- a/src/data/trainer-type.ts +++ b/src/data/trainer-type.ts @@ -1,8 +1,10 @@ import BattleScene from "../battle-scene"; +import { EnemyPokemon } from "../pokemon"; import * as Utils from "../utils"; import { Moves } from "./move"; +import { pokemonEvolutions, pokemonPrevolutions } from "./pokemon-evolutions"; import { pokemonLevelMoves } from "./pokemon-level-moves"; -import { PokemonSpeciesFilter } from "./pokemon-species"; +import PokemonSpecies, { PokemonSpeciesFilter, getPokemonSpecies } from "./pokemon-species"; import { Species } from "./species"; import { tmSpecies } from "./tms"; import { Type } from "./type"; @@ -56,14 +58,13 @@ export enum TrainerType { WAITER, WORKER, YOUNGSTER, + CYNTHIA, RIVAL, - CYNTHIA -} - -export enum TrainerPartyType { - DEFAULT, - BALANCED, - REPEATED + RIVAL_2, + RIVAL_3, + RIVAL_4, + RIVAL_5, + RIVAL_6 } export enum TrainerPoolTier { @@ -78,38 +79,185 @@ export interface TrainerTierPools { [key: integer]: Species[] } +export enum TrainerPartyMemberStrength { + WEAKER, + WEAK, + AVERAGE, + STRONG, + STRONGER +} + +export class TrainerPartyTemplate { + public size: integer; + public strength: TrainerPartyMemberStrength; + public sameSpecies: boolean; + public balanced: boolean; + + constructor(size: integer, strength: TrainerPartyMemberStrength, sameSpecies?: boolean, balanced?: boolean) { + this.size = size; + this.strength = strength; + this.sameSpecies = !!sameSpecies; + this.balanced = !!balanced; + } + + getStrength(index: integer): TrainerPartyMemberStrength { + return this.strength; + } + + isSameSpecies(index: integer): boolean { + return this.sameSpecies; + } + + isBalanced(index: integer): boolean { + return this.balanced; + } +} + +export class TrainerPartyCompoundTemplate extends TrainerPartyTemplate { + public templates: TrainerPartyTemplate[]; + + constructor(...templates: TrainerPartyTemplate[]) { + super(templates.reduce((total: integer, template: TrainerPartyTemplate) => { + total += template.size; + return total; + }, 0), TrainerPartyMemberStrength.AVERAGE); + this.templates = templates; + } + + getStrength(index: integer): TrainerPartyMemberStrength { + let t = 0; + for (let template of this.templates) { + if (t + template.size > index) + return template.getStrength(index - t); + t += template.size; + } + + return super.getStrength(index); + } + + isSameSpecies(index: integer): boolean { + let t = 0; + for (let template of this.templates) { + if (t + template.size > index) + return template.isSameSpecies(index - t); + t += template.size; + } + + return super.isSameSpecies(index); + } + + isBalanced(index: integer): boolean { + let t = 0; + for (let template of this.templates) { + if (t + template.size > index) + return template.isBalanced(index - t); + t += template.size; + } + + return super.isBalanced(index); + } +} + +export const trainerPartyTemplates = { + ONE_AVG: new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE), + ONE_STRONG: new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), + ONE_STRONGER: new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONGER), + TWO_WEAKER: new TrainerPartyTemplate(2, TrainerPartyMemberStrength.WEAKER), + TWO_WEAK: new TrainerPartyTemplate(2, TrainerPartyMemberStrength.WEAK), + TWO_WEAK_SAME_ONE_AVG: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(2, TrainerPartyMemberStrength.WEAK, true), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE)), + TWO_WEAK_SAME_TWO_WEAK_SAME: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(2, TrainerPartyMemberStrength.WEAK, true), new TrainerPartyTemplate(2, TrainerPartyMemberStrength.WEAK, true)), + TWO_AVG: new TrainerPartyTemplate(2, TrainerPartyMemberStrength.AVERAGE), + TWO_AVG_SAME_ONE_AVG: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(2, TrainerPartyMemberStrength.AVERAGE, true), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE)), + TWO_AVG_SAME_ONE_STRONG: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(2, TrainerPartyMemberStrength.AVERAGE, true), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG)), + TWO_AVG_SAME_TWO_AVG_SAME: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(2, TrainerPartyMemberStrength.AVERAGE, true), new TrainerPartyTemplate(2, TrainerPartyMemberStrength.AVERAGE, true)), + THREE_WEAK: new TrainerPartyTemplate(3, TrainerPartyMemberStrength.WEAK), + THREE_WEAK_SAME: new TrainerPartyTemplate(3, TrainerPartyMemberStrength.WEAK, true), + THREE_AVG: new TrainerPartyTemplate(3, TrainerPartyMemberStrength.AVERAGE), + THREE_AVG_SAME: new TrainerPartyTemplate(3, TrainerPartyMemberStrength.AVERAGE, true), + FOUR_WEAKER: new TrainerPartyTemplate(4, TrainerPartyMemberStrength.WEAKER), + FOUR_WEAKER_SAME: new TrainerPartyTemplate(4, TrainerPartyMemberStrength.WEAKER, true), + FOUR_WEAK: new TrainerPartyTemplate(4, TrainerPartyMemberStrength.WEAK), + FOUR_WEAK_SAME: new TrainerPartyTemplate(4, TrainerPartyMemberStrength.WEAK), + FIVE_WEAK: new TrainerPartyTemplate(5, TrainerPartyMemberStrength.WEAK, true), + SIX_WEAK_SAME: new TrainerPartyTemplate(6, TrainerPartyMemberStrength.WEAKER, true), + SIX_WEAKER: new TrainerPartyTemplate(6, TrainerPartyMemberStrength.WEAKER), + SIX_WEAKER_SAME: new TrainerPartyTemplate(6, TrainerPartyMemberStrength.WEAKER, true), + SIX_WEAK_BALANCED: new TrainerPartyTemplate(6, TrainerPartyMemberStrength.WEAK, false, true), + + RIVAL: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE)), + RIVAL_2: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE)), + RIVAL_3: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE, false, true)), + RIVAL_4: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE), new TrainerPartyTemplate(3, TrainerPartyMemberStrength.AVERAGE, false, true)), + RIVAL_5: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE), new TrainerPartyTemplate(3, TrainerPartyMemberStrength.AVERAGE, false, true), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG)), + RIVAL_6: new TrainerPartyCompoundTemplate(new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONG), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.AVERAGE), new TrainerPartyTemplate(3, TrainerPartyMemberStrength.AVERAGE, false, true), new TrainerPartyTemplate(1, TrainerPartyMemberStrength.STRONGER)) +}; + +type PartyMemberFunc = (scene: BattleScene, level: integer) => EnemyPokemon; + +export interface PartyMemberFuncs { + [key: integer]: PartyMemberFunc +} + export class TrainerConfig { public trainerType: TrainerType; public name: string; public nameFemale: string; public hasGenders: boolean = false; public isDouble: boolean = false; - public partyType: TrainerPartyType = TrainerPartyType.DEFAULT; + public staticParty: boolean = false; + public battleBgm: string; public encounterBgm: string; public femaleEncounterBgm: string; + public partyTemplates: TrainerPartyTemplate[]; + public partyMemberFuncs: PartyMemberFuncs = {}; public speciesPools: TrainerTierPools; public speciesFilter: PokemonSpeciesFilter; + public encounterMessages: string[] = []; + public victoryMessages: string[] = []; + public defeatMessages: string[] = []; + + public femaleEncounterMessages: string[]; + public femaleVictoryMessages: string[]; + public femaleDefeatMessages: string[]; + constructor(trainerType: TrainerType, allowLegendaries?: boolean) { this.trainerType = trainerType; - this.name = Utils.toPokemonUpperCase(TrainerType[this.trainerType].toString().replace(/\_/g, ' ')); + this.name = Utils.toReadableString(TrainerType[this.getDerivedType()]); + this.battleBgm = 'battle_trainer'; this.encounterBgm = this.name.toLowerCase(); + this.partyTemplates = [ trainerPartyTemplates.TWO_AVG ]; this.speciesFilter = species => allowLegendaries || (!species.legendary && !species.pseudoLegendary && !species.mythical); } - public getKey(female?: boolean): string { - let ret = TrainerType[this.trainerType].toString().toLowerCase(); + getKey(female?: boolean): string { + let ret = TrainerType[this.getDerivedType()].toString().toLowerCase(); if (this.hasGenders) ret += `_${female ? 'f' : 'm'}`; return ret; } - public setName(name: string): TrainerConfig { + setName(name: string): TrainerConfig { this.name = name; return this; } - public setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { + getDerivedType(): TrainerType { + let trainerType = this.trainerType; + switch (trainerType) { + case TrainerType.RIVAL_2: + case TrainerType.RIVAL_3: + case TrainerType.RIVAL_4: + case TrainerType.RIVAL_5: + case TrainerType.RIVAL_6: + trainerType = TrainerType.RIVAL; + break; + } + + return trainerType; + } + + setHasGenders(nameFemale?: string, femaleEncounterBgm?: TrainerType | string): TrainerConfig { this.hasGenders = true; this.nameFemale = nameFemale; if (femaleEncounterBgm) @@ -117,33 +265,66 @@ export class TrainerConfig { return this; } - public setDouble(): TrainerConfig { + setDouble(): TrainerConfig { this.isDouble = true; return this; } - public setEncounterBgm(encounterBgm: TrainerType | string): TrainerConfig { + setStaticParty(): TrainerConfig { + this.staticParty = true; + return this; + } + + setBattleBgm(battleBgm: string): TrainerConfig { + this.battleBgm = battleBgm; + return this; + } + + setEncounterBgm(encounterBgm: TrainerType | string): TrainerConfig { this.encounterBgm = typeof encounterBgm === 'number' ? TrainerType[encounterBgm].toString().replace(/\_/g, ' ').toLowerCase() : encounterBgm; return this; } - public setPartyType(partyType: TrainerPartyType): TrainerConfig { - this.partyType = partyType; + setPartyTemplates(...partyTemplates: TrainerPartyTemplate[]): TrainerConfig { + this.partyTemplates = partyTemplates; return this; } - public setSpeciesPools(speciesPools: TrainerTierPools | Species[]): TrainerConfig { - this.speciesPools = (Array.isArray(speciesPools) ? speciesPools : { [TrainerPoolTier.COMMON]: speciesPools }) as unknown as TrainerTierPools; + setPartyMemberFunc(slotIndex: integer, partyMemberFunc: PartyMemberFunc): TrainerConfig { + this.partyMemberFuncs[slotIndex] = partyMemberFunc; return this; } - public setSpeciesFilter(speciesFilter: PokemonSpeciesFilter, allowLegendaries?: boolean): TrainerConfig { + setSpeciesPools(speciesPools: TrainerTierPools | Species[]): TrainerConfig { + this.speciesPools = (Array.isArray(speciesPools) ? { [TrainerPoolTier.COMMON]: speciesPools } : speciesPools) as unknown as TrainerTierPools; + return this; + } + + setSpeciesFilter(speciesFilter: PokemonSpeciesFilter, allowLegendaries?: boolean): TrainerConfig { const baseFilter = this.speciesFilter; this.speciesFilter = allowLegendaries ? speciesFilter : species => speciesFilter(species) && baseFilter(species); return this; } - public getName(female?: boolean): string { + setEncounterMessages(messages: string[], femaleMessages?: string[]): TrainerConfig { + this.encounterMessages = messages; + this.femaleEncounterMessages = femaleMessages; + return this; + } + + setVictoryMessages(messages: string[], femaleMessages?: string[]): TrainerConfig { + this.victoryMessages = messages; + this.femaleVictoryMessages = femaleMessages; + return this; + } + + setDefeatMessages(messages: string[], femaleMessages?: string[]): TrainerConfig { + this.defeatMessages = messages; + this.femaleDefeatMessages = femaleMessages; + return this; + } + + getName(female?: boolean): string { let ret = this.name; if (this.hasGenders) { @@ -157,11 +338,6 @@ export class TrainerConfig { return ret; } - public genPartySize(): integer { - // TODO - return this.isDouble ? 2 : 1; - } - loadAssets(scene: BattleScene, female: boolean): Promise { return new Promise(resolve => { const trainerKey = this.getKey(female); @@ -170,7 +346,7 @@ export class TrainerConfig { const originalWarn = console.warn; // Ignore warnings for missing frames, because there will be a lot console.warn = () => {}; - const frameNames = scene.anims.generateFrameNames(trainerKey, { zeroPad: 4, suffix: ".png", start: 1, end:24 }); + const frameNames = scene.anims.generateFrameNames(trainerKey, { zeroPad: 4, suffix: ".png", start: 1, end: 24 }); console.warn = originalWarn; scene.anims.create({ key: trainerKey, @@ -192,30 +368,54 @@ interface TrainerConfigs { [key: integer]: TrainerConfig } +function getRandomPartyMemberFunc(species: Species[], postProcess?: (enemyPokemon: EnemyPokemon) => void): PartyMemberFunc { + return (scene: BattleScene, level: integer) => { + const ret = new EnemyPokemon(scene, getPokemonSpecies(Phaser.Math.RND.pick(species)), level); + if (postProcess) + postProcess(ret); + return ret; + }; +} + +function getSpeciesFilterRandomPartyMemberFunc(speciesFilter: PokemonSpeciesFilter, allowLegendaries?: boolean, postProcess?: (EnemyPokemon: EnemyPokemon) => void): PartyMemberFunc { + const originalSpeciesFilter = speciesFilter; + speciesFilter = (species: PokemonSpecies) => allowLegendaries || (!species.legendary && !species.pseudoLegendary && !species.mythical) && originalSpeciesFilter(species); + return (scene: BattleScene, level: integer) => { + const ret = new EnemyPokemon(scene, scene.randomSpecies(scene.currentBattle.waveIndex, level, speciesFilter), level); + if (postProcess) + postProcess(ret); + return ret; + }; +} + export const trainerConfigs: TrainerConfigs = { - [TrainerType.ACE_TRAINER]: new TrainerConfig(++t).setHasGenders().setPartyType(TrainerPartyType.BALANCED), - [TrainerType.ARTIST]: new TrainerConfig(++t).setEncounterBgm(TrainerType.RICH).setSpeciesPools([ Species.SMEARGLE ]), + [TrainerType.ACE_TRAINER]: new TrainerConfig(++t).setHasGenders().setPartyTemplates(trainerPartyTemplates.SIX_WEAK_BALANCED), + [TrainerType.ARTIST]: new TrainerConfig(++t).setEncounterBgm(TrainerType.RICH).setPartyTemplates(trainerPartyTemplates.ONE_STRONG, trainerPartyTemplates.TWO_AVG, trainerPartyTemplates.THREE_AVG).setSpeciesPools([ Species.SMEARGLE ]), [TrainerType.BACKERS]: new TrainerConfig(++t).setHasGenders().setEncounterBgm(TrainerType.CYCLIST).setDouble(), [TrainerType.BACKPACKER]: new TrainerConfig(++t).setHasGenders().setSpeciesFilter(s => s.isOfType(Type.FLYING) || s.isOfType(Type.ROCK)), [TrainerType.BAKER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK).setSpeciesFilter(s => s.isOfType(Type.GRASS) || s.isOfType(Type.FIRE)), [TrainerType.BEAUTY]: new TrainerConfig(++t).setEncounterBgm(TrainerType.PARASOL_LADY), [TrainerType.BIKER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.ROUGHNECK).setSpeciesFilter(s => s.isOfType(Type.POISON)), [TrainerType.BLACK_BELT]: new TrainerConfig(++t).setHasGenders('Battle Girl', TrainerType.PSYCHIC).setEncounterBgm(TrainerType.ROUGHNECK).setSpeciesFilter(s => s.isOfType(Type.FIGHTING)), - [TrainerType.BREEDER]: new TrainerConfig(++t).setHasGenders().setDouble().setEncounterBgm(TrainerType.POKEFAN), + [TrainerType.BREEDER]: new TrainerConfig(++t).setHasGenders().setDouble().setEncounterBgm(TrainerType.POKEFAN).setPartyTemplates(trainerPartyTemplates.SIX_WEAKER), [TrainerType.CLERK]: new TrainerConfig(++t).setHasGenders(), [TrainerType.CYCLIST]: new TrainerConfig(++t).setHasGenders().setSpeciesFilter(s => !!pokemonLevelMoves[s.speciesId].find(plm => plm[1] === Moves.QUICK_ATTACK)), [TrainerType.DANCER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CYCLIST), [TrainerType.DEPOT_AGENT]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK), [TrainerType.DOCTOR]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK), - [TrainerType.FISHERMAN]: new TrainerConfig(++t).setEncounterBgm(TrainerType.BACKPACKER).setSpeciesPools({ - [TrainerPoolTier.COMMON]: [ Species.TENTACOOL, Species.MAGIKARP, Species.GOLDEEN, Species.STARYU, Species.REMORAID ], - [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.SHELLDER, Species.KRABBY, Species.HORSEA, Species.CARVANHA, Species.BARBOACH, Species.CORPHISH, Species.FINNEON, Species.TYMPOLE, Species.BASCULIN, Species.FRILLISH ], - [TrainerPoolTier.RARE]: [ Species.CHINCHOU, Species.CORSOLA, Species.WAILMER, Species.CLAMPERL, Species.LUVDISC, Species.MANTYKE, Species.ALOMOMOLA ], - [TrainerPoolTier.SUPER_RARE]: [ Species.LAPRAS, Species.FEEBAS, Species.RELICANTH ] - }), + [TrainerType.FISHERMAN]: new TrainerConfig(++t).setEncounterBgm(TrainerType.BACKPACKER) + .setPartyTemplates(trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG, trainerPartyTemplates.ONE_AVG, trainerPartyTemplates.THREE_WEAK_SAME, trainerPartyTemplates.ONE_STRONG, trainerPartyTemplates.SIX_WEAKER) + .setSpeciesPools({ + [TrainerPoolTier.COMMON]: [ Species.TENTACOOL, Species.MAGIKARP, Species.GOLDEEN, Species.STARYU, Species.REMORAID ], + [TrainerPoolTier.UNCOMMON]: [ Species.POLIWAG, Species.SHELLDER, Species.KRABBY, Species.HORSEA, Species.CARVANHA, Species.BARBOACH, Species.CORPHISH, Species.FINNEON, Species.TYMPOLE, Species.BASCULIN, Species.FRILLISH ], + [TrainerPoolTier.RARE]: [ Species.CHINCHOU, Species.CORSOLA, Species.WAILMER, Species.CLAMPERL, Species.LUVDISC, Species.MANTYKE, Species.ALOMOMOLA ], + [TrainerPoolTier.SUPER_RARE]: [ Species.LAPRAS, Species.FEEBAS, Species.RELICANTH ] + } + ), [TrainerType.GUITARIST]: new TrainerConfig(++t).setEncounterBgm(TrainerType.ROUGHNECK).setSpeciesFilter(s => s.isOfType(Type.ELECTRIC)), [TrainerType.HARLEQUIN]: new TrainerConfig(++t).setEncounterBgm(TrainerType.PSYCHIC).setSpeciesFilter(s => tmSpecies[Moves.TRICK_ROOM].indexOf(s.speciesId) > -1), - [TrainerType.HIKER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.BACKPACKER).setSpeciesFilter(s => s.isOfType(Type.GROUND) || s.isOfType(Type.ROCK)), + [TrainerType.HIKER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.BACKPACKER).setSpeciesFilter(s => s.isOfType(Type.GROUND) || s.isOfType(Type.ROCK)) + .setPartyTemplates(trainerPartyTemplates.TWO_AVG_SAME_ONE_AVG, trainerPartyTemplates.TWO_AVG_SAME_ONE_STRONG, trainerPartyTemplates.TWO_AVG, trainerPartyTemplates.FOUR_WEAK, trainerPartyTemplates.ONE_STRONG), [TrainerType.HOOLIGANS]: new TrainerConfig(++t).setDouble().setEncounterBgm(TrainerType.ROUGHNECK).setSpeciesFilter(s => s.isOfType(Type.POISON) || s.isOfType(Type.DARK)), [TrainerType.HOOPSTER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CYCLIST), [TrainerType.INFIELDER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CYCLIST), @@ -228,7 +428,8 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.OFFICER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK).setSpeciesPools([ Species.VULPIX, Species.GROWLITHE, Species.SNUBBULL, Species.HOUNDOUR, Species.POOCHYENA, Species.ELECTRIKE, Species.LILLIPUP ]), [TrainerType.PARASOL_LADY]: new TrainerConfig(++t).setSpeciesFilter(s => s.isOfType(Type.WATER)), [TrainerType.PILOT]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK).setSpeciesFilter(s => tmSpecies[Moves.FLY].indexOf(s.speciesId) > -1), - [TrainerType.POKEFAN]: new TrainerConfig(++t).setHasGenders(), + [TrainerType.POKEFAN]: new TrainerConfig(++t).setHasGenders() + .setPartyTemplates(trainerPartyTemplates.SIX_WEAKER, trainerPartyTemplates.FOUR_WEAK, trainerPartyTemplates.TWO_AVG, trainerPartyTemplates.ONE_STRONG, trainerPartyTemplates.FOUR_WEAK_SAME, trainerPartyTemplates.FIVE_WEAK, trainerPartyTemplates.SIX_WEAKER_SAME), [TrainerType.PRESCHOOLER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.YOUNGSTER).setHasGenders(undefined, 'lass'), [TrainerType.PSYCHIC]: new TrainerConfig(++t).setHasGenders(), [TrainerType.RANGER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.BACKPACKER).setHasGenders(), @@ -250,7 +451,81 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.VETERAN]: new TrainerConfig(++t).setHasGenders().setEncounterBgm(TrainerType.RICH), [TrainerType.WAITER]: new TrainerConfig(++t).setHasGenders().setEncounterBgm(TrainerType.CLERK), [TrainerType.WORKER]: new TrainerConfig(++t).setEncounterBgm(TrainerType.CLERK).setSpeciesFilter(s => s.isOfType(Type.ROCK) || s.isOfType(Type.STEEL)), - [TrainerType.YOUNGSTER]: new TrainerConfig(++t).setHasGenders('Lass', 'lass').setEncounterBgm(TrainerType.YOUNGSTER), - [TrainerType.RIVAL]: new TrainerConfig(++t).setHasGenders(), + [TrainerType.YOUNGSTER]: new TrainerConfig(++t).setHasGenders('Lass', 'lass').setPartyTemplates(trainerPartyTemplates.TWO_WEAKER).setEncounterBgm(TrainerType.YOUNGSTER).setEncounterMessages([ + `Hey, wanna battle?`, + `Are you a new trainer too?`, + `Hey, I haven't seen you before. Let's battle!` + ], [ + `Let's have a battle, shall we?`, + `You look like a new trainer. Let's have a battle!`, + `I don't recognize you. How about a battle?` + ]).setVictoryMessages([ + `Wow! You're strong!`, + `I didn't stand a chance, huh.`, + `I'll find you again when I'm older and beat you!` + ], [ + `That was impressive! I've got a lot to learn.`, + `I didn't think you'd beat me that bad…`, + `I hope we get to have a rematch some day.` + ]), [TrainerType.CYNTHIA]: new TrainerConfig(++t), + [TrainerType.RIVAL]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival').setPartyTemplates(trainerPartyTemplates.RIVAL).setEncounterMessages([ + `There you are! I've been looking everywhere for you!\nDid you forget to say goodbye to your best friend? + $So you're finally pursuing your dream, huh?\nI knew you'd do it one day… + $Anyway, I'll forgive you for forgetting me, but on one condition. You have to battle me! + $You'd better give it your best! Wouldn't want your adventure to be over before it started, right?` + ]).setDefeatMessages([ + `You already have three Pokémon?!\nThat's not fair at all! + $Just kidding! I lost fair and square, and now I know you'll do fine out there. + $Do your best like always! I believe in you!` + ]).setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE ])), + [TrainerType.RIVAL_2]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival').setPartyTemplates(trainerPartyTemplates.RIVAL_2).setEncounterMessages([ + `Oh, fancy meeting you here. Looks like you're still undefeated. Right on! + $I know what you're thinking, and no, I wasn't following you. I just happened to be in the area. + $I'm happy for you but I just want to let you know that it's OK to lose sometimes. + $We learn from our mistakes, often more than we would if we kept succeeding. + $In any case, I've been training hard for our rematch, so you'd better give it your all!` + ]).setDefeatMessages([ + `I… wasn't supposed to lose that time…` + ]).setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL ])) + .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), + [TrainerType.RIVAL_3]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival').setPartyTemplates(trainerPartyTemplates.RIVAL_3).setEncounterMessages([ + `Long time no see! Still haven't lost, huh.\nYou're starting to get on my nerves. Just kidding! + $But really, I think it's about time you came home.\nYour family and friends miss you, you know. + $I know your dream means a lot to you, but the reality is you're going to lose sooner or later. + $And when you do, I'll be there for you like always.\nNow, let me show you how strong I've become!` + ]).setDefeatMessages([ + `After all that… it wasn't enough…?` + ]).setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT ])) + .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) + .setSpeciesFilter(species => species.baseTotal >= 540), + [TrainerType.RIVAL_4]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival_2').setPartyTemplates(trainerPartyTemplates.RIVAL_4).setEncounterMessages([ + `It's me! You didn't forget about me again did you? + $You made it really far! I'm proud of you.\nBut it looks like it's the end of your journey. + $You've awoken something in me I never knew was there.\nIt seems like all I do now is train. + $I hardly even eat or sleep now, I just train my Pokémon all day, getting stronger every time. + $And now, I've finally reached peak performance.\nI don't think anyone could beat me now. + $And you know what? It's all because of you.\nI don't know whether to thank you or hate you. + $Prepare yourself.` + ]).setDefeatMessages([ + `What…@d{64} what are you?` + ]).setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT ])) + .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) + .setSpeciesFilter(species => species.baseTotal >= 540), + [TrainerType.RIVAL_5]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival_3').setPartyTemplates(trainerPartyTemplates.RIVAL_5).setEncounterMessages([ `…` ]).setDefeatMessages([ '…' ]) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT ])) + .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) + .setSpeciesFilter(species => species.baseTotal >= 540) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ])), + [TrainerType.RIVAL_6]: new TrainerConfig(++t).setStaticParty().setBattleBgm('battle_rival_3').setPartyTemplates(trainerPartyTemplates.RIVAL_6) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT ])) + .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT ])) + .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) + .setSpeciesFilter(species => species.baseTotal >= 540) + .setPartyMemberFunc(5, getRandomPartyMemberFunc([ Species.RAYQUAZA ])), } \ No newline at end of file diff --git a/src/data/weather.ts b/src/data/weather.ts index b6a4a01392d..9dae6b4b518 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -316,7 +316,7 @@ export function getRandomWeatherType(biome: Biome): WeatherType { let totalWeight = 0; weatherPool.forEach(w => totalWeight += w.weight); - const rand = Utils.randInt(totalWeight); + const rand = Utils.randSeedInt(totalWeight); let w = 0; for (let weather of weatherPool) { w += weather.weight; diff --git a/src/debug.js b/src/debug.js new file mode 100644 index 00000000000..dadc09f2079 --- /dev/null +++ b/src/debug.js @@ -0,0 +1,6 @@ +function getSession() { + const sessionStr = localStorage.getItem('sessionData'); + if (!sessionStr) + return null; + return JSON.parse(atob(sessionStr)); +} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 70fd060c5dc..77a15487781 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,10 +1,11 @@ import Phaser from 'phaser'; import BattleScene from './battle-scene'; -import SpritePipeline from './pipelines/sprite'; +import * as Utils from './utils'; const config: Phaser.Types.Core.GameConfig = { type: Phaser.WEBGL, parent: 'app', + seed: [ Utils.randomString(16) ], scale: { width: 1920, height: 1080, diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 568cc9b55a2..88c5f765634 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -39,7 +39,7 @@ export class ModifierType { constructor(name: string, description: string, newModifierFunc: NewModifierFunc, iconImage?: string, group?: string, soundName?: string) { this.name = name; this.description = description; - this.iconImage = iconImage || name?.replace(/[ \-]/g, '_')?.replace(/'/g, '')?.toLowerCase(); + this.iconImage = iconImage || name?.replace(/[ \-]/g, '_')?.replace(/['\.]/g, '')?.toLowerCase(); this.group = group || ''; this.soundName = soundName || 'restore'; this.newModifierFunc = newModifierFunc; @@ -111,7 +111,7 @@ export class PokemonHpRestoreModifierType extends PokemonModifierType { protected restorePercent: integer; constructor(name: string, restorePoints: integer, restorePercent: integer, newModifierFunc?: NewModifierFunc, selectFilter?: PokemonSelectFilter, iconImage?: string, group?: string) { - super(name, restorePoints ? `Restore ${restorePoints} HP or ${restorePercent}% HP for one POKéMON, whichever is higher` : `Restore ${restorePercent}% HP for one POKéMON`, + super(name, restorePoints ? `Restore ${restorePoints} HP or ${restorePercent}% HP for one Pokémon, whichever is higher` : `Restore ${restorePercent}% HP for one Pokémon`, newModifierFunc || ((_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints, this.restorePercent, false)), selectFilter || ((pokemon: PlayerPokemon) => { if (!pokemon.hp || pokemon.hp >= pokemon.getMaxHp()) @@ -126,14 +126,14 @@ export class PokemonHpRestoreModifierType extends PokemonModifierType { export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { constructor(name: string, restorePercent: integer, iconImage?: string) { - super(name, 0, 100, (_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, 0, this.restorePercent, true), + super(name, 0, restorePercent, (_type, args) => new Modifiers.PokemonHpRestoreModifier(this, (args[0] as PlayerPokemon).id, 0, this.restorePercent, true), ((pokemon: PlayerPokemon) => { if (!pokemon.isFainted()) return PartyUiHandler.NoEffectMessage; return null; }), iconImage, 'revive'); - this.description = `Revive one POKéMON and restore ${restorePercent}% HP`; + this.description = `Revive one Pokémon and restore ${restorePercent}% HP`; this.selectFilter = (pokemon: PlayerPokemon) => { if (pokemon.hp) return PartyUiHandler.NoEffectMessage; @@ -144,7 +144,7 @@ export class PokemonReviveModifierType extends PokemonHpRestoreModifierType { export class PokemonStatusHealModifierType extends PokemonModifierType { constructor(name: string) { - super(name, `Heal any status ailment for one POKéMON`, + super(name, `Heal any status ailment for one Pokémon`, ((_type, args) => new Modifiers.PokemonStatusHealModifier(this, (args[0] as PlayerPokemon).id)), ((pokemon: PlayerPokemon) => { if (!pokemon.hp || !pokemon.status) @@ -169,7 +169,7 @@ export class PokemonPpRestoreModifierType extends PokemonMoveModifierType { protected restorePoints: integer; constructor(name: string, restorePoints: integer, iconImage?: string) { - super(name, `Restore ${restorePoints > -1 ? restorePoints : 'all'} PP for one POKéMON move`, (_type, args) => new Modifiers.PokemonPpRestoreModifier(this, (args[0] as PlayerPokemon).id, (args[1] as integer), this.restorePoints), + super(name, `Restore ${restorePoints > -1 ? restorePoints : 'all'} PP for one Pokémon move`, (_type, args) => new Modifiers.PokemonPpRestoreModifier(this, (args[0] as PlayerPokemon).id, (args[1] as integer), this.restorePoints), (_pokemon: PlayerPokemon) => { return null; }, (pokemonMove: PokemonMove) => { @@ -186,7 +186,7 @@ export class PokemonAllMovePpRestoreModifierType extends PokemonModifierType { protected restorePoints: integer; constructor(name: string, restorePoints: integer, iconImage?: string) { - super(name, `Restore ${restorePoints > -1 ? restorePoints : 'all'} PP for all of one POKéMON's moves`, (_type, args) => new Modifiers.PokemonAllMovePpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints), + super(name, `Restore ${restorePoints > -1 ? restorePoints : 'all'} PP for all of one Pokémon's moves`, (_type, args) => new Modifiers.PokemonAllMovePpRestoreModifier(this, (args[0] as PlayerPokemon).id, this.restorePoints), (pokemon: PlayerPokemon) => { if (!pokemon.getMoveset().filter(m => m.ppUsed).length) return PartyUiHandler.NoEffectMessage; @@ -212,7 +212,7 @@ export class TempBattleStatBoosterModifierType extends ModifierType implements G public tempBattleStat: TempBattleStat; constructor(tempBattleStat: TempBattleStat) { - super(Utils.toPokemonUpperCase(getTempBattleStatBoosterItemName(tempBattleStat)), + super(getTempBattleStatBoosterItemName(tempBattleStat), `Increases the ${getTempBattleStatName(tempBattleStat)} of all party members by 1 stage for 5 battles`, (_type, _args) => new Modifiers.TempBattleStatBoosterModifier(this, this.tempBattleStat), getTempBattleStatBoosterItemName(tempBattleStat).replace(/\./g, '').replace(/[ ]/g, '_').toLowerCase()); @@ -287,7 +287,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i public boostPercent: integer; constructor(moveType: Type, boostPercent: integer) { - super(Utils.toPokemonUpperCase(getAttackTypeBoosterItemName(moveType)), `Inceases the power of a POKéMON's ${Type[moveType]}-type moves by 20%`, + super(getAttackTypeBoosterItemName(moveType), `Inceases the power of a Pokémon's ${Utils.toReadableString(Type[moveType])}-type moves by 20%`, (_type, args) => new Modifiers.AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent), `${getAttackTypeBoosterItemName(moveType).replace(/[ \-]/g, '_').toLowerCase()}`); @@ -302,7 +302,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i export class PokemonLevelIncrementModifierType extends PokemonModifierType { constructor(name: string, iconImage?: string) { - super(name, `Increase a POKéMON\'s level by 1`, (_type, args) => new Modifiers.PokemonLevelIncrementModifier(this, (args[0] as PlayerPokemon).id), + super(name, `Increase a Pokémon\'s level by 1`, (_type, args) => new Modifiers.PokemonLevelIncrementModifier(this, (args[0] as PlayerPokemon).id), (_pokemon: PlayerPokemon) => null, iconImage); } } @@ -316,17 +316,17 @@ export class AllPokemonLevelIncrementModifierType extends ModifierType { function getBaseStatBoosterItemName(stat: Stat) { switch (stat) { case Stat.HP: - return 'HP-UP'; + return 'Hp-Up'; case Stat.ATK: - return 'PROTEIN'; + return 'Protein'; case Stat.DEF: - return 'IRON'; + return 'Iron'; case Stat.SPATK: - return 'CALCIUM'; + return 'Calcium'; case Stat.SPDEF: - return 'ZINC'; + return 'Zinc'; case Stat.SPD: - return 'CARBOS'; + return 'Carbos'; } } @@ -346,13 +346,13 @@ export class PokemonBaseStatBoosterModifierType extends PokemonHeldItemModifierT class AllPokemonFullHpRestoreModifierType extends ModifierType { constructor(name: string, description?: string, newModifierFunc?: NewModifierFunc, iconImage?: string) { - super(name, description || `Restore 100% HP for all POKéMON`, newModifierFunc || ((_type, _args) => new Modifiers.PokemonHpRestoreModifier(this, -1, 0, 100)), iconImage); + super(name, description || `Restore 100% HP for all Pokémon`, newModifierFunc || ((_type, _args) => new Modifiers.PokemonHpRestoreModifier(this, -1, 0, 100)), iconImage); } } class AllPokemonFullReviveModifierType extends AllPokemonFullHpRestoreModifierType { constructor(name: string, iconImage?: string) { - super(name, `Revives all fainted POKéMON, restoring 100% HP`, (_type, _args) => new Modifiers.PokemonHpRestoreModifier(this, -1, 0, 100, true), iconImage); + super(name, `Revives all fainted Pokémon, restoring 100% HP`, (_type, _args) => new Modifiers.PokemonHpRestoreModifier(this, -1, 0, 100, true), iconImage); } } @@ -373,7 +373,7 @@ export class TmModifierType extends PokemonModifierType { public moveId: Moves; constructor(moveId: Moves) { - super(`TM${Utils.padInt(Object.keys(tmSpecies).indexOf(moveId.toString()) + 1, 3)} - ${allMoves[moveId].name}`, `Teach ${allMoves[moveId].name} to a POKéMON`, (_type, args) => new Modifiers.TmModifier(this, (args[0] as PlayerPokemon).id), + super(`TM${Utils.padInt(Object.keys(tmSpecies).indexOf(moveId.toString()) + 1, 3)} - ${allMoves[moveId].name}`, `Teach ${allMoves[moveId].name} to a Pokémon`, (_type, args) => new Modifiers.TmModifier(this, (args[0] as PlayerPokemon).id), (pokemon: PlayerPokemon) => { if (pokemon.compatibleTms.indexOf(moveId) === -1 || pokemon.getMoveset().filter(m => m?.moveId === moveId).length) return PartyUiHandler.NoEffectMessage; @@ -415,7 +415,7 @@ export class EvolutionItemModifierType extends PokemonModifierType implements Ge public evolutionItem: EvolutionItem; constructor(evolutionItem: EvolutionItem) { - super(getEvolutionItemName(evolutionItem), `Causes certain POKéMON to evolve`, (_type, args) => new Modifiers.EvolutionItemModifier(this, (args[0] as PlayerPokemon).id), + super(getEvolutionItemName(evolutionItem), `Causes certain Pokémon to evolve`, (_type, args) => new Modifiers.EvolutionItemModifier(this, (args[0] as PlayerPokemon).id), (pokemon: PlayerPokemon) => { if (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions[pokemon.species.speciesId].filter(e => e.item === this.evolutionItem && (!e.condition || e.condition.predicate(pokemon))).length) @@ -457,7 +457,7 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { let type: Type; - const randInt = Utils.randInt(totalWeight); + const randInt = Utils.randSeedInt(totalWeight); let weight = 0; for (let t of attackMoveTypeWeights.keys()) { @@ -481,7 +481,7 @@ class TmModifierTypeGenerator extends ModifierTypeGenerator { const tierUniqueCompatibleTms = partyMemberCompatibleTms.flat().filter(tm => tmPoolTiers[tm] === tier).filter((tm, i, array) => array.indexOf(tm) === i); if (!tierUniqueCompatibleTms.length) return null; - const randTmIndex = Utils.randInt(tierUniqueCompatibleTms.length); + const randTmIndex = Utils.randSeedInt(tierUniqueCompatibleTms.length); return new TmModifierType(tierUniqueCompatibleTms[randTmIndex]); }); } @@ -501,7 +501,7 @@ class EvolutionItemModifierTypeGenerator extends ModifierTypeGenerator { if (!evolutionItemPool.length) return null; - return new EvolutionItemModifierType(evolutionItemPool[Utils.randInt(evolutionItemPool.length)]); + return new EvolutionItemModifierType(evolutionItemPool[Utils.randSeedInt(evolutionItemPool.length)]); }); } } @@ -542,42 +542,42 @@ const modifierTypes = { ULTRA_BALL: () => new AddPokeballModifierType(PokeballType.ULTRA_BALL, 5, 'ub'), MASTER_BALL: () => new AddPokeballModifierType(PokeballType.MASTER_BALL, 1, 'mb'), - RARE_CANDY: () => new PokemonLevelIncrementModifierType('RARE CANDY'), - RARER_CANDY: () => new AllPokemonLevelIncrementModifierType('RARER CANDY'), + RARE_CANDY: () => new PokemonLevelIncrementModifierType('Rare Candy'), + RARER_CANDY: () => new AllPokemonLevelIncrementModifierType('Rarer Candy'), EVOLUTION_ITEM: () => new EvolutionItemModifierTypeGenerator(), - MAP: () => new ModifierType('MAP', 'Allows you to choose your destination at a crossroads', (type, _args) => new Modifiers.MapModifier(type)), + MAP: () => new ModifierType('Map', 'Allows you to choose your destination at a crossroads', (type, _args) => new Modifiers.MapModifier(type)), - POTION: () => new PokemonHpRestoreModifierType('POTION', 20, 10), - SUPER_POTION: () => new PokemonHpRestoreModifierType('SUPER POTION', 50, 25), - HYPER_POTION: () => new PokemonHpRestoreModifierType('HYPER POTION', 200, 50), - MAX_POTION: () => new PokemonHpRestoreModifierType('MAX POTION', 100, 100), + POTION: () => new PokemonHpRestoreModifierType('Potion', 20, 10), + SUPER_POTION: () => new PokemonHpRestoreModifierType('Super Potion', 50, 25), + HYPER_POTION: () => new PokemonHpRestoreModifierType('Hyper Potion', 200, 50), + MAX_POTION: () => new PokemonHpRestoreModifierType('Max Potion', 100, 100), - REVIVE: () => new PokemonReviveModifierType('REVIVE', 50), - MAX_REVIVE: () => new PokemonReviveModifierType('MAX REVIVE', 100), + REVIVE: () => new PokemonReviveModifierType('Revive', 50), + MAX_REVIVE: () => new PokemonReviveModifierType('Max Revive', 100), - FULL_HEAL: () => new PokemonStatusHealModifierType('FULL HEAL'), + FULL_HEAL: () => new PokemonStatusHealModifierType('Full Heal'), - SACRED_ASH: () => new AllPokemonFullReviveModifierType('SACRED ASH'), + SACRED_ASH: () => new AllPokemonFullReviveModifierType('Sacred Ash'), - REVIVER_SEED: () => new PokemonHeldItemModifierType('REVIVER SEED', 'Revives the holder for 1/2 HP upon fainting', + REVIVER_SEED: () => new PokemonHeldItemModifierType('Reviver Seed', 'Revives the holder for 1/2 HP upon fainting', (type, args) => new Modifiers.PokemonInstantReviveModifier(type, (args[0] as Pokemon).id)), - ETHER: () => new PokemonPpRestoreModifierType('ETHER', 10), - MAX_ETHER: () => new PokemonPpRestoreModifierType('MAX ETHER', -1), + ETHER: () => new PokemonPpRestoreModifierType('Ether', 10), + MAX_ETHER: () => new PokemonPpRestoreModifierType('Max Ether', -1), - ELIXIR: () => new PokemonAllMovePpRestoreModifierType('ELIXIR', 10), - MAX_ELIXIR: () => new PokemonAllMovePpRestoreModifierType('MAX ELIXIR', -1), + ELIXIR: () => new PokemonAllMovePpRestoreModifierType('Elixir', 10), + MAX_ELIXIR: () => new PokemonAllMovePpRestoreModifierType('Max Elixir', -1), - LURE: () => new DoubleBattleChanceBoosterModifierType('LURE', 5), - SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType('SUPER LURE', 10), - MAX_LURE: () => new DoubleBattleChanceBoosterModifierType('MAX LURE', 25), + LURE: () => new DoubleBattleChanceBoosterModifierType('Lure', 5), + SUPER_LURE: () => new DoubleBattleChanceBoosterModifierType('Super Lure', 10), + MAX_LURE: () => new DoubleBattleChanceBoosterModifierType('Max Lure', 25), TEMP_STAT_BOOSTER: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => { if (pregenArgs) return new TempBattleStatBoosterModifierType(pregenArgs[0] as TempBattleStat); - const randTempBattleStat = Utils.randInt(7) as TempBattleStat; + const randTempBattleStat = Utils.randSeedInt(7) as TempBattleStat; return new TempBattleStatBoosterModifierType(randTempBattleStat); }), @@ -586,7 +586,7 @@ const modifierTypes = { const stat = pregenArgs[0] as Stat; return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(stat), stat); } - const randStat = Utils.randInt(6) as Stat; + const randStat = Utils.randSeedInt(6) as Stat; return new PokemonBaseStatBoosterModifierType(getBaseStatBoosterItemName(randStat), randStat); }), @@ -597,13 +597,13 @@ const modifierTypes = { return new BerryModifierType(pregenArgs[0] as BerryType); const berryTypes = Utils.getEnumValues(BerryType); let randBerryType: BerryType; - let rand = Utils.randInt(10); + let rand = Utils.randSeedInt(10); if (rand < 2) randBerryType = BerryType.SITRUS; else if (rand < 4) randBerryType = BerryType.LUM; else - randBerryType = berryTypes[Utils.randInt(berryTypes.length - 2) + 2]; + randBerryType = berryTypes[Utils.randSeedInt(berryTypes.length - 2) + 2]; return new BerryModifierType(randBerryType); }), @@ -611,48 +611,48 @@ const modifierTypes = { TM_GREAT: () => new TmModifierTypeGenerator(ModifierTier.GREAT), TM_ULTRA: () => new TmModifierTypeGenerator(ModifierTier.ULTRA), - EXP_SHARE: () => new ModifierType('EXP. SHARE', 'All POKéMON in your party gain an additional 10% of a battle\'s EXP. Points', - (type, _args) => new Modifiers.ExpShareModifier(type), 'exp_share'), - EXP_BALANCE: () => new ModifierType('EXP. BALANCE', 'All EXP. Points received from battles are split between the lower leveled party members', - (type, _args) => new Modifiers.ExpBalanceModifier(type), 'exp_balance'), + EXP_SHARE: () => new ModifierType('EXP. Share', 'All Pokémon in your party gain an additional 10% of a battle\'s EXP. Points', + (type, _args) => new Modifiers.ExpShareModifier(type)), + EXP_BALANCE: () => new ModifierType('EXP. Balance', 'All EXP. Points received from battles are split between the lower leveled party members', + (type, _args) => new Modifiers.ExpBalanceModifier(type)), - OVAL_CHARM: () => new ModifierType('OVAL CHARM', 'When multiple POKéMON participate in a battle, each gets an extra 10% of the total EXP', - (type, _args) => new Modifiers.MultipleParticipantExpBonusModifier(type), 'oval_charm'), + OVAL_CHARM: () => new ModifierType('Oval Charm', 'When multiple Pokémon participate in a battle, each gets an extra 10% of the total EXP', + (type, _args) => new Modifiers.MultipleParticipantExpBonusModifier(type)), - EXP_CHARM: () => new ExpBoosterModifierType('EXP CHARM', 25), - GOLDEN_EXP_CHARM: () => new ExpBoosterModifierType('GOLDEN EXP CHARM', 50), + EXP_CHARM: () => new ExpBoosterModifierType('EXP. Charm', 25), + GOLDEN_EXP_CHARM: () => new ExpBoosterModifierType('Golden EXP. Charm', 50), - LUCKY_EGG: () => new PokemonExpBoosterModifierType('LUCKY EGG', 50), - GOLDEN_EGG: () => new PokemonExpBoosterModifierType('GOLDEN EGG', 200), + LUCKY_EGG: () => new PokemonExpBoosterModifierType('Lucky Egg', 50), + GOLDEN_EGG: () => new PokemonExpBoosterModifierType('Golden Egg', 200), - GRIP_CLAW: () => new ContactHeldItemTransferChanceModifierType('GRIP CLAW', 10), + GRIP_CLAW: () => new ContactHeldItemTransferChanceModifierType('Grip Claw', 10), - HEALING_CHARM: () => new ModifierType('HEALING CHARM', 'Increases the effectiveness of HP restoring moves and items by 100% (excludes revives)', + HEALING_CHARM: () => new ModifierType('Healing Charm', 'Increases the effectiveness of HP restoring moves and items by 100% (excludes revives)', (type, _args) => new Modifiers.HealingBoosterModifier(type, 2), 'healing_charm'), - CANDY_JAR: () => new ModifierType('CANDY JAR', 'Increases the number of levels added by RARE CANDY items by 1', (type, _args) => new Modifiers.LevelIncrementBoosterModifier(type)), + CANDY_JAR: () => new ModifierType('Candy Jar', 'Increases the number of levels added by RARE CANDY items by 1', (type, _args) => new Modifiers.LevelIncrementBoosterModifier(type)), - BERRY_POUCH: () => new ModifierType('BERRY POUCH', 'Adds a 25% chance that a used berry will not be consumed', + BERRY_POUCH: () => new ModifierType('Berry Pouch', 'Adds a 25% chance that a used berry will not be consumed', (type, _args) => new Modifiers.PreserveBerryModifier(type)), - FOCUS_BAND: () => new PokemonHeldItemModifierType('FOCUS BAND', 'Adds a 10% chance to survive with 1 HP after being damaged enough to faint', + FOCUS_BAND: () => new PokemonHeldItemModifierType('Focus Band', 'Adds a 10% chance to survive with 1 HP after being damaged enough to faint', (type, args) => new Modifiers.SurviveDamageModifier(type, (args[0] as Pokemon).id)), - KINGS_ROCK: () => new PokemonHeldItemModifierType('KING\'S ROCK', 'Adds a 10% chance an attack move will cause the opponent to flinch', + KINGS_ROCK: () => new PokemonHeldItemModifierType('King\'s Rock', 'Adds a 10% chance an attack move will cause the opponent to flinch', (type, args) => new Modifiers.FlinchChanceModifier(type, (args[0] as Pokemon).id)), - LEFTOVERS: () => new PokemonHeldItemModifierType('LEFTOVERS', 'Heals 1/16 of a POKéMON\'s maximum HP every turn', + LEFTOVERS: () => new PokemonHeldItemModifierType('Leftovers', 'Heals 1/16 of a Pokémon\'s maximum HP every turn', (type, args) => new Modifiers.TurnHealModifier(type, (args[0] as Pokemon).id)), - SHELL_BELL: () => new PokemonHeldItemModifierType('SHELL BELL', 'Heals 1/8 of a POKéMON\'s dealt damage', + SHELL_BELL: () => new PokemonHeldItemModifierType('Shell Bell', 'Heals 1/8 of a Pokémon\'s dealt damage', (type, args) => new Modifiers.HitHealModifier(type, (args[0] as Pokemon).id)), - BATON: () => new PokemonHeldItemModifierType('BATON', 'Allows passing along effects when switching POKéMON, which also bypasses traps', + BATON: () => new PokemonHeldItemModifierType('Baton', 'Allows passing along effects when switching Pokémon, which also bypasses traps', (type, args) => new Modifiers.SwitchEffectTransferModifier(type, (args[0] as Pokemon).id), 'stick'), - SHINY_CHARM: () => new ModifierType('SHINY CHARM', 'Dramatically increases the chance of a wild POKéMON being shiny', (type, _args) => new Modifiers.ShinyRateBoosterModifier(type)), + SHINY_CHARM: () => new ModifierType('Shiny Charm', 'Dramatically increases the chance of a wild Pokémon being shiny', (type, _args) => new Modifiers.ShinyRateBoosterModifier(type)), - MINI_BLACK_HOLE: () => new TurnHeldItemTransferModifierType('MINI BLACK HOLE'), + MINI_BLACK_HOLE: () => new TurnHeldItemTransferModifierType('Mini Black Hole'), - GOLDEN_POKEBALL: () => new ModifierType(`GOLDEN ${getPokeballName(PokeballType.POKEBALL)}`, 'Adds 1 extra item option at the end of every battle', + GOLDEN_POKEBALL: () => new ModifierType(`Golden ${getPokeballName(PokeballType.POKEBALL)}`, 'Adds 1 extra item option at the end of every battle', (type, _args) => new Modifiers.ExtraModifierModifier(type), 'pb_gold', null, 'pb_bounce_1'), }; @@ -846,11 +846,11 @@ function getNewModifierTypeOption(party: Pokemon[], player?: boolean, tier?: Mod if (player === undefined) player = true; if (tier === undefined) { - const tierValue = Utils.randInt(256); + const tierValue = Utils.randSeedInt(256); if (player && tierValue) { const partyShinyCount = party.filter(p => p.shiny).length; const upgradeOdds = Math.floor(32 / Math.max((partyShinyCount * 2), 1)); - upgrade = !Utils.randInt(upgradeOdds); + upgrade = !Utils.randSeedInt(upgradeOdds); } else upgrade = false; tier = (tierValue >= 52 ? ModifierTier.COMMON : tierValue >= 8 ? ModifierTier.GREAT : tierValue >= 1 ? ModifierTier.ULTRA : ModifierTier.MASTER) + (upgrade ? 1 : 0); @@ -858,7 +858,7 @@ function getNewModifierTypeOption(party: Pokemon[], player?: boolean, tier?: Mod const thresholds = Object.keys((player ? modifierPoolThresholds : enemyModifierPoolThresholds)[tier]); const totalWeight = parseInt(thresholds[thresholds.length - 1]); - const value = Utils.randInt(totalWeight); + const value = Utils.randSeedInt(totalWeight); let index: integer; for (let t of thresholds) { let threshold = parseInt(t); diff --git a/src/pokemon.ts b/src/pokemon.ts index 58d65c725e1..fc52fea6da6 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -73,7 +73,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!species.isObtainable() && this.isPlayer()) throw `Cannot create a player Pokemon for species '${species.name}'`; - this.name = Utils.toPokemonUpperCase(species.name); + this.name = species.name; this.species = species; this.battleInfo = this.isPlayer() ? new PlayerBattleInfo(scene) @@ -709,6 +709,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (isCritical) this.scene.queueMessage('A critical hit!'); this.scene.setPhaseQueueSplice(); + damage = Math.min(damage, this.hp); this.damage(damage); source.turnData.damageDealt += damage; this.turnData.attacksReceived.unshift({ move: move.id, result: result as DamageResult, damage: damage, sourceId: source.id }); @@ -963,7 +964,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } resetStatus(): void { - const lastStatus = this.status.effect; + const lastStatus = this.status?.effect; this.status = undefined; if (lastStatus === StatusEffect.SLEEP) { this.setFrameRate(12); @@ -1122,7 +1123,7 @@ export class PlayerPokemon extends Pokemon { return new Promise(resolve => { this.handleSpecialEvolutions(evolution); this.species = getPokemonSpecies(evolution.speciesId); - this.name = this.species.name.toUpperCase(); + this.name = this.species.name; const abilityCount = this.species.getAbilityCount(); if (this.abilityIndex >= abilityCount) // Shouldn't happen this.abilityIndex = abilityCount - 1; diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 39ff24e9c71..9cfaf983bc0 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -24,6 +24,7 @@ interface SystemSaveData { } interface SessionSaveData { + seed: string; gameMode: GameMode; party: PokemonData[]; enemyParty: PokemonData[]; @@ -134,6 +135,7 @@ export class GameData { saveSession(scene: BattleScene): boolean { const sessionData = { + seed: scene.seed, gameMode: scene.gameMode, party: scene.getParty().map(p => new PokemonData(p)), enemyParty: scene.getEnemyParty().map(p => new PokemonData(p)), @@ -191,6 +193,9 @@ export class GameData { console.debug(sessionData); + scene.seed = sessionData.seed || this.scene.game.config.seed[0]; + scene.resetSeed(); + scene.gameMode = sessionData.gameMode || GameMode.CLASSIC; const loadPokemonAssets: Promise[] = []; @@ -213,11 +218,11 @@ export class GameData { if (sessionData.enemyField) sessionData.enemyParty = sessionData.enemyField; - scene.newArena(sessionData.arena.biome, true); - const battleType = sessionData.battleType || 0; const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfigs[sessionData.trainer.trainerType].isDouble : sessionData.enemyParty.length > 1); + scene.newArena(sessionData.arena.biome, true); + sessionData.enemyParty.forEach((enemyData, e) => { const enemyPokemon = enemyData.toPokemon(scene) as EnemyPokemon; battle.enemyParty[e] = enemyPokemon; @@ -334,7 +339,7 @@ export class GameData { if (newCatch && !hasPrevolution) { this.scene.playSoundWithoutBgm('level_up_fanfare', 1500); - this.scene.ui.showText(`${species.name.toUpperCase()} has been\nadded as a starter!`, null, () => resolve(), null, true); + this.scene.ui.showText(`${species.name} has been\nadded as a starter!`, null, () => resolve(), null, true); return; } } diff --git a/src/system/game-speed.ts b/src/system/game-speed.ts index eadfccc595d..edd2ea74aa8 100644 --- a/src/system/game-speed.ts +++ b/src/system/game-speed.ts @@ -1,6 +1,12 @@ +import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; +import FadeIn from 'phaser3-rex-plugins/plugins/audio/fade/FadeIn'; +import FadeOut from 'phaser3-rex-plugins/plugins/audio/fade/FadeOut'; import BattleScene from "../battle-scene"; import * as Utils from "../utils"; +type FadeIn = typeof FadeIn; +type FadeOut = typeof FadeOut; + export function initGameSpeed() { const thisArg = this as BattleScene; @@ -32,4 +38,21 @@ export function initGameSpeed() { config.delay = transformValue(config.delay); return originalAddCounter.apply(this, [ config ]); }; + + const originalFadeOut = SoundFade.fadeOut; + SoundFade.fadeOut = (( + scene: Phaser.Scene, + sound: Phaser.Sound.BaseSound, + duration: number, + destroy?: boolean + ) => originalFadeOut(scene, sound, thisArg.gameSpeed === 1 ? duration : Math.ceil(duration /= thisArg.gameSpeed), destroy)) as FadeOut; + + const originalFadeIn = SoundFade.fadeIn; + SoundFade.fadeIn = (( + scene: Phaser.Scene, + sound: string | Phaser.Sound.BaseSound, + duration: number, + endVolume?: number, + startVolume?: number + ) => originalFadeIn(scene, sound, thisArg.gameSpeed === 1 ? duration : Math.ceil(duration /= thisArg.gameSpeed), endVolume, startVolume)) as FadeIn; } \ No newline at end of file diff --git a/src/system/trainer-data.ts b/src/system/trainer-data.ts index e03f8884bfe..5df18ca8b26 100644 --- a/src/system/trainer-data.ts +++ b/src/system/trainer-data.ts @@ -5,14 +5,16 @@ import Trainer from "../trainer"; export default class TrainerData { public trainerType: TrainerType; public female: boolean; + public partyTemplateIndex: integer; constructor(source: Trainer | any) { const sourceTrainer = source instanceof Trainer ? source as Trainer : null; this.trainerType = sourceTrainer ? sourceTrainer.config.trainerType : source.trainerType; this.female = source.female; + this.partyTemplateIndex = source.partyMemberTemplateIndex; } toTrainer(scene: BattleScene): Trainer { - return new Trainer(scene, this.trainerType, this.female); + return new Trainer(scene, this.trainerType, this.female, this.partyTemplateIndex); } } \ No newline at end of file diff --git a/src/system/unlockables.ts b/src/system/unlockables.ts index aa0e85dd093..c5e254572a0 100644 --- a/src/system/unlockables.ts +++ b/src/system/unlockables.ts @@ -8,6 +8,6 @@ export function getUnlockableName(unlockable: Unlockables) { case Unlockables.ENDLESS_MODE: return 'Endless Mode'; case Unlockables.MINI_BLACK_HOLE: - return 'MINI BLACK HOLE'; + return 'Mini Black Hole'; } } \ No newline at end of file diff --git a/src/trainer.ts b/src/trainer.ts index c3ba63cec5e..68c3b804086 100644 --- a/src/trainer.ts +++ b/src/trainer.ts @@ -1,16 +1,21 @@ import BattleScene from "./battle-scene"; import PokemonSpecies, { getPokemonSpecies } from "./data/pokemon-species"; -import { TrainerConfig, TrainerPartyType, TrainerType, trainerConfigs } from "./data/trainer-type"; +import { TrainerConfig, TrainerPartyCompoundTemplate, TrainerPartyMemberStrength, TrainerPartyTemplate, TrainerPoolTier, TrainerType, trainerConfigs, trainerPartyTemplates } from "./data/trainer-type"; +import { EnemyPokemon } from "./pokemon"; import * as Utils from "./utils"; export default class Trainer extends Phaser.GameObjects.Container { public config: TrainerConfig; public female: boolean; + public partyTemplateIndex: integer; - constructor(scene: BattleScene, trainerType: TrainerType, female?: boolean) { + constructor(scene: BattleScene, trainerType: TrainerType, female?: boolean, partyTemplateIndex?: integer) { super(scene, -72, 80); this.config = trainerConfigs[trainerType]; this.female = female; + this.partyTemplateIndex = Math.min(partyTemplateIndex !== undefined ? partyTemplateIndex : Phaser.Math.RND.weightedPick(this.config.partyTemplates.map((_, i) => i)), this.config.partyTemplates.length - 1); + + console.log(Object.keys(trainerPartyTemplates)[Object.values(trainerPartyTemplates).indexOf(this.getPartyTemplate())]); const getSprite = (hasShadow?: boolean) => { const ret = this.scene.add.sprite(0, 0, this.getKey()); @@ -36,16 +41,116 @@ export default class Trainer extends Phaser.GameObjects.Container { return this.config.getName(this.female); } - genPartyMemberSpecies(level: integer, attempt?: integer): PokemonSpecies { - const battle = this.scene.currentBattle; + getBattleBgm(): string { + return this.config.battleBgm; + } - if (this.config.partyType === TrainerPartyType.REPEATED && battle.enemyParty.length) - return getPokemonSpecies(battle.enemyParty[0].species.getSpeciesForLevel(level)); - const ret = getPokemonSpecies(this.scene.randomSpecies(battle.waveIndex, level, this.config.speciesFilter, true).getSpeciesForLevel(level)); - if (this.config.partyType === TrainerPartyType.BALANCED) { - const partyTypes = this.scene.getEnemyParty().map(p => p.getTypes()).flat(); - if ((attempt || 0) < 10 && (partyTypes.indexOf(ret.type1) > -1 || (ret.type2 !== null && partyTypes.indexOf(ret.type2) > -1))) - return this.genPartyMemberSpecies(level, (attempt || 0) + 1); + getEncounterBgm(): string { + return !this.female ? this.config.encounterBgm : this.config.femaleEncounterBgm || this.config.encounterBgm; + } + + getPartyTemplate(): TrainerPartyTemplate { + return this.config.partyTemplates[this.partyTemplateIndex]; + } + + getPartyLevels(waveIndex: integer): integer[] { + const ret = []; + const partyTemplate = this.getPartyTemplate(); + + let baseLevel = 1 + waveIndex / 2 + Math.pow(waveIndex / 25, 2); + + for (let i = 0; i < partyTemplate.size; i++) { + let multiplier = 1; + + const strength = partyTemplate.getStrength(i) + + switch (strength) { + case TrainerPartyMemberStrength.WEAKER: + multiplier = 0.95; + break; + case TrainerPartyMemberStrength.WEAK: + multiplier = 1.0; + break; + case TrainerPartyMemberStrength.AVERAGE: + multiplier = 1.1; + break; + case TrainerPartyMemberStrength.STRONG: + multiplier = 1.2; + break; + case TrainerPartyMemberStrength.STRONGER: + multiplier = 1.25; + break; + } + + let level = Math.ceil(baseLevel * multiplier); + if (strength < TrainerPartyMemberStrength.STRONG) { + const minLevel = Math.ceil(baseLevel * 1.2) - Math.floor(waveIndex / 25); + if (level < minLevel) + level = minLevel; + } + + ret.push(level); + } + + return ret; + } + + genPartyMember(index: integer): EnemyPokemon { + const battle = this.scene.currentBattle; + const level = battle.enemyLevels[index]; + + let ret: EnemyPokemon; + + this.scene.executeWithSeedOffset(() => { + if (this.config.partyMemberFuncs.hasOwnProperty(index)) { + ret = this.config.partyMemberFuncs[index](this.scene, level); + return; + } + + const template = this.getPartyTemplate(); + + let offset = 0; + + if (template instanceof TrainerPartyCompoundTemplate) { + for (let innerTemplate of template.templates) { + if (offset + innerTemplate.size > index) + break; + offset += innerTemplate.size; + } + } + + const species = template.isSameSpecies(index) && index > offset + ? getPokemonSpecies(battle.enemyParty[offset].species.getSpeciesForLevel(level)) + : this.genNewPartyMemberSpecies(level); + + ret = new EnemyPokemon(this.scene, species, level); + }, this.config.staticParty ? this.config.getDerivedType() + ((index + 1) << 8) : this.scene.currentBattle.waveIndex + (this.config.getDerivedType() << 10) + ((index + 1) << 8)); + + return ret; + } + + genNewPartyMemberSpecies(level: integer, attempt?: integer): PokemonSpecies { + const battle = this.scene.currentBattle; + const template = this.getPartyTemplate(); + + let ret: PokemonSpecies; + if (this.config.speciesPools) { + const tierValue = Utils.randSeedInt(512); + let tier = tierValue >= 156 ? TrainerPoolTier.COMMON : tierValue >= 32 ? TrainerPoolTier.UNCOMMON : tierValue >= 6 ? TrainerPoolTier.RARE : tierValue >= 1 ? TrainerPoolTier.SUPER_RARE : TrainerPoolTier.ULTRA_RARE + console.log(TrainerPoolTier[tier]); + while (!this.config.speciesPools[tier].length) { + console.log(`Downgraded trainer Pokemon rarity tier from ${TrainerPoolTier[tier]} to ${TrainerPoolTier[tier - 1]}`); + tier--; + } + const tierPool = this.config.speciesPools[tier]; + ret = getPokemonSpecies(getPokemonSpecies(Phaser.Math.RND.pick(tierPool)).getSpeciesForLevel(level)); + } else + ret = getPokemonSpecies(this.scene.randomSpecies(battle.waveIndex, level, this.config.speciesFilter).getSpeciesForLevel(level)); + + if (template.isBalanced(battle.enemyParty.length)) { + const partyMemberTypes = battle.enemyParty.map(p => p.getTypes()).flat(); + if ((attempt || 0) < 10 && (partyMemberTypes.indexOf(ret.type1) > -1 || (ret.type2 !== null && partyMemberTypes.indexOf(ret.type2) > -1))) + ret = this.genNewPartyMemberSpecies(level, (attempt || 0) + 1); } return ret; @@ -57,8 +162,11 @@ export default class Trainer extends Phaser.GameObjects.Container { const partyMemberScores = nonFaintedPartyMembers.map(p => { const playerField = this.scene.getPlayerField(); let score = 0; - for (let playerPokemon of playerField) + for (let playerPokemon of playerField) { score += p.getMatchupScore(playerPokemon); + if (playerPokemon.species.legendary) + score /= 2; + } score /= playerField.length; return [ party.indexOf(p), score ]; }); @@ -71,16 +179,21 @@ export default class Trainer extends Phaser.GameObjects.Container { }); const maxScorePartyMemberIndexes = partyMemberScores.filter(pms => pms[1] === sortedPartyMemberScores[0][1]).map(pms => pms[0]); - return maxScorePartyMemberIndexes[Utils.randInt(maxScorePartyMemberIndexes.length)]; + return maxScorePartyMemberIndexes[Utils.randSeedInt(maxScorePartyMemberIndexes.length)]; } loadAssets(): Promise { return this.config.loadAssets(this.scene, this.female); } + initSprite(): void { + this.getSprite().setTexture(this.getKey()); + this.getTintSprite().setTexture(this.getKey()); + } + playAnim(): void { const trainerAnimConfig = { - key: this.scene.currentBattle.trainer.getKey(), + key: this.getKey(), repeat: 0 }; this.getSprite().play(trainerAnimConfig); @@ -134,7 +247,6 @@ export default class Trainer extends Phaser.GameObjects.Container { } } - export default interface Trainer { scene: BattleScene } \ No newline at end of file diff --git a/src/ui/ball-ui-handler.ts b/src/ui/ball-ui-handler.ts index 0264d9e82cf..f7daf28ca4f 100644 --- a/src/ui/ball-ui-handler.ts +++ b/src/ui/ball-ui-handler.ts @@ -32,7 +32,7 @@ export default class BallUiHandler extends UiHandler { for (let pb = 0; pb < Object.keys(this.scene.pokeballCounts).length; pb++) optionsTextContent += `${getPokeballName(pb)}\n`; - optionsTextContent += 'CANCEL'; + optionsTextContent += 'Cancel'; const optionsText = addTextObject(this.scene, 0, 0, optionsTextContent, TextStyle.WINDOW, { align: 'right', maxLines: 6 }); optionsText.setOrigin(0, 0); optionsText.setPositionRelative(this.pokeballSelectBg, 42, 9); diff --git a/src/ui/battle-message-ui-handler.ts b/src/ui/battle-message-ui-handler.ts index 0532ca79307..84deb34425e 100644 --- a/src/ui/battle-message-ui-handler.ts +++ b/src/ui/battle-message-ui-handler.ts @@ -9,14 +9,16 @@ export default class BattleMessageUiHandler extends MessageUiHandler { private levelUpStatsContainer: Phaser.GameObjects.Container; private levelUpStatsIncrContent: Phaser.GameObjects.Text; private levelUpStatsValuesContent: Phaser.GameObjects.Text; + private nameText: Phaser.GameObjects.Text; public bg: Phaser.GameObjects.Image; + public nameBoxContainer: Phaser.GameObjects.Container; constructor(scene: BattleScene) { super(scene, Mode.MESSAGE); } - setup() { + setup(): void { const ui = this.getUi(); this.textTimer = null; @@ -41,6 +43,18 @@ export default class BattleMessageUiHandler extends MessageUiHandler { this.message = message; + this.nameBoxContainer = this.scene.add.container(0, -16); + this.nameBoxContainer.setVisible(false); + + const nameBox = this.scene.add.nineslice(0, 0, 'namebox', null, 72, 16, 8, 8, 5, 5); + nameBox.setOrigin(0, 0); + + this.nameText = addTextObject(this.scene, 8, 0, 'Rival', TextStyle.MESSAGE, { maxLines: 1 }); + + this.nameBoxContainer.add(nameBox); + this.nameBoxContainer.add(this.nameText); + messageContainer.add(this.nameBoxContainer); + const prompt = this.scene.add.sprite(0, 0, 'prompt'); prompt.setVisible(false); prompt.setOrigin(0, 0); @@ -81,14 +95,14 @@ export default class BattleMessageUiHandler extends MessageUiHandler { this.levelUpStatsValuesContent = levelUpStatsValuesContent; } - show(args: any[]) { + show(args: any[]): void { super.show(args); this.bg.setTexture('bg'); this.message.setWordWrapWidth(1780); } - processInput(button: Button) { + processInput(button: Button): void { const ui = this.getUi(); if (this.awaitingActionInput) { if (button === Button.CANCEL || button === Button.ACTION) { @@ -102,11 +116,21 @@ export default class BattleMessageUiHandler extends MessageUiHandler { } } - clear() { + clear(): void { super.clear(); } - promptLevelUpStats(partyMemberIndex: integer, prevStats: integer[], showTotals: boolean, callback?: Function) { + showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + this.hideNameText(); + super.showText(text, delay, callback, callbackDelay, prompt, promptDelay); + } + + showDialogue(text: string, name: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + this.showNameText(name); + super.showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay); + } + + promptLevelUpStats(partyMemberIndex: integer, prevStats: integer[], showTotals: boolean, callback?: Function): void { const newStats = (this.scene as BattleScene).getParty()[partyMemberIndex].stats; let levelUpStatsValuesText = ''; const stats = Utils.getEnumValues(Stat); @@ -126,4 +150,13 @@ export default class BattleMessageUiHandler extends MessageUiHandler { } }; } + + showNameText(name: string): void { + this.nameBoxContainer.setVisible(true); + this.nameText.setText(name); + } + + hideNameText(): void { + this.nameBoxContainer.setVisible(false); + } } \ No newline at end of file diff --git a/src/ui/command-ui-handler.ts b/src/ui/command-ui-handler.ts index d4c6498303c..7437192b08f 100644 --- a/src/ui/command-ui-handler.ts +++ b/src/ui/command-ui-handler.ts @@ -1,7 +1,6 @@ import { CommandPhase } from "../battle-phases"; import BattleScene, { Button } from "../battle-scene"; import { addTextObject, TextStyle } from "./text"; -import { toPokemonUpperCase } from "../utils"; import PartyUiHandler, { PartyUiMode } from "./party-ui-handler"; import UI, { Mode } from "./ui"; import UiHandler from "./uiHandler"; @@ -23,7 +22,7 @@ export default class CommandUiHandler extends UiHandler { setup() { const ui = this.getUi(); - const commands = [ 'Fight', 'Ball', 'Pokémon', 'Run' ].map(s => toPokemonUpperCase(s)); + const commands = [ 'Fight', 'Ball', 'Pokémon', 'Run' ]; this.commandsContainer = this.scene.add.container(216, -38.7); this.commandsContainer.setVisible(false); diff --git a/src/ui/message-ui-handler.ts b/src/ui/message-ui-handler.ts index 7086a793e9a..39742483615 100644 --- a/src/ui/message-ui-handler.ts +++ b/src/ui/message-ui-handler.ts @@ -18,6 +18,14 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler { } showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay); + } + + showDialogue(text: string, name: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) { + this.showTextInternal(text, delay, callback, callbackDelay, prompt, promptDelay); + } + + private showTextInternal(text: string, delay: integer, callback: Function, callbackDelay: integer, prompt: boolean, promptDelay: integer) { if (delay === null || delay === undefined) delay = 20; let delayMap = new Map(); diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index f6d3208b523..6d37c0de188 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -188,7 +188,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler { ui.showText(this.options[this.cursor].modifierTypeOption.type.description); } else { this.cursorObj.setPosition((this.scene.game.canvas.width / 6) - 50, -60); - ui.showText('Transfer a held item from one POKéMON to another instead of selecting an item'); + ui.showText('Transfer a held item from one Pokémon to another instead of selecting an item'); } return ret; diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index fbc799cf91d..8212c669ce3 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -249,7 +249,7 @@ export default class PartyUiHandler extends MessageUiHandler { }); }); } else - this.showText('You can\'t release a POKéMON that\'s in battle!', null, () => this.showText(null, 0), null, true); + this.showText('You can\'t release a Pokémon that\'s in battle!', null, () => this.showText(null, 0), null, true); } else if (option === PartyOption.CANCEL) this.processInput(Button.CANCEL); } else if (button === Button.CANCEL) { @@ -473,7 +473,7 @@ export default class PartyUiHandler extends MessageUiHandler { optionName = pokemon.moveset[option - PartyOption.MOVE_1].getName(); break; default: - optionName = PartyOption[option].replace(/\_/g, ' '); + optionName = Utils.toReadableString(PartyOption[option]); break; } } else { @@ -694,13 +694,13 @@ class PartySlot extends Phaser.GameObjects.Container { let slotTmText: string; switch (true) { case (this.pokemon.compatibleTms.indexOf(tmMoveId) === -1): - slotTmText = 'NOT ABLE'; + slotTmText = 'Not Able'; break; case (this.pokemon.getMoveset().filter(m => m?.moveId === tmMoveId).length > 0): - slotTmText = 'LEARNED'; + slotTmText = 'Learned'; break; default: - slotTmText = 'ABLE'; + slotTmText = 'Able'; break; } @@ -770,7 +770,7 @@ class PartyCancelButton extends Phaser.GameObjects.Container { this.partyCancelPb = partyCancelPb; - const partyCancelText = addTextObject(this.scene, -7, -6, 'CANCEL', TextStyle.PARTY); + const partyCancelText = addTextObject(this.scene, -7, -6, 'Cancel', TextStyle.PARTY); this.add(partyCancelText); } diff --git a/src/ui/pokeball-tray.ts b/src/ui/pokeball-tray.ts new file mode 100644 index 00000000000..3f3a79dab97 --- /dev/null +++ b/src/ui/pokeball-tray.ts @@ -0,0 +1,116 @@ +import BattleScene from "../battle-scene"; +import Pokemon from "../pokemon"; + +export default class PokeballTray extends Phaser.GameObjects.Container { + private player: boolean; + + private bg: Phaser.GameObjects.NineSlice; + private balls: Phaser.GameObjects.Sprite[]; + + public shown: boolean; + + constructor(scene: BattleScene, player: boolean) { + super(scene, player ? (scene.game.canvas.width / 6) : 0, player ? -72 : -144); + this.player = player; + } + + setup(): void { + this.bg = this.scene.add.nineslice(0, 0, `pb_tray_overlay_${this.player ? 'player' : 'enemy'}`, null, 104, 4, 48, 8, 0, 0); + this.bg.setOrigin(this.player ? 1 : 0, 0); + + this.add(this.bg); + + this.balls = new Array(6).fill(null).map((_, i) => this.scene.add.sprite((this.player ? -83 : 76) + (this.scene.game.canvas.width / 6) * (this.player ? -1 : 1) + 10 * i * (this.player ? 1 : -1), -8, 'pb_tray_ball', 'empty')); + + for (let ball of this.balls) { + ball.setOrigin(0, 0); + this.add(ball); + } + + this.setVisible(false); + this.shown = false; + } + + showPbTray(party: Pokemon[]): Promise { + return new Promise(resolve => { + if (this.shown) + return resolve(); + + (this.scene as BattleScene).fieldUI.bringToTop(this); + + this.x += 104 * (this.player ? 1 : -1); + + this.bg.width = 104; + this.bg.alpha = 1; + + this.balls.forEach((ball, b) => { + ball.x += (this.scene.game.canvas.width / 6 + 104) * (this.player ? 1 : -1); + let ballFrame = 'ball'; + if (b >= party.length) + ballFrame = 'empty'; + else if (!party[b].hp) + ballFrame = 'fainted'; + else if (party[b].status) + ballFrame = 'status'; + ball.setFrame(ballFrame); + }); + + this.scene.sound.play('pb_tray_enter'); + + this.scene.tweens.add({ + targets: this, + x: `${this.player ? '-' : '+'}=104`, + duration: 500, + ease: 'Sine.easeIn', + onComplete: () => { + this.balls.forEach((ball, b) => { + this.scene.tweens.add({ + targets: ball, + x: `${this.player ? '-' : '+'}=104`, + duration: b * 100, + ease: 'Sine.easeIn', + onComplete: () => this.scene.sound.play(b < party.length ? 'pb_tray_ball' : 'pb_tray_empty') + }); + }); + } + }); + + this.setVisible(true); + this.shown = true; + + this.scene.time.delayedCall(1100, () => resolve()); + }); + } + + hide(): Promise { + return new Promise(resolve => { + if (!this.shown) + return resolve(); + + this.balls.forEach((ball, b) => { + this.scene.tweens.add({ + targets: ball, + x: `${this.player ? '-' : '+'}=${this.scene.game.canvas.width / 6}`, + duration: 250, + delay: b * 100, + ease: 'Sine.easeIn' + }); + }); + + this.scene.tweens.add({ + targets: this.bg, + width: 144, + alpha: 0, + duration: 500, + ease: 'Sine.easeIn' + }); + + this.scene.time.delayedCall(850, () => { + this.setVisible(false); + resolve(); + }); + + this.shown = false; + }); + }; +} \ No newline at end of file diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 114d62b778b..ff11c674ae1 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -94,7 +94,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonGenderText.setOrigin(0, 0); this.starterSelectContainer.add(this.pokemonGenderText); - this.pokemonAbilityLabelText = addTextObject(this.scene, 6, 126, 'ABILITY:', TextStyle.SUMMARY, { fontSize: '64px' }); + this.pokemonAbilityLabelText = addTextObject(this.scene, 6, 126, 'Ability:', TextStyle.SUMMARY, { fontSize: '64px' }); this.pokemonAbilityLabelText.setOrigin(0, 0); this.pokemonAbilityLabelText.setVisible(false); this.starterSelectContainer.add(this.pokemonAbilityLabelText); @@ -276,7 +276,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.popStarter(); this.clearText(); }; - ui.showText('Begin with these POKéMON?', null, () => { + ui.showText('Begin with these Pokémon?', null, () => { ui.setModeWithoutClear(Mode.CONFIRM, () => { const startRun = (gameMode: GameMode) => { this.scene.gameMode = gameMode; @@ -467,7 +467,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (species && this.speciesStarterDexEntry) { this.pokemonNumberText.setText(Utils.padInt(species.speciesId, 3)); - this.pokemonNameText.setText(species.name.toUpperCase()); + this.pokemonNameText.setText(species.name); this.pokemonAbilityLabelText.setVisible(true); this.setSpeciesDetails(species, !!this.speciesStarterDexEntry?.shiny, this.speciesStarterDexEntry?.formIndex, !!this.speciesStarterDexEntry?.female, this.speciesStarterDexEntry?.abilityIndex); diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index a976387ef0c..aa114135bee 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -109,12 +109,19 @@ export default class SummaryUiHandler extends UiHandler { this.summaryContainer.add(this.genderText); this.moveEffectContainer = this.scene.add.container(106, -62); + this.summaryContainer.add(this.moveEffectContainer); const moveEffectBg = this.scene.add.image(0, 0, 'summary_moves_effect'); moveEffectBg.setOrigin(0, 0); this.moveEffectContainer.add(moveEffectBg); + const moveEffectLabels = addTextObject(this.scene, 8, 12, 'Power\nAccuracy\nCategory', TextStyle.SUMMARY); + moveEffectLabels.setLineSpacing(9); + moveEffectLabels.setOrigin(0, 0); + + this.moveEffectContainer.add(moveEffectLabels); + this.movePowerText = addTextObject(this.scene, 99, 27, '0', TextStyle.WINDOW); this.movePowerText.setOrigin(1, 1); this.moveEffectContainer.add(this.movePowerText); @@ -420,7 +427,7 @@ export default class SummaryUiHandler extends UiHandler { const profileContainer = this.scene.add.container(0, -pageBg.height); pageContainer.add(profileContainer); - const typeLabel = addTextObject(this.scene, 7, 28, 'TYPE/', TextStyle.WINDOW); + const typeLabel = addTextObject(this.scene, 7, 28, 'Type/', TextStyle.WINDOW); typeLabel.setOrigin(0, 0); profileContainer.add(typeLabel); @@ -483,11 +490,11 @@ export default class SummaryUiHandler extends UiHandler { const totalLvExp = getLevelTotalExp(this.pokemon.level, this.pokemon.species.growthRate); const expRatio = this.pokemon.level < this.scene.getMaxExpLevel() ? this.pokemon.levelExp / totalLvExp : 0; - const expLabel = addTextObject(this.scene, 6, 112, 'EXP. POINTS', TextStyle.SUMMARY); + const expLabel = addTextObject(this.scene, 6, 112, 'EXP. Points', TextStyle.SUMMARY); expLabel.setOrigin(0, 0); statsContainer.add(expLabel); - const nextLvExpLabel = addTextObject(this.scene, 6, 128, 'NEXT LV.', TextStyle.SUMMARY); + const nextLvExpLabel = addTextObject(this.scene, 6, 128, 'Next Lv.', TextStyle.SUMMARY); nextLvExpLabel.setOrigin(0, 0); statsContainer.add(nextLvExpLabel); @@ -528,7 +535,7 @@ export default class SummaryUiHandler extends UiHandler { extraRowOverlay.setOrigin(0, 1); this.extraMoveRowContainer.add(extraRowOverlay); - const extraRowText = addTextObject(this.scene, 35, 0, this.summaryUiMode === SummaryUiMode.LEARN_MOVE ? this.newMove.name : 'CANCEL', + const extraRowText = addTextObject(this.scene, 35, 0, this.summaryUiMode === SummaryUiMode.LEARN_MOVE ? this.newMove.name : 'Cancel', this.summaryUiMode === SummaryUiMode.LEARN_MOVE ? TextStyle.SUMMARY_RED : TextStyle.SUMMARY); extraRowText.setOrigin(0, 1); this.extraMoveRowContainer.add(extraRowText); diff --git a/src/ui/text.ts b/src/ui/text.ts index 32169ffe612..e2d88d758e8 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -26,7 +26,6 @@ export function addTextObject(scene: Phaser.Scene, x: number, y: number, content case TextStyle.SUMMARY: case TextStyle.SUMMARY_RED: case TextStyle.SUMMARY_GOLD: - styleOptions.padding = undefined; case TextStyle.WINDOW: case TextStyle.MESSAGE: styleOptions.fontSize = '96px'; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 45dce7ba308..35982bcc61d 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -104,6 +104,14 @@ export default class UI extends Phaser.GameObjects.Container { this.getMessageHandler().showText(text, delay, callback, callbackDelay, prompt, promptDelay); } + showDialogue(text: string, name: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void { + const handler = this.getHandler(); + if (handler instanceof MessageUiHandler) + (handler as MessageUiHandler).showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay); + else + this.getMessageHandler().showDialogue(text, name, delay, callback, callbackDelay, prompt, promptDelay); + } + clearText(): void { const handler = this.getHandler(); if (handler instanceof MessageUiHandler) diff --git a/src/utils.ts b/src/utils.ts index 826194a850f..80c04b02cc7 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,32 @@ -export function toPokemonUpperCase(input: string): string { - return input.replace(/([a-z]+)/g, s => s.toUpperCase()); +export function toReadableString(str: string): string { + return str.replace(/\_/g, ' ').split(' ').map(s => `${s.slice(0, 1)}${s.slice(1).toLowerCase()}`).join(' '); +} + +export function randomString(length: integer) { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * characters.length); + result += characters[randomIndex]; + } + + return result; +} + +export function shiftCharCodes(str: string, shiftCount: integer) { + if (!shiftCount) + shiftCount = 0; + + let newStr = ''; + + for (let i = 0; i < str.length; i++) { + let charCode = str.charCodeAt(i); + let newCharCode = charCode + shiftCount; + newStr += String.fromCharCode(newCharCode); + } + + return newStr; } export function clampInt(value: integer, min: integer, max: integer): integer { @@ -30,6 +57,14 @@ export function randInt(range: integer, min?: integer): integer { return Math.floor(Math.random() * range) + min; } +export function randSeedInt(range: integer, min?: integer): integer { + if (!min) + min = 0; + if (range === 1) + return min; + return Phaser.Math.RND.integerInRange(min, (range - 1) + min); +} + export function randIntRange(min: integer, max: integer): integer { return randInt(max - min, min); }