From a72e76b6fdfdf25f8b60469613b15d29e3502bc8 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Fri, 12 Jul 2024 17:33:55 -0400 Subject: [PATCH 1/3] safari zone --- .../encounters/safari-zone-encounter.ts | 172 ++++++++++++++++++ .../mystery-encounters/mystery-encounters.ts | 11 +- src/enums/mystery-encounter-type.ts | 3 +- 3 files changed, 182 insertions(+), 4 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/safari-zone-encounter.ts diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts new file mode 100644 index 00000000000..e2dd7849a49 --- /dev/null +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -0,0 +1,172 @@ +import { + getHighestLevelPlayerPokemon, + koPlayerPokemon, + leaveEncounterWithoutBattle, + queueEncounterMessage, + setEncounterRewards, + showEncounterText, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { GameOverPhase } from "#app/phases"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:safari_zone"; + +export const SafariZoneEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.SAFARI_ZONE + ) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 + .withHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 8, + disableAnimation: true, // Re-enabled after option select + }, + ]) + .withIntroDialogue([ + { + text: `mysteryEncounter:${namespace}_intro_message`, + }, + ]) + .withTitle(`mysteryEncounter:${namespace}_title`) + .withDescription(`mysteryEncounter:${namespace}_description`) + .withQuery(`mysteryEncounter:${namespace}_query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: "mysteryEncounter:${namespace}_option_1_label", + buttonTooltip: "mysteryEncounter:${namespace}_option_1_tooltip", + selected: [ + { + text: "mysteryEncounter:${namespace}_option_1_selected_message", + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play animation + const introVisuals = + scene.currentBattle.mysteryEncounter.introVisuals; + introVisuals.spriteConfigs[0].disableAnimation = false; + introVisuals.playAnim(); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Open the chest + const roll = randSeedInt(100); + if (roll > 60) { + // Choose between 2 COMMON / 2 GREAT tier items (40%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.COMMON, + ModifierTier.COMMON, + ModifierTier.GREAT, + ModifierTier.GREAT, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:${namespace}_option_1_normal_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 40) { + // Choose between 3 ULTRA tier items (20%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:${namespace}_option_1_good_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 36) { + // Choose between 2 ROGUE tier items (4%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:${namespace}_option_1_great_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 35) { + // Choose 1 MASTER tier item (1%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ModifierTier.MASTER], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:${namespace}_option_1_amazing_result" + ); + leaveEncounterWithoutBattle(scene); + } else { + // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) + const highestLevelPokemon = getHighestLevelPlayerPokemon( + scene, + true + ); + koPlayerPokemon(highestLevelPokemon); + + scene.currentBattle.mysteryEncounter.setDialogueToken( + "pokeName", + highestLevelPokemon.name + ); + // Show which Pokemon was KOed, then leave encounter with no rewards + // Does this synchronously so that game over doesn't happen over result message + await showEncounterText( + scene, + "mysteryEncounter:${namespace}_option_1_bad_result" + ).then(() => { + if ( + scene.getParty().filter((p) => p.isAllowedInBattle()).length === + 0 + ) { + // All pokemon fainted, game over + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + leaveEncounterWithoutBattle(scene); + } + }); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: "mysteryEncounter:${namespace}_option_2_label", + buttonTooltip: "mysteryEncounter:${namespace}_option_2_tooltip", + selected: [ + { + text: "mysteryEncounter:${namespace}_option_2_selected_message", + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 7a78c6edb4c..c384606f5e3 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -162,10 +162,13 @@ export const mysteryEncountersByBiome = new Map([ [Biome.TALL_GRASS, []], [Biome.METROPOLIS, []], [Biome.FOREST, [ - MysteryEncounterType.SLEEPING_SNORLAX + MysteryEncounterType.SLEEPING_SNORLAX, + MysteryEncounterType.SAFARI_ZONE ]], [Biome.SEA, []], - [Biome.SWAMP, []], + [Biome.SWAMP, [ + MysteryEncounterType.SAFARI_ZONE + ]], [Biome.BEACH, []], [Biome.LAKE, []], [Biome.SEABED, []], @@ -189,7 +192,9 @@ export const mysteryEncountersByBiome = new Map([ [Biome.ABYSS, []], [Biome.SPACE, []], [Biome.CONSTRUCTION_SITE, []], - [Biome.JUNGLE, []], + [Biome.JUNGLE, [ + MysteryEncounterType.SAFARI_ZONE + ]], [Biome.FAIRY_CAVE, []], [Biome.TEMPLE, []], [Biome.SLUM, []], diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index b5b0144c3be..2819b0eb6fb 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -7,5 +7,6 @@ export enum MysteryEncounterType { TRAINING_SESSION, DEPARTMENT_STORE_SALE, SHADY_VITAMIN_DEALER, - FIELD_TRIP + FIELD_TRIP, + SAFARI_ZONE } From da0aea0d1e6213c04c4d3c2c6345111d101898ce Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sun, 14 Jul 2024 17:37:03 -0400 Subject: [PATCH 2/3] safari encounter and sprite offset positioning updates --- .../images/mystery-encounters/chest_blue.json | 169 ++-- .../images/mystery-encounters/chest_blue.png | Bin 2723 -> 2671 bytes .../encounters/mysterious-chest-encounter.ts | 52 +- .../encounters/safari-zone-encounter.ts | 913 +++++++++++++++--- .../shady-vitamin-dealer-encounter.ts | 10 +- .../mystery-encounter-utils.ts | 24 +- .../mystery-encounters/mystery-encounter.ts | 15 +- .../mystery-encounters/mystery-encounters.ts | 2 + src/field/mystery-encounter-intro.ts | 11 +- src/locales/en/battle.ts | 3 + src/locales/en/mystery-encounter.ts | 38 +- src/phases.ts | 6 +- src/phases/mystery-encounter-phase.ts | 39 +- src/pipelines/sprite.ts | 9 +- src/ui/mystery-encounter-ui-handler.ts | 45 +- 15 files changed, 1048 insertions(+), 288 deletions(-) diff --git a/public/images/mystery-encounters/chest_blue.json b/public/images/mystery-encounters/chest_blue.json index 88aadda845a..9a386802e03 100644 --- a/public/images/mystery-encounters/chest_blue.json +++ b/public/images/mystery-encounters/chest_blue.json @@ -4,177 +4,198 @@ "image": "chest_blue.png", "format": "RGBA8888", "size": { - "w": 300, - "h": 75 + "w": 58, + "h": 528 }, "scale": 1, "frames": [ { - "filename": "0001.png", + "filename": "0000.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, + "x": 14, + "y": 30, + "w": 48, + "h": 41 + }, + "frame": { + "x": 1, + "y": 1, + "w": 48, + "h": 41 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { "w": 75, "h": 75 }, + "spriteSourceSize": { + "x": 14, + "y": 34, + "w": 49, + "h": 37 + }, "frame": { - "x": -15, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 44, + "w": 49, + "h": 37 } }, { "filename": "0002.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 14, + "y": 30, + "w": 48, + "h": 41 }, "frame": { - "x": -15, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 83, + "w": 48, + "h": 41 } }, { "filename": "0003.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 14, + "y": 23, + "w": 48, + "h": 48 }, "frame": { - "x": 57, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 126, + "w": 48, + "h": 48 } }, { "filename": "0004.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 4, + "w": 55, + "h": 67 }, "frame": { - "x": 57, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 176, + "w": 55, + "h": 67 } }, { "filename": "0005.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 15, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 129, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 245, + "w": 56, + "h": 69 } }, { "filename": "0006.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 15, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 129, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 316, + "w": 56, + "h": 69 } }, { "filename": "0007.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 201, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 387, + "w": 56, + "h": 69 } }, { "filename": "0008.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 201, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 458, + "w": 56, + "h": 69 } } ] @@ -183,6 +204,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + "smartupdate": "$TexturePacker:SmartUpdate:5f36000f6160ee6f397afe5a6fd60b73:cf6f4b08e23400447813583c322eb6c7:f4f3c064e6c93b8d1290f93bee927f60$" } } diff --git a/public/images/mystery-encounters/chest_blue.png b/public/images/mystery-encounters/chest_blue.png index 0097346e845a2a21d52fb840704f7566b2f23921..ac1039544e3249789b3ece452203dfdde1f6933a 100644 GIT binary patch literal 2671 zcmV-#3Xt`QP)ZJui1hb-q-f`(9d}b3Furx#6K>7NVZht>yp8n>yLq|K-YQ@Bjb!dK$|9U;`ZvW7MCWgf4Xu^(;A$hmCIkz9 zF#uK*g2*ly{!92?RiyvM9@qX-1#n!}Midi0>!-x8hR+pNFq zYKLP)kJM;|>cd95zU%h+e74QA=5W!2uA3c&TVy-XLt`jx^Yd9h+ln6?6s?AT{MyF) z0d`xl%j9#sO!b2^8G^b-KftSstWlkjn+;$y6i1=aZFF;ut|e-KY@o`XBs}LI2DN0} zVHICn$|B(kZ|P$UOV%h3sKdZ*{iG|rD!kL$hf@VI`kt971?Ca(GdyU#LzK|Ep3z}UV+9W*-KMCaPW&iK!MKMCFuNMwt zHvzUtJCO0YR9PJHR9mR)aq$a)5p9ytEb|NKjJ_bD3B?}qs=Li2s%ZqH*0?V()f&h4 zsx|vvQfrmYYpS&c-Y(D7nw5x6wWc0RF0CF*E*|ZYTseA6a&c&<~L6(kp;wpCHBeI?0-j}E-T$UicEam(x%gSO%jFU@SBJ|b%+Fmeml`hD zaJh!dHH6*}dPC?9q4x)bUj5|s3qAE%=o$4`=rzzk=dJOR&pf~Peb0p6>B4@z!ymN> zlO>YpPKBN)9lY-gzqDvyrf>Y72)#h=dke*9GO|wSMbfjnfqtWUwW0HXYs{lMy=$d_ zcRP#X+nbFzr7Xe)4l3O1OZWB4bxWGw;6Y_O)tvO9{KeyMKBl%>lIcVvz);v`B zP~ra}70#_TOFCTZ2)f6%RNC;WmV~(bMo{56QR<1G0GcX1%$58SR8xg(mNkaRT0w>D zG)IfFR#4&EFl>~q_)8RsjFfhXkk$<9q+Sq+we&|6r#r$16v<2=<|-V?1*fl4`$x+N z1fqy8ESd?#-$I2)l(6agW&-iIRN)j=nf5n27l;|^l%9lQqeLJwXe1-5{awukB1I1Y zowQFIa)B6SPlXcGN(XjJS6C%k>QKYCikXhR3L6oKHAydU-l`SD{xZR3E>X?h8-JYu z{cHgxDmtNQG!gWOC^jsi?#@zc&!Y;VvcKe9P0&DO9)TX`=ux9Y<Anb>l#%m zqPVo^VO64bIJwaaC{ik*IKkQ%hvJqeQAbeph*JwZs8DUw60_%FKQzB{6q^)ZdKJep1%Of`B;6a^t*d+1L^{m3d0 zXCXChUOjBeW?LB8ZVR0py2T_-&F>I!8Pq(t(FqQHp7U%IqIbxtcDO=s`yoHI0_)NF zJYyEn&o4ll=f5xV z{0hC1=bvFXJjwI97}BjpXrD%GN0?uC8x01yXB@#$_*``)-hguGJEsPmwZr!5j(o zji%H$OV+);Ku*@LUBz5|+diyA5E4S#V~?(`zuQMbD|aC*dkML^OR!7>wF*Dx>g$%F zN1njeD6AoEZdD~;EIaaqJ98Jv(v`g{8_n?rRAp~kpLfPAFpI3a8S2V*Q?$|ijgE;U zDgG^wVzL2|PX>_r{lwspl3cE;RG7jpR$n|6j-UfsE!RN~R-!&sG6?v|OJz;3Gy>Zw zCQePSE5|m=kfG_CT-R$7Y1{BBu$S|@)-|fTruS-%FDUX3W#HTo5}G#%SoSBR_UXU+ zVoousUi3ulodWSb6giqd0xkLj5rcTw$!NnLh-aAR>L+Pg^ao-J`)1Po(6H)KwNS8zAJ71&J8ldO>Zd(f{H8MK=fho5ZXx4n;50nN$%GWRb0csGOG||8E z2MU7#&$7=Ss0@OgkZpgUWe|M-VShmAaGyWGp*9kFy;ppYlc%rK=qNsLzphIjmcJWiY>82~FzObDxiHLyVJ`f$ac6@rsP8+7) z|7wFmmFCyJ1iL{&X9=MuD3YxW3Ytl{GfShnL4n!a;A{4U4GKqWR7msX4GLr{YL%=9 zRP-f|s%k%X2(%V9C|sjZv`==hGVTIrE|_AE@LGEsk}siiH-sUD{^m}K+rip0CbB7r z?1>iKknq@4Xt5u-5t=bWE`#iC=rA<0R6nxJ#8(>{xl6!ri=c*v7D&P>(UXYuZFeEk zg1gW_&p@v{tD4!mY)sVKB5G8=)zc;V#F;qH%9-dbdpKct!vkl6uU_bLCZ^}k#QC)B zVJeLs32!* z#QC4k!*Pt*GxR7pJ}N4rwQ?!}1X9)UaKYf?M&B2O&~#4fbo$U-eL zOjEg<*e+@5t&ct9N36dr{;_y#WW8l?(a4#Lk6(T5zWDr)SN(V-PnmuD9qpscs3k@E zsbtdjkxcgS>eEuC^~U18-q^>Nv{?!masIZ<>1Mw@dqyvPEvYc~75{N_e$0XKRtS5z ziq21x{`XDTx1_G;*(C}R_H9Kuf2a+cd@pX}YR)#KcD-4><-x^}BaLR;@v@Dt`niJL zs$`6>Q9DxIfYV;t%q+RFQuF1*hDi-=mb>Un^ri)z_+VLd?$u;plSH2ozm_X!2~uNY z(+2OfVjBCIllkFhNuMQSljFsevii(qTx)onr$IdkrMz9<0)|Hl5}*av{i=ip375@7 z(7;4!-X2k$0gdes6%qYJb>Ja@X94X1B4eOu@`Y^j=Ov=CmR1pZcs3F@1v?nTY2{W{ zthLmaDv&W*BF1LgGN_R?k;oK2ct1K==;BgebyjP(GExxUh@3DiFRY!*NT;1+7DhP& zR%xgU;6#|}aC4;kei*5?DC{)YtF}m?m0Rwl2AE?%G}JNU8M63oAl#njNf4l$uJ!c< zy`2Ym@#cY*dvO&ViUfS471$zjOS+tBWlO>PSK?ynJBNi7T}b$x@G}+HE}S@RE(jI3 zxJLC9hNZ%^c;GI13;GY6MUar1#PK$(=o>6po=^{u{B2sU>w>*9;Cjk@-0!b*R5cUr z`R!bsj@NjP7!^R)%GM-d=2azZ_>=rPyaL$E8ynwOvi_1L~!jHd553iT}c{*;eZgbP4zm!7uCb z?h=_5>K^Y%!_9_B47M}*zWy>cazJ5*FEoa?llUwuSYnIE2DIH+3*~BLe{}SshdyB z!n*4z$aR%qN{uaIcg8LrSPh_5KKaOo#J3eOq87l!`048Fh_xArF87%`)I-ifc+hU9 zv!U0i8L)51CE`v8ozwo0bYT=TxeAwjz2m*c@hVTkf+_k@ZsiAzN`>-Fm6e!^5O+Fw% z(z}D7ORSQg!LXDitoCrj@3p}}B>P_lEKXOSJCwKDUebHF`1Jh;#3Jvr(qT9IuHc`> zXsz{UxC4f6GwkrEhAxlM4L*VD!pLg@&53lWQN7%j1A}setZjksqR(|NHl`OP)#W}6 zgBS)w2n(DtPl{oq7tCZ!??^}uj1MWA0EK~_Db&J+m3U6TGIgA9zg=#R2 zx9sBc-0TGr0BH56>Pw%#mz$=;i5yctpFtfpmRhS@78`ICug70-*(AI!i2*)JMfB2S zmm9hxbo6n2f*EU8Zg*@6y|DYk(VwRj5*buaI~J$RYP(CVY-;gGVeN>8g0Ayris?M- ziZ!f0ndenhvzs;Oo?o5~Z=;UUC_wMR#G${`XF$I7jsBcAbXBOOBtVeGuQit6MwG^W zYEz4kf6G0@y!8*OZC`uZ$lcRNC?^Rv zs7l;^`5FD@4`>YIbx8)&ys!^qj0$^T6dT^{aMVt>0DfG_?ClfH9XQ94M{C;tk7JVk z^BnC(i2dh?4J2m*_Zi)wTDU{n@qV2e_WRxKsgbvawNn|)RqL^a&}(b}+sC5LT#Ib0 zOZFd<-BToN?rqybj(mL&Im1!wQ3}aqzo9b`z4>46gw1Zh~xtfax%rb4}qo!2#oP$CV zZIAQJloc<$99>%*IdFLck6gtuHx7ZlGHVltk4%1a7}$s*^^X@{+=nbd54#36chr4} zUwhA}NeSwMiOiYA#4$}Q5x1=@-^+c+|1>#kcTQaM_qejWq_i=hLtMDe!K67H9h$@A z?QT9ztA2h!5$$X~pJr#s3{)I@@kC?c&61XEpq$Hl zCp$~{;7Td|%8@?-Ox0fIt>lh-yt$T&`^P4wDR`i@PvLuf#`I|X`Kd5*=8ZGXkunv7 z=moNxVKgAXrrNPb!sXUhmo6-*2eewBT)P2(QYvRh7{<((YO7Weuzulal_IXK8 zaRl%PbrdebRzKXTzth)NxR4gp>idbcmAR{qvg4iHGx%IC;?0ZNrj}i)4QU0_)m26L zubtqsWXB~pJyFEzvw2m{CI{2g6P6>;!q8YrtMMv?nF_ufTlu|Zf5Rv$`>0J!3aO%H z=7^fiFYeg+Z(~=|KEbY_Zm4Wppd~MW<38`-c?MbdG>nC=QEX0JsD_@h)7g+1ubSu) zJBMW}m^b>8%T?dE<)MA}!dYQ(OgHx?AY5XMkxb92eU7bLT$%h*VD!+_);1!kNWpnZ z6A;H{!E^#<5=H{|oy2C^j+^9|TK~IVUaT-(pnu^4#rb}y8Gu}JgNpFs+NMo$#dMEC zl!5BMx=8EyayC*1Qvj8lbP5=#Ja8Uv1#m-=$AEPI3F!Y*d6adfEKGa;H;wK$z%_Ck emFN-b?F~W#b7delpFUjf=HTIa!bRvrX8#QV5%_Qb diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 49672ee636c..ae6c14f725e 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -30,7 +30,8 @@ export const MysteriousChestEncounter: IMysteryEncounter = fileRoot: "mystery-encounters", hasShadow: true, x: 4, - y: 8, + y: 10, + yShadowOffset: 3, disableAnimation: true, // Re-enabled after option select }, ]) @@ -75,10 +76,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_normal_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 40) { // Choose between 3 ULTRA tier items (20%) @@ -90,10 +88,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_good_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 36) { // Choose between 2 ROGUE tier items (4%) @@ -101,10 +96,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_great_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 35) { // Choose 1 MASTER tier item (1%) @@ -112,10 +104,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.MASTER], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_amazing_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result"); leaveEncounterWithoutBattle(scene); } else { // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) @@ -125,27 +114,22 @@ export const MysteriousChestEncounter: IMysteryEncounter = ); koPlayerPokemon(highestLevelPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken( - "pokeName", - highestLevelPokemon.name - ); + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); // Show which Pokemon was KOed, then leave encounter with no rewards // Does this synchronously so that game over doesn't happen over result message - await showEncounterText( - scene, - "mysteryEncounter:mysterious_chest_option_1_bad_result" - ).then(() => { - if ( - scene.getParty().filter((p) => p.isAllowedInBattle()).length === + await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result") + .then(() => { + if ( + scene.getParty().filter((p) => p.isAllowedInBattle()).length === 0 - ) { + ) { // All pokemon fainted, game over - scene.clearPhaseQueue(); - scene.unshiftPhase(new GameOverPhase(scene)); - } else { - leaveEncounterWithoutBattle(scene); - } - }); + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + leaveEncounterWithoutBattle(scene); + } + }); } }) .build() diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index e2dd7849a49..000c5f24f8d 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,31 +1,167 @@ -import { - getHighestLevelPlayerPokemon, - koPlayerPokemon, - leaveEncounterWithoutBattle, - queueEncounterMessage, - setEncounterRewards, - showEncounterText, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { GameOverPhase } from "#app/phases"; -import { randSeedInt } from "#app/utils"; +import { getRandomSpeciesByStarterTier, initFollowupOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, MysteryEncounterVariant } from "../mystery-encounter"; +import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases"; +import i18next from "i18next"; +import { HiddenAbilityRateBoosterModifier, IvScannerModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { StatusEffect } from "#app/data/status-effect"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; +import { achvs } from "#app/system/achv"; +import { Mode } from "#app/ui/ui"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { BattlerIndex } from "#app/battle"; +import { PlayerGender } from "#enums/player-gender"; +import { IntegerHolder, randSeedInt } from "#app/utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:safari_zone"; +/** + * SAFARI ZONE OPTIONS + * + * Catch and flee rate **multipliers** are calculated in the same way stat changes are (they range from -6/+6) + * https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone + * + * Catch Rate calculation: + * catchRate = speciesCatchRate [1 to 255] * catchStageMultiplier [2/8 to 8/2] * ballCatchRate [1.5] + * + * Flee calculation: + * The harder a species is to catch, the higher its flee rate is + * (Caps at 50% base chance to flee for the hardest to catch Pokemon, before factoring in flee stage) + * fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2] + * Flee chance = fleeRate / 255 + */ +const safariZoneOptions: MysteryEncounterOption[] = [ + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_pokeball_option_label`, + buttonTooltip: `${namespace}_pokeball_option_tooltip`, + selected: [ + { + text: `${namespace}_pokeball_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw a ball option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + const catchResult = await throwPokeball(scene, pokemon); + + if (catchResult) { + // You caught pokemon + scene.unshiftPhase(new VictoryPhase(scene, 0)); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 0, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + } else { + // Pokemon failed to catch, end turn + await doEndTurn(scene, 0); + } + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_bait_option_label`, + buttonTooltip: `${namespace}_bait_option_tooltip`, + selected: [ + { + text: `${namespace}_bait_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw bait option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await throwBait(scene, pokemon); + + // 100% chance to increase catch stage +2 + tryChangeCatchStage(scene, 2); + // 80% chance to increase flee stage +1 + const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); + if (!fleeChangeResult) { + scene.queueMessage(i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 0, null, 500); + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 0, null, 500); + } + // TODO: throw bait with eat animation + // TODO: play bug bite sfx, maybe spike cannon? + + await doEndTurn(scene, 1); + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_mud_option_label`, + buttonTooltip: `${namespace}_mud_option_tooltip`, + selected: [ + { + text: `${namespace}_mud_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw mud option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await throwMud(scene, pokemon); + // 100% chance to decrease flee stage -2 + tryChangeFleeStage(scene, -2); + // 80% chance to decrease catch stage -1 + const catchChangeResult = tryChangeCatchStage(scene, -1, 8); + if (!catchChangeResult) { + scene.queueMessage(i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 0, null, 500); + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 0, null, 500); + } + + await doEndTurn(scene, 2); + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_flee_option_label`, + buttonTooltip: `${namespace}_flee_option_tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Flee option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await doPlayerFlee(scene, pokemon); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 3, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + return true; + }) + .build() +]; + export const SafariZoneEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.SAFARI_ZONE - ) + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive .withHideIntroVisuals(false) .withIntroSpriteConfigs([ { @@ -33,133 +169,57 @@ export const SafariZoneEncounter: IMysteryEncounter = fileRoot: "mystery-encounters", hasShadow: true, x: 4, - y: 8, + y: 10, + yShadowOffset: 3, disableAnimation: true, // Re-enabled after option select }, ]) .withIntroDialogue([ { - text: `mysteryEncounter:${namespace}_intro_message`, + text: `${namespace}_intro_message`, }, ]) - .withTitle(`mysteryEncounter:${namespace}_title`) - .withDescription(`mysteryEncounter:${namespace}_description`) - .withQuery(`mysteryEncounter:${namespace}_query`) - .withOption( - new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) - .withDialogue({ - buttonLabel: "mysteryEncounter:${namespace}_option_1_label", - buttonTooltip: "mysteryEncounter:${namespace}_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:${namespace}_option_1_selected_message", - }, - ], - }) - .withPreOptionPhase(async (scene: BattleScene) => { - // Play animation - const introVisuals = - scene.currentBattle.mysteryEncounter.introVisuals; - introVisuals.spriteConfigs[0].disableAnimation = false; - introVisuals.playAnim(); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Open the chest - const roll = randSeedInt(100); - if (roll > 60) { - // Choose between 2 COMMON / 2 GREAT tier items (40%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ - ModifierTier.COMMON, - ModifierTier.COMMON, - ModifierTier.GREAT, - ModifierTier.GREAT, - ], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_normal_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 40) { - // Choose between 3 ULTRA tier items (20%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_good_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 36) { - // Choose between 2 ROGUE tier items (4%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_great_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 35) { - // Choose 1 MASTER tier item (1%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ModifierTier.MASTER], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_amazing_result" - ); - leaveEncounterWithoutBattle(scene); - } else { - // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) - const highestLevelPokemon = getHighestLevelPlayerPokemon( - scene, - true - ); - koPlayerPokemon(highestLevelPokemon); - - scene.currentBattle.mysteryEncounter.setDialogueToken( - "pokeName", - highestLevelPokemon.name - ); - // Show which Pokemon was KOed, then leave encounter with no rewards - // Does this synchronously so that game over doesn't happen over result message - await showEncounterText( - scene, - "mysteryEncounter:${namespace}_option_1_bad_result" - ).then(() => { - if ( - scene.getParty().filter((p) => p.isAllowedInBattle()).length === - 0 - ) { - // All pokemon fainted, game over - scene.clearPhaseQueue(); - scene.unshiftPhase(new GameOverPhase(scene)); - } else { - leaveEncounterWithoutBattle(scene); - } - }); - } - }) - .build() + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + // TODO: update + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start safari encounter + const encounter = scene.currentBattle.mysteryEncounter; + encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE; + encounter.misc = { + safariPokemonRemaining: 3 + }; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + scene.loadSe("PRSFX- Bug Bite", "battle_anims"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims"); + scene.loadSe("PRSFX- Taunt2", "battle_anims"); + await hideMysteryEncounterIntroVisuals(scene); + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, hideDescription: true }); + return true; + }) + .build() ) .withSimpleOption( { - buttonLabel: "mysteryEncounter:${namespace}_option_2_label", - buttonTooltip: "mysteryEncounter:${namespace}_option_2_tooltip", + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, selected: [ { - text: "mysteryEncounter:${namespace}_option_2_selected_message", + text: `${namespace}_option_2_selected_message`, }, ], }, @@ -170,3 +230,588 @@ export const SafariZoneEncounter: IMysteryEncounter = } ) .build(); + +function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { + return new Promise(resolve => { + const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; + if (introVisuals) { + // Hide + scene.tweens.add({ + targets: introVisuals, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(introVisuals); + introVisuals.setVisible(false); + introVisuals.destroy(); + scene.currentBattle.mysteryEncounter.introVisuals = null; + resolve(true); + } + }); + } else { + resolve(true); + } + }); +} + +async function summonSafariPokemon(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter; + // Message pokemon remaining + scene.queueMessage(i18next.t(`${namespace}_remaining_count`, { remainingCount: encounter.misc.safariPokemonRemaining}), null, true); + + // Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken + // Safari pokemon roll twice on shiny and HA chances, but are otherwise normal + let enemySpecies; + let pokemon; + scene.executeWithSeedOffset(() => { + enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); + enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(scene.currentBattle.waveIndex, true, false, scene.gameMode)); + scene.currentBattle.enemyParty = []; + pokemon = scene.addEnemyPokemon(enemySpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, false); + + // Roll shiny twice + if (!pokemon.shiny) { + pokemon.trySetShiny(); + } + + // Roll HA twice + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + pokemon.calculateStats(); + + scene.currentBattle.enemyParty[0] = pokemon; + }, scene.currentBattle.waveIndex + encounter.misc.safariPokemonRemaining); + + scene.gameData.setPokemonSeen(pokemon, true); + await pokemon.loadAssets(); + + // Reset safari catch and flee rates + encounter.misc.catchStage = 0; + encounter.misc.fleeStage = 0; + encounter.misc.pokemon = pokemon; + encounter.misc.safariPokemonRemaining -= 1; + + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + + scene.ui.showText(i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), null, () => { + const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); + } + }, 1500); +} + +async function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const baseCatchRate = pokemon.species.catchRate; + // Catch stage ranges from -6 to +6 (like stat boost stages) + const safariCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage; + // Catch modifier ranges from 2/8 (-6 stage) to 8/2 (+6) + const safariModifier = (2 + Math.min(Math.max(safariCatchStage, 0), 6)) / (2 - Math.max(Math.min(safariCatchStage, 0), -6)); + // Catch rate same as safari ball + const pokeballMultiplier = 1.5; + const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier); + const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate))); + const fpOffset = pokemon.getFieldPositionOffset(); + const catchSuccess = (ballTwitchRate / 65536) * (ballTwitchRate / 65536) * (ballTwitchRate / 65536); + console.log("Catch success rate: " + catchSuccess); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + scene.playSound("pb_throw"); + scene.time.delayedCall(300, () => { + scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); + }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: pokeball, + x: { value: 236 + fpOffset[0], ease: "Linear" }, + y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("pb_rel"); + pokemon.tint(getPokeballTintColor(pokeballType)); + + addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + + scene.tweens.add({ + targets: pokemon, + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + y: 20, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + pokemon.setVisible(false); + scene.playSound("pb_catch"); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); + + const doShake = () => { + let shakeCount = 0; + const pbX = pokeball.x; + const shakeCounter = scene.tweens.addCounter({ + from: 0, + to: 1, + repeat: 4, + yoyo: true, + ease: "Cubic.easeOut", + duration: 250, + repeatDelay: 500, + onUpdate: t => { + if (shakeCount && shakeCount < 4) { + const value = t.getValue(); + const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; + pokeball.setX(pbX + value * 4 * directionMultiplier); + pokeball.setAngle(value * 27.5 * directionMultiplier); + } + }, + onRepeat: () => { + if (!pokemon.species.isObtainable()) { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } else if (shakeCount++ < 3) { + if (randSeedInt(65536) < ballTwitchRate) { + scene.playSound("pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("pb_lock"); + addPokeballCaptureStars(scene, pokeball); + + const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); + pbTint.setOrigin(pokeball.originX, pokeball.originY); + pbTint.setTintFill(0); + pbTint.setAlpha(0); + scene.field.add(pbTint); + scene.tweens.add({ + targets: pbTint, + alpha: 0.375, + duration: 200, + easing: "Sine.easeOut", + onComplete: () => { + scene.tweens.add({ + targets: pbTint, + alpha: 0, + duration: 200, + easing: "Sine.easeIn", + onComplete: () => pbTint.destroy() + }); + } + }); + } + }, + onComplete: () => { + catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); + } + }); + }; + + scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); + } + }); + } + }); + }); + }); +} + +async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { + // TODO: replace with bait + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + bait.setOrigin(0.5, 0.625); + scene.field.add(bait); + + scene.playSound("pb_throw"); + // scene.time.delayedCall(300, () => { + // scene.field.moveBelow(pokemon, pokeball as Phaser.GameObjects.GameObject); + // }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: bait, + x: { value: 210 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Bait frame 2 + bait.setTexture("pb", `${pokeballAtlasKey}_opening`); + // Bait frame 3 + scene.time.delayedCall(17, () => bait.setTexture("pb", `${pokeballAtlasKey}_open`)); + // scene.playSound("pb_rel"); + // pokemon.tint(getPokeballTintColor(pokeballType)); + + // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + scene.time.delayedCall(512, () => { + scene.tweens.add({ + targets: pokemon, + duration: 200, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 30, + loop: 2, + onStart: () => { + scene.playSound("PRSFX- Bug Bite"); + }, + onLoop: () => { + scene.playSound("PRSFX- Bug Bite"); + }, + onComplete: () => { + resolve(true); + bait.destroy(); + } + }); + }); + } + }); + }); + }); +} + +async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { + // TODO: replace with mud + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + mud.setOrigin(0.5, 0.625); + scene.field.add(mud); + + scene.playSound("pb_throw"); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: mud, + x: { value: 230 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Bait frame 2 + mud.setTexture("pb", `${pokeballAtlasKey}_opening`); + // Bait frame 3 + scene.time.delayedCall(17, () => mud.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("PRSFX- Sludge Bomb2"); + // pokemon.tint(getPokeballTintColor(pokeballType)); + + // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + scene.time.delayedCall(1536, () => { + mud.destroy(); + scene.tweens.add({ + targets: pokemon, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 20, + loop: 1, + onStart: () => { + scene.playSound("PRSFX- Taunt2"); + }, + onLoop: () => { + scene.playSound("PRSFX- Taunt2"); + }, + onComplete: () => { + resolve(true); + } + }); + }); + } + }); + }); + }); +} + +async function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("pb_rel"); + pokemon.setY(originalY); + if (pokemon.status?.effect !== StatusEffect.SLEEP) { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + } + pokemon.tint(getPokeballTintColor(pokeballType)); + pokemon.setVisible(true); + pokemon.untint(250, "Sine.easeOut"); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + + scene.currentBattle.lastUsedPokeball = pokeballType; + removePb(scene, pokeball); + + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); + }); +} + +async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise { + scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); + + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.pokemonInfoContainer.show(pokemon, true); + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + + return new Promise(resolve => { + scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + const end = () => { + scene.pokemonInfoContainer.hide(); + removePb(scene, pokeball); + resolve(); + }; + const removePokemon = () => { + scene.field.remove(pokemon, true); + }; + const addToParty = () => { + const newPokemon = pokemon.addToParty(pokeballType); + const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + if (scene.getParty().filter(p => p.isShiny()).length === 6) { + scene.validateAchv(achvs.SHINY_PARTY); + } + Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { + scene.updateModifiers(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } + }); + }; + Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { + if (scene.getParty().length === 6) { + const promptRelease = () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { + scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.ui.setMode(Mode.CONFIRM, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (slotIndex < 6) { + addToParty(); + } else { + promptRelease(); + } + }); + }); + }, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + removePokemon(); + end(); + }); + }); + }); + }; + promptRelease(); + } else { + addToParty(); + } + }); + }, 0, true); + }); +} + +function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => pokeball.destroy() + }); +} + +function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { + const speciesCatchRate = pokemon.species.catchRate; + const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6)); + const fleeRate = (255 * 255 - speciesCatchRate * speciesCatchRate) / 255 / 2 * fleeModifier; + console.log("Flee rate: " + fleeRate); + const roll = randSeedInt(256); + console.log("Roll: " + roll); + return roll < fleeRate; +} + +async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const fleeAnimation = new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + resolve(); + } + }); + }); + + const prompt = new Promise(resolve => { + scene.ui.showText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); + }); + + await Promise.all([fleeAnimation, prompt]); +} + +async function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const fleeAnimation = new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + resolve(); + } + }); + }); + + const prompt = new Promise(resolve => { + scene.ui.showText(i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); + }); + + await Promise.all([fleeAnimation, prompt]); +} + +function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + console.log("Failed to change flee stage"); + return false; + } + const currentFleeStage = scene.currentBattle.mysteryEncounter.misc.fleeStage ?? 0; + // console.log("currentFleeStage: " + currentFleeStage); + scene.currentBattle.mysteryEncounter.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); + return true; +} + +function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + console.log("Failed to change catch stage"); + return false; + } + const currentCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage ?? 0; + // console.log("currentCatchStage: " + currentCatchStage); + scene.currentBattle.mysteryEncounter.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); + return true; +} + +async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: string) { + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + console.log("fleeStage: " + scene.currentBattle.mysteryEncounter.misc.fleeStage); + console.log("catchStage: " + scene.currentBattle.mysteryEncounter.misc.catchStage); + const isFlee = isPokemonFlee(pokemon, scene.currentBattle.mysteryEncounter.misc.fleeStage); + if (isFlee) { + // Pokemon flees! + await doPokemonFlee(scene, pokemon); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 500); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } +} diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 6018b651639..8bfd6daa9f9 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -28,15 +28,17 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = fileRoot: "pokemon", hasShadow: true, repeat: true, - x: 10, - y: -1, + x: 12, + y: -5, + yShadowOffset: -5 }, { spriteKey: "b2w2_veteran_m", fileRoot: "mystery-encounters", hasShadow: true, - x: -10, - y: 2, + x: -12, + y: 3, + yShadowOffset: 3 }, ]) .withIntroDialogue([ diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/mystery-encounter-utils.ts index 5c6e7eefa8d..e0c8f73e569 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/mystery-encounter-utils.ts @@ -10,7 +10,7 @@ import Trainer, { TrainerVariant } from "../../field/trainer"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BattleEndPhase, EggLapsePhase, ExpPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; -import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; +import { MysteryEncounterBattlePhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; import * as Utils from "../../utils"; import { isNullOrUndefined } from "#app/utils"; import { TrainerType } from "#enums/trainer-type"; @@ -27,6 +27,7 @@ import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/myst import { getTextWithColors, TextStyle } from "#app/ui/text"; import * as Overrides from "#app/overrides"; import { UiTheme } from "#enums/ui-theme"; +import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; /** * @@ -427,6 +428,11 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS if (playSound) { scene.playSound("buy"); } + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true); + } } /** @@ -459,7 +465,7 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M } /** - * + * This function is intended for use inside onPreOptionPhase() of an encounter option * @param scene * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object @@ -674,6 +680,16 @@ export function setEncounterExp(scene: BattleScene, participantIds: integer[], b }; } +/** + * Can be used to exit an encounter without any battles or followup + * Will skip any shops and rewards, and queue the next encounter phase as normal + * @param scene + * @param followupOptionSelectSettings + */ +export function initFollowupOptionSelect(scene: BattleScene, followupOptionSelectSettings: MysteryEncounterUiSettings) { + scene.pushPhase(new MysteryEncounterPhase(scene, followupOptionSelectSettings)); +} + /** * Can be used to exit an encounter without any battles or followup * Will skip any shops and rewards, and queue the next encounter phase as normal @@ -688,7 +704,9 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo } export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false) { - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { + if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.SAFARI_BATTLE) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + } else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); } else if (!scene.getEnemyParty().find(p => scene.currentBattle.mysteryEncounter.encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index e02639939b8..0be21d1520f 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -24,7 +24,8 @@ export enum MysteryEncounterVariant { TRAINER_BATTLE, WILD_BATTLE, BOSS_BATTLE, - NO_BATTLE + NO_BATTLE, + SAFARI_BATTLE } export enum MysteryEncounterTier { @@ -118,6 +119,18 @@ export default interface IMysteryEncounter { */ expMultiplier?: number; + /** + * When true, will never queue PostSummon phases from a SummonPhase + * Defaults to false + */ + disableAllPostSummon?: boolean; + + /** + * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave + * You should never need to modify this + */ + seedOffset?: any; + /** * Generic property to set any custom data required for the encounter * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index c384606f5e3..a53f12a0bfa 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -10,6 +10,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter"; import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter"; import { FieldTripEncounter } from "./encounters/field-trip-encounter"; +import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -213,6 +214,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter; + allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 1b0fb3bca01..2c606ad61cc 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -11,6 +11,7 @@ export class MysteryEncounterSpriteConfig { tint?: number; x?: number; // X offset y?: number; // Y offset + yShadowOffset?: number; scale?: number; isItem?: boolean; // For item sprites, set to true } @@ -37,10 +38,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return; } - const getSprite = (spriteKey: string, hasShadow?: boolean) => { + const getSprite = (spriteKey: string, hasShadow?: boolean, yShadowOffset?: number) => { const ret = this.scene.addFieldSprite(0, 0, spriteKey); ret.setOrigin(0.5, 1); - ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow }); + ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadowOffset ?? 0 }); return ret; }; @@ -62,7 +63,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con let sprite: GameObjects.Sprite; let tintSprite: GameObjects.Sprite; if (!config.isItem) { - sprite = getSprite(config.spriteKey, config.hasShadow); + sprite = getSprite(config.spriteKey, config.hasShadow, config.yShadowOffset); tintSprite = getSprite(config.spriteKey); } else { sprite = getItemSprite(config.spriteKey); @@ -83,8 +84,8 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con tintSprite.setPosition(origin + config.x, tintSprite.y); } if (config.y) { - sprite.setPosition(sprite.x, config.y); - tintSprite.setPosition(tintSprite.x, config.y); + sprite.setPosition(sprite.x, sprite.y + config.y); + tintSprite.setPosition(tintSprite.x, tintSprite.y + config.y); } } else { // Single sprite diff --git a/src/locales/en/battle.ts b/src/locales/en/battle.ts index b10e5507b3b..6deaf4496a0 100644 --- a/src/locales/en/battle.ts +++ b/src/locales/en/battle.ts @@ -16,6 +16,9 @@ export const battle: SimpleTranslationEntries = { "moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} was caught!", + "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", + "pokemonFled": "The wild {{pokemonName}} fled!", + "playerFled": "You fled from the {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} has been\nadded as a starter!", "partyFull": "Your party is full.\nRelease a Pokémon to make room for {{pokemonName}}?", "pokemon": "Pokémon", diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 6ba84f6142b..a13e35a4474 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -17,6 +17,10 @@ export const mysteryEncounter: SimpleTranslationEntries = { // DO NOT REMOVE "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + // General use content + "paid_money": "You paid ₽{{amount, number}}.", + "receive_money": "You received ₽{{amount, number}}!", + // Mystery Encounters -- Common Tier "mysterious_chest_intro_message": "You found...@d{32} a chest?", @@ -125,7 +129,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "field_trip_outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!", "field_trip_outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere.", - // Mystery Encounters -- Uncommon Tier + // Mystery Encounters -- Great Tier "mysterious_challengers_intro_message": "Mysterious challengers have appeared!", "mysterious_challengers_title": "Mysterious Challengers", @@ -140,7 +144,35 @@ export const mysteryEncounter: SimpleTranslationEntries = { "mysterious_challengers_option_selected_message": "The trainer steps forward...", "mysterious_challengers_outro_win": "The mysterious challenger was defeated!", - // Mystery Encounters -- Rare Tier + "safari_zone_intro_message": "It's a safari zone!", + "safari_zone_title": "The Safari Zone", + "safari_zone_description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\nBeware, though. These Pokémon may flee before you're able to catch them!", + "safari_zone_query": "Would you like to enter?", + "safari_zone_option_1_label": "Enter", + "safari_zone_option_1_tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "safari_zone_option_2_label": "Leave", + "safari_zone_option_2_tooltip": "(-) No Rewards", + "safari_zone_option_1_selected_message": "Time to test your luck.", + "safari_zone_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.", + "safari_zone_pokeball_option_label": "Throw a Pokéball", + "safari_zone_pokeball_option_tooltip": "(+) Throw a Pokéball", + "safari_zone_pokeball_option_selected": "You throw a Pokéball!", + "safari_zone_bait_option_label": "Throw bait", + "safari_zone_bait_option_tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", + "safari_zone_bait_option_selected": "You throw some bait!", + "safari_zone_mud_option_label": "Throw mud", + "safari_zone_mud_option_tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", + "safari_zone_mud_option_selected": "You throw some mud!", + "safari_zone_flee_option_label": "Flee", + "safari_zone_flee_option_tooltip": "(?) Flee from this Pokémon", + "safari_zone_pokemon_watching": "{{pokemonName}} is watching carefully!", + "safari_zone_pokemon_eating": "{{pokemonName}} is eating!", + "safari_zone_pokemon_busy_eating": "{{pokemonName}} is busy eating!", + "safari_zone_pokemon_angry": "{{pokemonName}} is angry!", + "safari_zone_pokemon_beside_itself_angry": "{{pokemonName}} is beside itself with anger!", + "safari_zone_remaining_count": "{{remainingCount}} Pokémon remaining!", + + // Mystery Encounters -- Ultra Tier "training_session_intro_message": "You've come across some\ntraining tools and supplies.", "training_session_title": "Training Session", @@ -163,7 +195,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { $Its ability was changed to {{ability}}!`, "training_session_outro_win": "That was a successful training session!", - // Mystery Encounters -- Super Rare Tier + // Mystery Encounters -- Rogue Tier "dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...", "dark_deal_speaker": "Shady Guy", diff --git a/src/phases.ts b/src/phases.ts index 4be1305c8b7..9bc61aa8454 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1650,7 +1650,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.untint(250, "Sine.easeIn"); this.scene.updateFieldScale(); pokemon.x += 16; - pokemon.y -= 16; + pokemon.y -= 20; pokemon.alpha = 0; // Ease pokemon in @@ -1680,7 +1680,9 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) { + const addPostSummonForEncounter = this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter?.disableAllPostSummon; + + if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1 || addPostSummonForEncounter) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phase.ts index 638f2c0b15d..96acce6adcc 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phase.ts @@ -15,6 +15,7 @@ import { Tutorial, handleTutorial } from "../tutorial"; import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; +import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; /** * Will handle (in order): @@ -26,8 +27,11 @@ import { isNullOrUndefined } from "../utils"; * - Queuing of the MysteryEncounterOptionSelectedPhase */ export class MysteryEncounterPhase extends Phase { - constructor(scene: BattleScene) { + followupOptionSelectSettings: MysteryEncounterUiSettings; + + constructor(scene: BattleScene, followupOptionSelectSettings?: MysteryEncounterUiSettings) { super(scene); + this.followupOptionSelectSettings = followupOptionSelectSettings; } start() { @@ -37,12 +41,18 @@ export class MysteryEncounterPhase extends Phase { this.scene.clearPhaseQueue(); this.scene.clearPhaseQueueSplice(); - // Sets flag that ME was encountered - // Can be used in later MEs to check for requirements to spawn, etc. - this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]); + // Generates seed offset for RNG consistency, but incremented if the same MysteryEncounter has multiple option select cycles + const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000; + this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512; + + if (!this.followupOptionSelectSettings) { + // Sets flag that ME was encountered, only if this is not a followup option select phase + // Can be used in later MEs to check for requirements to spawn, etc. + this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]); + } // Initiates encounter dialogue window and option select - this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER); + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.followupOptionSelectSettings); } handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { @@ -61,24 +71,24 @@ export class MysteryEncounterPhase extends Phase { return await option.onPreOptionPhase(this.scene) .then((result) => { if (isNullOrUndefined(result) || result) { - this.continueEncounter(index); + this.continueEncounter(); } }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } else { - this.continueEncounter(index); + this.continueEncounter(); } return true; } - continueEncounter(optionIndex: number) { + continueEncounter() { const endDialogueAndContinueEncounter = () => { this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene)); this.end(); }; - const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.options?.[optionIndex]?.dialogue; + const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue; if (optionSelectDialogue?.selected?.length > 0) { // Handle intermediate dialogue (between player selection event and the onOptionSelect logic) this.scene.ui.setMode(Mode.MESSAGE); @@ -139,14 +149,14 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); }); } else { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } } @@ -265,7 +275,7 @@ export class MysteryEncounterBattlePhase extends Phase { } else { const trainer = this.scene.currentBattle.trainer; let message: string; - scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), scene.currentBattle.waveIndex * 1000); + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.seedOffset); const showDialogueAndSummon = () => { scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { @@ -390,6 +400,7 @@ export class MysteryEncounterRewardsPhase extends Phase { this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase); this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, null, { fillRemaining: false, rerollMultiplier: 0 })); } + // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) }, this.scene.currentBattle.waveIndex * 1000); this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene)); @@ -423,7 +434,7 @@ export class PostMysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } else { this.continueEncounter(); } diff --git a/src/pipelines/sprite.ts b/src/pipelines/sprite.ts index a61d321c765..e36765f0d4c 100644 --- a/src/pipelines/sprite.ts +++ b/src/pipelines/sprite.ts @@ -38,6 +38,7 @@ uniform vec2 texFrameUv; uniform vec2 size; uniform vec2 texSize; uniform float yOffset; +uniform float yShadowOffset; uniform vec4 tone; uniform ivec4 baseVariantColors[32]; uniform vec4 variantColors[32]; @@ -252,7 +253,7 @@ void main() { float width = size.x - (yOffset / 2.0); float spriteX = ((floor(outPosition.x / fieldScale) - relPosition.x) / width) + 0.5; - float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y) / size.y); + float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y - yShadowOffset) / size.y); if (yCenter == 1) { spriteY += 0.5; @@ -339,6 +340,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", 0, 0); this.set2f("texSize", 0, 0); this.set1f("yOffset", 0); + this.set1f("yShadowOffset", 0); this.set4fv("tone", this._tone); } @@ -351,6 +353,7 @@ export default class SpritePipeline extends FieldSpritePipeline { const tone = data["tone"] as number[]; const teraColor = data["teraColor"] as integer[] ?? [ 0, 0, 0 ]; const hasShadow = data["hasShadow"] as boolean; + const yShadowOffset = data["yShadowOffset"] as number; const ignoreFieldPos = data["ignoreFieldPos"] as boolean; const ignoreOverride = data["ignoreOverride"] as boolean; @@ -377,6 +380,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", sprite.frame.width, sprite.height); this.set2f("texSize", sprite.texture.source[0].width, sprite.texture.source[0].height); this.set1f("yOffset", sprite.height - sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); + this.set1f("yShadowOffset", yShadowOffset ?? 0); this.set4fv("tone", tone); this.bindTexture(this.game.textures.get("tera").source[0].glTexture, 1); @@ -448,6 +452,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set1f("vCutoff", v1); const hasShadow = sprite.pipelineData["hasShadow"] as boolean; + const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number; if (hasShadow) { const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; @@ -455,7 +460,7 @@ export default class SpritePipeline extends FieldSpritePipeline { const baseY = (isEntityObj ? sprite.parentContainer.y : sprite.y + sprite.height) * 6 / fieldScaleRatio; - const bottomPadding = Math.ceil(sprite.height * 0.05) * 6 / fieldScaleRatio; + const bottomPadding = Math.ceil(sprite.height * 0.05 + Math.max(yShadowOffset, 0)) * 6 / fieldScaleRatio; const yDelta = (baseY - y1) / field.scale; y2 = y1 = baseY + bottomPadding; const pixelHeight = (v1 - v0) / (sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index e9ee0f1f6e7..800dd551dc4 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -12,6 +12,16 @@ import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; import { getEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; +export class MysteryEncounterUiSettings { + hideDescription?: boolean; + slideInDescription?: boolean; + overrideTitle?: string; + overrideDescription?: string; + overrideQuery?: string; + overrideOptions?: MysteryEncounterOption[]; + startingCursorIndex?: number; +} + export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; private cursorObj: Phaser.GameObjects.Image; @@ -27,7 +37,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { private descriptionScrollTween: Phaser.Tweens.Tween; private rarityBall: Phaser.GameObjects.Sprite; - private filteredEncounterOptions: MysteryEncounterOption[] = []; + private overrideSettings: MysteryEncounterUiSettings; + private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; protected viewPartyIndex: integer = 0; @@ -70,16 +81,21 @@ export default class MysteryEncounterUiHandler extends UiHandler { show(args: any[]): boolean { super.show(args); + this.overrideSettings = args[0] as MysteryEncounterUiSettings ?? {}; + const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription; + const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription; + const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; + this.cursorContainer.setVisible(true); - this.descriptionContainer.setVisible(true); + this.descriptionContainer.setVisible(showDescriptionContainer); this.optionsContainer.setVisible(true); - this.displayEncounterOptions(!(args[0] as boolean || false)); + this.displayEncounterOptions(slideInDescription); const cursor = this.getCursor(); if (cursor === (this?.optionsContainer?.length || 0) - 1) { // Always resets cursor on view party button if it was last there this.setCursor(cursor); } else { - this.setCursor(0); + this.setCursor(startingCursorIndex); } if (this.blockInput) { setTimeout(() => { @@ -100,12 +116,16 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (button === Button.CANCEL || button === Button.ACTION) { if (button === Button.ACTION) { - const selected = this.filteredEncounterOptions[cursor]; + const selected = this.encounterOptions[cursor]; if (cursor === this.viewPartyIndex) { // Handle view party success = true; + const overrideSettings: MysteryEncounterUiSettings = { + ...this.overrideSettings, + slideInDescription: false + }; this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, true); + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings); setTimeout(() => { this.setCursor(this.viewPartyIndex); this.unblockInput(); @@ -253,7 +273,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (this.blockInput) { this.blockInput = false; for (let i = 0; i < this.optionsContainer.length - 1; i++) { - const optionMode = this.filteredEncounterOptions[i].optionMode; + const optionMode = this.encounterOptions[i].optionMode; if (!this.optionsMeetsReqs[i] && (optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) { continue; } @@ -296,7 +316,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { displayEncounterOptions(slideInDescription: boolean = true): void { this.getUi().clearText(); const mysteryEncounter = this.scene.currentBattle.mysteryEncounter; - this.filteredEncounterOptions = mysteryEncounter.options; + this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options; this.optionsMeetsReqs = []; const titleText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE); @@ -307,11 +327,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.optionsContainer.removeAll(); // Options Window - for (let i = 0; i < this.filteredEncounterOptions.length; i++) { - const option = this.filteredEncounterOptions[i]; + for (let i = 0; i < this.encounterOptions.length; i++) { + const option = this.encounterOptions[i]; let optionText; - switch (this.filteredEncounterOptions.length) { + switch (this.encounterOptions.length) { case 2: optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; @@ -424,7 +444,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { } let text: string; - const cursorOption = this.filteredEncounterOptions[cursor]; + const cursorOption = this.encounterOptions[cursor]; const optionDialogue = cursorOption.dialogue; if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) { text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT); @@ -474,6 +494,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { clear(): void { super.clear(); + this.overrideSettings = null; this.optionsContainer.setVisible(false); this.optionsContainer.removeAll(true); this.descriptionContainer.setVisible(false); From e1bb1e24815cdda2816254381e49479ed765bcd2 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Mon, 15 Jul 2024 12:36:10 -0400 Subject: [PATCH 3/3] clean up safari code and utils --- public/images/mystery-encounters/bait.json | 83 +++ public/images/mystery-encounters/bait.png | Bin 0 -> 277 bytes public/images/mystery-encounters/mud.json | 104 +++ public/images/mystery-encounters/mud.png | Bin 0 -> 361 bytes .../encounters/dark-deal-encounter.ts | 3 +- .../department-store-sale-encounter.ts | 2 +- .../encounters/field-trip-encounter.ts | 2 +- .../encounters/fight-or-flight-encounter.ts | 7 +- .../mysterious-challengers-encounter.ts | 2 +- .../encounters/mysterious-chest-encounter.ts | 17 +- .../encounters/safari-zone-encounter.ts | 598 ++++-------------- .../shady-vitamin-dealer-encounter.ts | 3 +- .../encounters/sleeping-snorlax-encounter.ts | 3 +- .../encounters/training-session-encounter.ts | 17 +- .../mystery-encounters/mystery-encounter.ts | 22 +- .../utils/encounter-dialogue-utils.ts | 72 +++ .../encounter-phase-utils.ts} | 261 ++------ .../utils/encounter-pokemon-utils.ts | 451 +++++++++++++ src/locales/en/mystery-encounter.ts | 4 +- src/phases.ts | 7 +- src/phases/mystery-encounter-phase.ts | 50 +- src/pipelines/sprite.ts | 2 +- .../mystery-encounter-utils.test.ts | 7 +- src/ui/mystery-encounter-ui-handler.ts | 23 +- 24 files changed, 953 insertions(+), 787 deletions(-) create mode 100644 public/images/mystery-encounters/bait.json create mode 100644 public/images/mystery-encounters/bait.png create mode 100644 public/images/mystery-encounters/mud.json create mode 100644 public/images/mystery-encounters/mud.png create mode 100644 src/data/mystery-encounters/utils/encounter-dialogue-utils.ts rename src/data/mystery-encounters/{mystery-encounter-utils.ts => utils/encounter-phase-utils.ts} (76%) create mode 100644 src/data/mystery-encounters/utils/encounter-pokemon-utils.ts diff --git a/public/images/mystery-encounters/bait.json b/public/images/mystery-encounters/bait.json new file mode 100644 index 00000000000..ae9ee38ee13 --- /dev/null +++ b/public/images/mystery-encounters/bait.json @@ -0,0 +1,83 @@ +{ + "textures": [ + { + "image": "bait.png", + "format": "RGBA8888", + "size": { + "w": 14, + "h": 43 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 12, + "h": 13 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 12, + "h": 13 + }, + "frame": { + "x": 1, + "y": 16, + "w": 12, + "h": 13 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 12, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 5, + "w": 11, + "h": 11 + }, + "frame": { + "x": 1, + "y": 31, + "w": 11, + "h": 11 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:f0ec04fcd67ac346dce973693711d032:b697e09191c4312b8faaa0a080a309b7:1af241a52e61fa01ca849aa03c112f85$" + } +} diff --git a/public/images/mystery-encounters/bait.png b/public/images/mystery-encounters/bait.png new file mode 100644 index 0000000000000000000000000000000000000000..7de9169d187544cc2c992c19274f6299c09a39b8 GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^d_b(t!3-qpzk8+uDd_;85LX}#1Qo8)6O+sDE!h5X z+qSPK5B`67_y3O{(G^kWfpUx`L4Lsu4$p3Y^l%n4nJa0`PlBg3pY5E0jpgZkEMEml5YNEcaJqbjoWo&CSJ?MySOwYlD|`oOs2F-wn{ T@dF>A^B6o`{an^LB{Ts5i$-hZ literal 0 HcmV?d00001 diff --git a/public/images/mystery-encounters/mud.json b/public/images/mystery-encounters/mud.json new file mode 100644 index 00000000000..804eed36052 --- /dev/null +++ b/public/images/mystery-encounters/mud.json @@ -0,0 +1,104 @@ +{ + "textures": [ + { + "image": "mud.png", + "format": "RGBA8888", + "size": { + "w": 18, + "h": 55 + }, + "scale": 1, + "frames": [ + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 16, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 3, + "w": 16, + "h": 13 + }, + "frame": { + "x": 1, + "y": 1, + "w": 16, + "h": 13 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 16, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 4, + "w": 16, + "h": 12 + }, + "frame": { + "x": 1, + "y": 16, + "w": 16, + "h": 12 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 16, + "h": 16 + }, + "spriteSourceSize": { + "x": 0, + "y": 7, + "w": 16, + "h": 9 + }, + "frame": { + "x": 1, + "y": 30, + "w": 16, + "h": 9 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 16, + "h": 16 + }, + "spriteSourceSize": { + "x": 1, + "y": 3, + "w": 14, + "h": 13 + }, + "frame": { + "x": 1, + "y": 41, + "w": 14, + "h": 13 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:a9f7ae83758a2dffaacdaba2ee9dc2e2:0ebff9db47ce74a0ec049f5d74d589fa:c64f6b8befc3d5e9f836246d2b9536be$" + } +} diff --git a/public/images/mystery-encounters/mud.png b/public/images/mystery-encounters/mud.png new file mode 100644 index 0000000000000000000000000000000000000000..89f174bef74e73d32fd9538b3be7e58339212226 GIT binary patch literal 361 zcmV-v0ha!WP)CYkI;Xo&W#<0d!JMQvg8b*k%9#010qNS#tmY4#WTe4#WYK zD-Ig~008YtL_t(I%cYYG4#OY}L$MRM|B>f}mQUABs}`x1cnyfyZYhrnxU37RZx9Vl zs4h$`wYn$=5vWNAVx^WYNIMZhx{5duwGp6t^tQ7}`_u^a9c`x1y~bI^%yN_2dVjmD z@6pfdypx7&$0kkhpzo+sb90rIo_cXPE|qa9Jtc&!@OW@AL+#>3*{;n*Ir3TMBl<&$ zo3Sf&-7t7Y%bA%Np|O(NmeX6>8s{GS@*wsvdQ=4MigyBM00000NkvXX Hu0mjfv1XH$ literal 0 HcmV?d00001 diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index cb66208a6ff..f9a339db8c2 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -9,7 +9,8 @@ import { PokeballType } from "../../pokeball"; import { getPokemonSpecies } from "../../pokemon-species"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { EnemyPartyConfig, EnemyPokemonConfig, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../mystery-encounter-utils"; +import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; +import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:dark_deal"; diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index 9ad90a92cfb..f3bb422aa64 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -1,7 +1,7 @@ import { leaveEncounterWithoutBattle, setEncounterRewards, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index b0131fa54c5..bc369ee4c4b 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -6,7 +6,7 @@ import { selectPokemonForOption, setEncounterExp, setEncounterRewards, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TempBattleStat } from "#app/data/temp-battle-stat"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 7a96e3f89aa..db7495f6cfa 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -4,10 +4,8 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, - queueEncounterMessage, - setEncounterRewards, - showEncounterText, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import Pokemon from "#app/field/pokemon"; import { ModifierTier } from "#app/modifier/modifier-tier"; @@ -28,6 +26,7 @@ import IMysteryEncounter, { MysteryEncounterTier, } from "../mystery-encounter"; import { MoveRequirement } from "../mystery-encounter-requirements"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:fight_or_flight"; diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 7c7d03b7895..f9fe8ab3d42 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -2,7 +2,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, setEncounterRewards, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { trainerConfigs, TrainerPartyCompoundTemplate, diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index ae6c14f725e..7773e03501b 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -1,11 +1,7 @@ import { - getHighestLevelPlayerPokemon, - koPlayerPokemon, leaveEncounterWithoutBattle, - queueEncounterMessage, - setEncounterRewards, - showEncounterText, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { ModifierTier } from "#app/modifier/modifier-tier"; import { GameOverPhase } from "#app/phases"; import { randSeedInt } from "#app/utils"; @@ -16,6 +12,8 @@ import IMysteryEncounter, { MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; export const MysteriousChestEncounter: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType( @@ -119,11 +117,8 @@ export const MysteriousChestEncounter: IMysteryEncounter = // Does this synchronously so that game over doesn't happen over result message await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result") .then(() => { - if ( - scene.getParty().filter((p) => p.isAllowedInBattle()).length === - 0 - ) { - // All pokemon fainted, game over + if (scene.getParty().filter((p) => p.isAllowedInBattle()).length === 0) { + // All pokemon fainted, game over scene.clearPhaseQueue(); scene.unshiftPhase(new GameOverPhase(scene)); } else { diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 000c5f24f8d..2b7a013dd22 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,4 +1,4 @@ -import { getRandomSpeciesByStarterTier, initFollowupOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, MysteryEncounterVariant } from "../mystery-encounter"; @@ -6,27 +6,95 @@ import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuil import { TrainerSlot } from "#app/data/trainer-config"; import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases"; import i18next from "i18next"; -import { HiddenAbilityRateBoosterModifier, IvScannerModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; import { EnemyPokemon } from "#app/field/pokemon"; -import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; -import { StatusEffect } from "#app/data/status-effect"; -import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; -import { achvs } from "#app/system/achv"; -import { Mode } from "#app/ui/ui"; -import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; -import { BattlerIndex } from "#app/battle"; +import { PokeballType } from "#app/data/pokeball"; import { PlayerGender } from "#enums/player-gender"; import { IntegerHolder, randSeedInt } from "#app/utils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:safari_zone"; +export const SafariZoneEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive + .withIntroSpriteConfigs([ + { + spriteKey: "chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 10, + yShadowOffset: 3 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + ]) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start safari encounter + const encounter = scene.currentBattle.mysteryEncounter; + encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE; + encounter.misc = { + safariPokemonRemaining: 3 + }; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + scene.loadSe("PRSFX- Bug Bite", "battle_anims"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims"); + scene.loadSe("PRSFX- Taunt2", "battle_anims"); + scene.loadAtlas("bait", "mystery-encounters"); + scene.loadAtlas("mud", "mystery-encounters"); + await summonSafariPokemon(scene); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, hideDescription: true }); + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + selected: [ + { + text: `${namespace}_option_2_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + /** - * SAFARI ZONE OPTIONS + * SAFARI ZONE MINIGAME OPTIONS * - * Catch and flee rate **multipliers** are calculated in the same way stat changes are (they range from -6/+6) + * Catch and flee rate **stages** are calculated in the same way stat changes are (they range from -6/+6) * https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone * * Catch Rate calculation: @@ -38,7 +106,7 @@ const namespace = "mysteryEncounter:safari_zone"; * fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2] * Flee chance = fleeRate / 255 */ -const safariZoneOptions: MysteryEncounterOption[] = [ +const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) .withDialogue({ @@ -47,7 +115,7 @@ const safariZoneOptions: MysteryEncounterOption[] = [ selected: [ { text: `${namespace}_pokeball_option_selected`, - }, + } ], }) .withOptionPhase(async (scene: BattleScene) => { @@ -61,7 +129,7 @@ const safariZoneOptions: MysteryEncounterOption[] = [ // Check how many safari pokemon left if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { await summonSafariPokemon(scene); - initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 0, hideDescription: true }); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 0, hideDescription: true }); } else { // End safari mode leaveEncounterWithoutBattle(scene, true); @@ -94,12 +162,10 @@ const safariZoneOptions: MysteryEncounterOption[] = [ // 80% chance to increase flee stage +1 const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); if (!fleeChangeResult) { - scene.queueMessage(i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 0, null, 500); + await showEncounterText(scene, i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 1500, false ); } else { - scene.queueMessage(i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 0, null, 500); + await showEncounterText(scene, i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 1500, false); } - // TODO: throw bait with eat animation - // TODO: play bug bite sfx, maybe spike cannon? await doEndTurn(scene, 1); return true; @@ -125,9 +191,9 @@ const safariZoneOptions: MysteryEncounterOption[] = [ // 80% chance to decrease catch stage -1 const catchChangeResult = tryChangeCatchStage(scene, -1, 8); if (!catchChangeResult) { - scene.queueMessage(i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 0, null, 500); + await showEncounterText(scene, i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 1500, false ); } else { - scene.queueMessage(i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 0, null, 500); + await showEncounterText(scene, i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 1500, false ); } await doEndTurn(scene, 2); @@ -147,7 +213,7 @@ const safariZoneOptions: MysteryEncounterOption[] = [ // Check how many safari pokemon left if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { await summonSafariPokemon(scene); - initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 3, hideDescription: true }); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: 3, hideDescription: true }); } else { // End safari mode leaveEncounterWithoutBattle(scene, true); @@ -157,106 +223,6 @@ const safariZoneOptions: MysteryEncounterOption[] = [ .build() ]; -export const SafariZoneEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) - .withEncounterTier(MysteryEncounterTier.GREAT) - .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 - .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive - .withHideIntroVisuals(false) - .withIntroSpriteConfigs([ - { - spriteKey: "chest_blue", - fileRoot: "mystery-encounters", - hasShadow: true, - x: 4, - y: 10, - yShadowOffset: 3, - disableAnimation: true, // Re-enabled after option select - }, - ]) - .withIntroDialogue([ - { - text: `${namespace}_intro_message`, - }, - ]) - .withTitle(`${namespace}_title`) - .withDescription(`${namespace}_description`) - .withQuery(`${namespace}_query`) - .withOption(new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) - // TODO: update - .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive - .withDialogue({ - buttonLabel: `${namespace}_option_1_label`, - buttonTooltip: `${namespace}_option_1_tooltip`, - selected: [ - { - text: `${namespace}_option_1_selected_message`, - }, - ], - }) - .withOptionPhase(async (scene: BattleScene) => { - // Start safari encounter - const encounter = scene.currentBattle.mysteryEncounter; - encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE; - encounter.misc = { - safariPokemonRemaining: 3 - }; - updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); - scene.loadSe("PRSFX- Bug Bite", "battle_anims"); - scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims"); - scene.loadSe("PRSFX- Taunt2", "battle_anims"); - await hideMysteryEncounterIntroVisuals(scene); - await summonSafariPokemon(scene); - initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, hideDescription: true }); - return true; - }) - .build() - ) - .withSimpleOption( - { - buttonLabel: `${namespace}_option_2_label`, - buttonTooltip: `${namespace}_option_2_tooltip`, - selected: [ - { - text: `${namespace}_option_2_selected_message`, - }, - ], - }, - async (scene: BattleScene) => { - // Leave encounter with no rewards or exp - leaveEncounterWithoutBattle(scene, true); - return true; - } - ) - .build(); - -function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { - return new Promise(resolve => { - const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; - if (introVisuals) { - // Hide - scene.tweens.add({ - targets: introVisuals, - x: "+=16", - y: "-=16", - alpha: 0, - ease: "Sine.easeInOut", - duration: 750, - onComplete: () => { - scene.field.remove(introVisuals); - introVisuals.setVisible(false); - introVisuals.destroy(); - scene.currentBattle.mysteryEncounter.introVisuals = null; - resolve(true); - } - }); - } else { - resolve(true); - } - }); -} - async function summonSafariPokemon(scene: BattleScene) { const encounter = scene.currentBattle.mysteryEncounter; // Message pokemon remaining @@ -295,7 +261,7 @@ async function summonSafariPokemon(scene: BattleScene) { pokemon.calculateStats(); scene.currentBattle.enemyParty[0] = pokemon; - }, scene.currentBattle.waveIndex + encounter.misc.safariPokemonRemaining); + }, scene.currentBattle.waveIndex * 1000 + encounter.misc.safariPokemonRemaining); scene.gameData.setPokemonSeen(pokemon, true); await pokemon.loadAssets(); @@ -308,18 +274,16 @@ async function summonSafariPokemon(scene: BattleScene) { scene.unshiftPhase(new SummonPhase(scene, 0, false)); - scene.ui.showText(i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), null, () => { - const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); - if (ivScannerModifier) { - scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); - } - }, 1500); + showEncounterText(scene, i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), 1500, false) + .then(() => { + const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); + } + }); } -async function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { - const pokeballType: PokeballType = PokeballType.POKEBALL; - const originalY: number = pokemon.y; - +function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { const baseCatchRate = pokemon.species.catchRate; // Catch stage ranges from -6 to +6 (like stat boost stages) const safariCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage; @@ -329,144 +293,18 @@ async function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise const pokeballMultiplier = 1.5; const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier); const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate))); - const fpOffset = pokemon.getFieldPositionOffset(); - const catchSuccess = (ballTwitchRate / 65536) * (ballTwitchRate / 65536) * (ballTwitchRate / 65536); - console.log("Catch success rate: " + catchSuccess); - - const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); - const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); - pokeball.setOrigin(0.5, 0.625); - scene.field.add(pokeball); - - scene.playSound("pb_throw"); - scene.time.delayedCall(300, () => { - scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); - }); - - return new Promise(resolve => { - scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); - scene.time.delayedCall(512, () => { - // Trainer throw frames - scene.trainer.setFrame("2"); - scene.time.delayedCall(256, () => { - scene.trainer.setFrame("3"); - scene.time.delayedCall(768, () => { - scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); - }); - }); - - // Pokeball move and catch logic - scene.tweens.add({ - targets: pokeball, - x: { value: 236 + fpOffset[0], ease: "Linear" }, - y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, - duration: 500, - onComplete: () => { - pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); - scene.playSound("pb_rel"); - pokemon.tint(getPokeballTintColor(pokeballType)); - - addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); - - scene.tweens.add({ - targets: pokemon, - duration: 500, - ease: "Sine.easeIn", - scale: 0.25, - y: 20, - onComplete: () => { - pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - pokemon.setVisible(false); - scene.playSound("pb_catch"); - scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); - - const doShake = () => { - let shakeCount = 0; - const pbX = pokeball.x; - const shakeCounter = scene.tweens.addCounter({ - from: 0, - to: 1, - repeat: 4, - yoyo: true, - ease: "Cubic.easeOut", - duration: 250, - repeatDelay: 500, - onUpdate: t => { - if (shakeCount && shakeCount < 4) { - const value = t.getValue(); - const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; - pokeball.setX(pbX + value * 4 * directionMultiplier); - pokeball.setAngle(value * 27.5 * directionMultiplier); - } - }, - onRepeat: () => { - if (!pokemon.species.isObtainable()) { - shakeCounter.stop(); - failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); - } else if (shakeCount++ < 3) { - if (randSeedInt(65536) < ballTwitchRate) { - scene.playSound("pb_move"); - } else { - shakeCounter.stop(); - failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); - } - } else { - scene.playSound("pb_lock"); - addPokeballCaptureStars(scene, pokeball); - - const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); - pbTint.setOrigin(pokeball.originX, pokeball.originY); - pbTint.setTintFill(0); - pbTint.setAlpha(0); - scene.field.add(pbTint); - scene.tweens.add({ - targets: pbTint, - alpha: 0.375, - duration: 200, - easing: "Sine.easeOut", - onComplete: () => { - scene.tweens.add({ - targets: pbTint, - alpha: 0, - duration: 200, - easing: "Sine.easeIn", - onComplete: () => pbTint.destroy() - }); - } - }); - } - }, - onComplete: () => { - catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); - } - }); - }; - - scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); - } - }); - } - }); - }); - }); + return trainerThrowPokeball(scene, pokemon, PokeballType.POKEBALL, ballTwitchRate); } async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { - // TODO: replace with bait - const pokeballType: PokeballType = PokeballType.POKEBALL; const originalY: number = pokemon.y; const fpOffset = pokemon.getFieldPositionOffset(); - const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); - const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "bait", "0001.png"); bait.setOrigin(0.5, 0.625); scene.field.add(bait); scene.playSound("pb_throw"); - // scene.time.delayedCall(300, () => { - // scene.field.moveBelow(pokemon, pokeball as Phaser.GameObjects.GameObject); - // }); return new Promise(resolve => { scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); @@ -487,31 +325,34 @@ async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { - // Bait frame 2 - bait.setTexture("pb", `${pokeballAtlasKey}_opening`); - // Bait frame 3 - scene.time.delayedCall(17, () => bait.setTexture("pb", `${pokeballAtlasKey}_open`)); - // scene.playSound("pb_rel"); - // pokemon.tint(getPokeballTintColor(pokeballType)); - // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); - scene.time.delayedCall(512, () => { + let index = 1; + scene.time.delayedCall(768, () => { scene.tweens.add({ targets: pokemon, - duration: 200, + duration: 150, ease: "Cubic.easeOut", yoyo: true, - y: originalY - 30, - loop: 2, + y: originalY - 5, + loop: 6, onStart: () => { scene.playSound("PRSFX- Bug Bite"); + bait.setFrame("0002.png"); }, onLoop: () => { - scene.playSound("PRSFX- Bug Bite"); + if (index % 2 === 0) { + scene.playSound("PRSFX- Bug Bite"); + } + if (index === 4) { + bait.setFrame("0003.png"); + } + index++; }, onComplete: () => { - resolve(true); - bait.destroy(); + scene.time.delayedCall(256, () => { + bait.destroy(); + resolve(true); + }); } }); }); @@ -522,13 +363,10 @@ async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { - // TODO: replace with mud - const pokeballType: PokeballType = PokeballType.POKEBALL; const originalY: number = pokemon.y; const fpOffset = pokemon.getFieldPositionOffset(); - const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); - const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "mud", "0001.png"); mud.setOrigin(0.5, 0.625); scene.field.add(mud); @@ -546,21 +384,24 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { - // Bait frame 2 - mud.setTexture("pb", `${pokeballAtlasKey}_opening`); - // Bait frame 3 - scene.time.delayedCall(17, () => mud.setTexture("pb", `${pokeballAtlasKey}_open`)); + // Mud frame 2 scene.playSound("PRSFX- Sludge Bomb2"); - // pokemon.tint(getPokeballTintColor(pokeballType)); + mud.setFrame("0002.png"); + // Mud splat + scene.time.delayedCall(512, () => { + mud.setFrame("0003.png"); + scene.time.delayedCall(512, () => { + mud.setFrame("0004.png"); + }); + }); - // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); scene.time.delayedCall(1536, () => { mud.destroy(); scene.tweens.add({ @@ -587,129 +428,6 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise(resolve => { - scene.playSound("pb_rel"); - pokemon.setY(originalY); - if (pokemon.status?.effect !== StatusEffect.SLEEP) { - pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); - } - pokemon.tint(getPokeballTintColor(pokeballType)); - pokemon.setVisible(true); - pokemon.untint(250, "Sine.easeOut"); - - const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); - pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); - scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); - - scene.tweens.add({ - targets: pokemon, - duration: 250, - ease: "Sine.easeOut", - scale: 1 - }); - - scene.currentBattle.lastUsedPokeball = pokeballType; - removePb(scene, pokeball); - - scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); - }); -} - -async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise { - scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); - - const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); - - if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { - scene.validateAchv(achvs.HIDDEN_ABILITY); - } - - if (pokemon.species.subLegendary) { - scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); - } - - if (pokemon.species.legendary) { - scene.validateAchv(achvs.CATCH_LEGENDARY); - } - - if (pokemon.species.mythical) { - scene.validateAchv(achvs.CATCH_MYTHICAL); - } - - scene.pokemonInfoContainer.show(pokemon, true); - - scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); - - return new Promise(resolve => { - scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { - const end = () => { - scene.pokemonInfoContainer.hide(); - removePb(scene, pokeball); - resolve(); - }; - const removePokemon = () => { - scene.field.remove(pokemon, true); - }; - const addToParty = () => { - const newPokemon = pokemon.addToParty(pokeballType); - const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); - if (scene.getParty().filter(p => p.isShiny()).length === 6) { - scene.validateAchv(achvs.SHINY_PARTY); - } - Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { - scene.updateModifiers(true); - removePokemon(); - if (newPokemon) { - newPokemon.loadAssets().then(end); - } else { - end(); - } - }); - }; - Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { - if (scene.getParty().length === 6) { - const promptRelease = () => { - scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { - scene.pokemonInfoContainer.makeRoomForConfirmUi(); - scene.ui.setMode(Mode.CONFIRM, () => { - scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { - scene.ui.setMode(Mode.MESSAGE).then(() => { - if (slotIndex < 6) { - addToParty(); - } else { - promptRelease(); - } - }); - }); - }, () => { - scene.ui.setMode(Mode.MESSAGE).then(() => { - removePokemon(); - end(); - }); - }); - }); - }; - promptRelease(); - } else { - addToParty(); - } - }); - }, 0, true); - }); -} - -function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { - scene.tweens.add({ - targets: pokeball, - duration: 250, - delay: 250, - ease: "Sine.easeIn", - alpha: 0, - onComplete: () => pokeball.destroy() - }); -} - function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { const speciesCatchRate = pokemon.species.catchRate; const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6)); @@ -720,84 +438,26 @@ function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { return roll < fleeRate; } -async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { - const fleeAnimation = new Promise(resolve => { - // Ease pokemon out - scene.tweens.add({ - targets: pokemon, - x: "+=16", - y: "-=16", - alpha: 0, - duration: 1000, - ease: "Sine.easeIn", - scale: pokemon.getSpriteScale(), - onComplete: () => { - pokemon.setVisible(false); - scene.field.remove(pokemon, true); - resolve(); - } - }); - }); - - const prompt = new Promise(resolve => { - scene.ui.showText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); - }); - - await Promise.all([fleeAnimation, prompt]); -} - -async function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { - const fleeAnimation = new Promise(resolve => { - // Ease pokemon out - scene.tweens.add({ - targets: pokemon, - x: "+=16", - y: "-=16", - alpha: 0, - duration: 1000, - ease: "Sine.easeIn", - scale: pokemon.getSpriteScale(), - onComplete: () => { - pokemon.setVisible(false); - scene.field.remove(pokemon, true); - resolve(); - } - }); - }); - - const prompt = new Promise(resolve => { - scene.ui.showText(i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); - }); - - await Promise.all([fleeAnimation, prompt]); -} - function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean { if (chance && randSeedInt(10) >= chance) { - console.log("Failed to change flee stage"); return false; } const currentFleeStage = scene.currentBattle.mysteryEncounter.misc.fleeStage ?? 0; - // console.log("currentFleeStage: " + currentFleeStage); scene.currentBattle.mysteryEncounter.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); return true; } function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean { if (chance && randSeedInt(10) >= chance) { - console.log("Failed to change catch stage"); return false; } const currentCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage ?? 0; - // console.log("currentCatchStage: " + currentCatchStage); scene.currentBattle.mysteryEncounter.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); return true; } async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: string) { const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; - console.log("fleeStage: " + scene.currentBattle.mysteryEncounter.misc.fleeStage); - console.log("catchStage: " + scene.currentBattle.mysteryEncounter.misc.catchStage); const isFlee = isPokemonFlee(pokemon, scene.currentBattle.mysteryEncounter.misc.fleeStage); if (isFlee) { // Pokemon flees! @@ -805,13 +465,13 @@ async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: stri // Check how many safari pokemon left if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { await summonSafariPokemon(scene); - initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); } else { // End safari mode leaveEncounterWithoutBattle(scene, true); } } else { - scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 500); - initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 1000); + initSubsequentOptionSelect(scene, { overrideOptions: safariZoneGameOptions, startingCursorIndex: cursorIndex, hideDescription: true }); } } diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 8bfd6daa9f9..7e32ca87c55 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -1,4 +1,4 @@ -import { generateModifierTypeOption, leaveEncounterWithoutBattle, queueEncounterMessage, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { StatusEffect } from "#app/data/status-effect"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; @@ -10,6 +10,7 @@ import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:shady_vitamin_dealer"; diff --git a/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts index e0f4d2406e8..9d3659f9443 100644 --- a/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts @@ -10,7 +10,8 @@ import { Status, StatusEffect } from "../../status-effect"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoveRequirement } from "../mystery-encounter-requirements"; -import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, queueEncounterMessage, setEncounterExp, setEncounterRewards, } from "../mystery-encounter-utils"; +import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounter:sleeping_snorlax"; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 6abb340469a..1c0c3633883 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -1,5 +1,5 @@ import { Ability, allAbilities } from "#app/data/ability"; -import { EnemyPartyConfig, getEncounterText, initBattleWithEnemyConfig, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, selectPokemonForOption, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getNatureName, Nature } from "#app/data/nature"; import { speciesStarters } from "#app/data/pokemon-species"; import { Stat } from "#app/data/pokemon-stat"; @@ -16,6 +16,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounter:training_session"; @@ -32,7 +33,9 @@ export const TrainingSessionEncounter: IMysteryEncounter = spriteKey: "training_gear", fileRoot: "mystery-encounters", hasShadow: true, - y: 3, + y: 6, + x: 5, + yShadowOffset: -2 }, ]) .withIntroDialogue([ @@ -162,7 +165,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); - scene.queueMessage(getEncounterText(scene, `${namespace}_battle_finished_1`), null, true); + queueEncounterMessage(scene, `${namespace}_battle_finished_1`); }; setEncounterRewards( @@ -234,11 +237,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage( - getEncounterText(scene, `${namespace}_battle_finished_2`), - null, - true - ); + queueEncounterMessage(scene, `${namespace}_battle_finished_2`); // Add the pokemon back to party with Nature change playerPokemon.setNature(encounter.misc.chosenNature); scene.gameData.setPokemonCaught(playerPokemon, false); @@ -333,7 +332,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage(getEncounterText(scene, `${namespace}_battle_finished_3`), null, true); + queueEncounterMessage(scene, `${namespace}_battle_finished_3`); // Add the pokemon back to party with ability change const abilityIndex = encounter.misc.abilityIndex; if (!!playerPokemon.getFusionSpeciesForm()) { diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 0be21d1520f..45ff955bc33 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -1,4 +1,4 @@ -import { EnemyPartyConfig } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { isNullOrUndefined } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -58,14 +58,16 @@ export default interface IMysteryEncounter { * Requirements */ requirements?: EncounterSceneRequirement[]; + /** Primary Pokemon is a single pokemon randomly selected from the party that meet ALL primary pokemon requirements */ primaryPokemonRequirements?: EncounterPokemonRequirement[]; - secondaryPokemonRequirements?: EncounterPokemonRequirement[]; // A list of requirements that must ALL be met by a subset of pokemon to trigger the event + /** + * Secondary Pokemon are pokemon that meet ALL secondary pokemon requirements + * Note that an individual requirement may require multiple pokemon, but the resulting pokemon after all secondary requirements are met may be lower than expected + * If the primary pokemon and secondary pokemon are the same and ExcludePrimaryFromSupportRequirements flag is true, primary pokemon may be promoted from secondary pool + */ + secondaryPokemonRequirements?: EncounterPokemonRequirement[]; excludePrimaryFromSupportRequirements?: boolean; - // Primary Pokemon is a single pokemon randomly selected from a set of pokemon that meet ALL primary pokemon requirements primaryPokemon?: PlayerPokemon; - // Support Pokemon are pokemon that meet ALL support pokemon requirements. - // Note that an individual requirement may require multiple pokemon, but the resulting pokemon after all secondary requirements are met may be lower than expected - // If the primary pokemon and supporting pokemon are the same and ExcludePrimaryFromSupportRequirements flag is true, primary pokemon may be promoted from secondary pool secondaryPokemon?: PlayerPokemon[]; /** @@ -118,19 +120,11 @@ export default interface IMysteryEncounter { * Defaults to 1 */ expMultiplier?: number; - - /** - * When true, will never queue PostSummon phases from a SummonPhase - * Defaults to false - */ - disableAllPostSummon?: boolean; - /** * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave * You should never need to modify this */ seedOffset?: any; - /** * Generic property to set any custom data required for the encounter * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts new file mode 100644 index 00000000000..1042984533d --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -0,0 +1,72 @@ +import BattleScene from "#app/battle-scene"; +import { getTextWithColors, TextStyle } from "#app/ui/text"; +import { UiTheme } from "#enums/ui-theme"; +import { isNullOrUndefined } from "#app/utils"; +import i18next from "i18next"; + +export function getEncounterText(scene: BattleScene, keyOrString: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { + if (isNullOrUndefined(keyOrString)) { + return null; + } + + let textString: string = getTextWithDialogueTokens(scene, keyOrString); + + // Can only color the text if a Primary Style is defined + // primaryStyle is applied to all text that does not have its own specified style + if (primaryStyle) { + textString = getTextWithColors(textString, primaryStyle, uiTheme); + } + + return textString; +} + +function getTextWithDialogueTokens(scene: BattleScene, keyOrString: string): string { + if (isNullOrUndefined(keyOrString)) { + return null; + } + + if (i18next.exists(keyOrString, scene.currentBattle?.mysteryEncounter?.dialogueTokens)) { + const stringArray = [`${keyOrString}`] as any; + stringArray.raw = [`${keyOrString}`]; + return i18next.t(stringArray, scene.currentBattle?.mysteryEncounter?.dialogueTokens); + } + + return keyOrString; +} + +/** + * Will queue a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + */ +export function queueEncounterMessage(scene: BattleScene, contentKey: string): void { + const text: string = getEncounterText(scene, contentKey); + scene.queueMessage(text, null, true); +} + +/** + * Will display a message in UI with injected encounter data tokens + * @param scene + * @param contentKey + * @param prompt + * @param callbackDelay + */ +export function showEncounterText(scene: BattleScene, contentKey: string, callbackDelay: number = 0, prompt: boolean = true): Promise { + return new Promise(resolve => { + const text: string = getEncounterText(scene, contentKey); + scene.ui.showText(text, null, () => resolve(), callbackDelay, prompt); + }); +} + +/** + * Will display a dialogue (with speaker title) in UI with injected encounter data tokens + * @param scene + * @param textContentKey + * @param speakerContentKey + * @param callback + */ +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callback?: Function) { + const text: string = getEncounterText(scene, textContentKey); + const speaker: string = getEncounterText(scene, speakerContentKey); + scene.ui.showDialogue(text, speaker, null, callback, 0, 0); +} diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts similarity index 76% rename from src/data/mystery-encounters/mystery-encounter-utils.ts rename to src/data/mystery-encounters/utils/encounter-phase-utils.ts index e0c8f73e569..100c25aa45d 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,21 +1,19 @@ import i18next from "i18next"; import { BattleType } from "#app/battle"; -import BattleScene from "../../battle-scene"; -import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "../pokemon-species"; -import { MysteryEncounterVariant } from "./mystery-encounter"; -import { Status, StatusEffect } from "../status-effect"; -import { TrainerConfig, trainerConfigs, TrainerSlot } from "../trainer-config"; +import BattleScene from "../../../battle-scene"; +import PokemonSpecies from "../../pokemon-species"; +import { MysteryEncounterVariant } from "../mystery-encounter"; +import { Status, StatusEffect } from "../../status-effect"; +import { TrainerConfig, trainerConfigs, TrainerSlot } from "../../trainer-config"; import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; -import Trainer, { TrainerVariant } from "../../field/trainer"; +import Trainer, { TrainerVariant } from "../../../field/trainer"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BattleEndPhase, EggLapsePhase, ExpPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; import { MysteryEncounterBattlePhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; -import * as Utils from "../../utils"; +import * as Utils from "../../../utils"; import { isNullOrUndefined } from "#app/utils"; import { TrainerType } from "#enums/trainer-type"; -import { Species } from "#enums/species"; -import { Type } from "#app/data/type"; import { BattlerTagType } from "#enums/battler-tag-type"; import PokemonData from "#app/system/pokemon-data"; import { Biome } from "#enums/biome"; @@ -24,201 +22,9 @@ import { Mode } from "#app/ui/ui"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; -import { getTextWithColors, TextStyle } from "#app/ui/text"; import * as Overrides from "#app/overrides"; -import { UiTheme } from "#enums/ui-theme"; -import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; - -/** - * - * Will never remove the player's last non-fainted Pokemon (if they only have 1) - * Otherwise, picks a Pokemon completely at random and removes from the party - * @param scene - * @param isAllowedInBattle - default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon - * @param doNotReturnLastAbleMon - If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) - * @returns - */ -export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon { - const party = scene.getParty(); - let chosenIndex: number; - let chosenPokemon: PlayerPokemon; - const unfaintedMons = party.filter(p => p.isAllowedInBattle()); - const faintedMons = party.filter(p => !p.isAllowedInBattle()); - - if (doNotReturnLastAbleMon && unfaintedMons.length === 1) { - chosenIndex = Utils.randSeedInt(faintedMons.length); - chosenPokemon = faintedMons.at(chosenIndex); - } else if (isAllowedInBattle) { - chosenIndex = Utils.randSeedInt(unfaintedMons.length); - chosenPokemon = unfaintedMons.at(chosenIndex); - } else { - chosenIndex = Utils.randSeedInt(party.length); - chosenPokemon = party.at(chosenIndex); - } - - return chosenPokemon; -} - -// export function getTokensFromScene(scene: BattleScene, reqs: EncounterSceneRequirement[]): Array<[RegExp, String]> { -// const arr = []; -// if (scene) { -// for (const req of reqs) { -// req.getDialogueToken(scene); -// } -// } -// return arr; -// } - -/** - * Ties are broken by whatever mon is closer to the front of the party - * @param scene - * @param unfainted - default false. If true, only picks from unfainted mons. - * @returns - */ -export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); - let pokemon: PlayerPokemon; - party.every(p => { - if (unfainted && p.isFainted()) { - return true; - } - - pokemon = pokemon ? pokemon?.level < p?.level ? p : pokemon : p; - return true; - }); - - return pokemon; -} - -/** - * Ties are broken by whatever mon is closer to the front of the party - * @param scene - * @param unfainted - default false. If true, only picks from unfainted mons. - * @returns - */ -export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { - const party = scene.getParty(); - let pokemon: PlayerPokemon; - party.every(p => { - if (unfainted && p.isFainted()) { - return true; - } - - pokemon = pokemon ? pokemon?.level > p?.level ? p : pokemon : p; - return true; - }); - - return pokemon; -} - -/** - * - * NOTE: This returns ANY random species, including those locked behind eggs, etc. - * @param starterTiers - * @param excludedSpecies - * @param types - * @returns - */ -export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species { - let min = starterTiers instanceof Array ? starterTiers[0] : starterTiers; - let max = starterTiers instanceof Array ? starterTiers[1] : starterTiers; - - let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters) - .map(s => [parseInt(s) as Species, speciesStarters[s] as number]) - .filter(s => getPokemonSpecies(s[0]) && (!excludedSpecies || !excludedSpecies.includes(s[0]))) - .map(s => [getPokemonSpecies(s[0]), s[1]]); - - if (!isNullOrUndefined(types) && types.length > 0) { - filteredSpecies = filteredSpecies.filter(s => types.includes(s[0].type1) || types.includes(s[0].type2)); - } - - // If no filtered mons exist at specified starter tiers, will expand starter search range until there are - // Starts by decrementing starter tier min until it is 0, then increments tier max up to 10 - let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max)); - while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) { - if (min > 0) { - min--; - } else { - max++; - } - - tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max); - } - - if (tryFilterStarterTiers.length > 0) { - const index = Utils.randSeedInt(tryFilterStarterTiers.length); - return Phaser.Math.RND.shuffle(tryFilterStarterTiers)[index][0].speciesId; - } - - return Species.BULBASAUR; -} - -export function koPlayerPokemon(pokemon: PlayerPokemon) { - pokemon.hp = 0; - pokemon.trySetStatus(StatusEffect.FAINT); - pokemon.updateInfo(); -} - -export function getEncounterText(scene: BattleScene, textKey: string, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { - if (isNullOrUndefined(textKey)) { - return null; - } - - const stringArray = [`${textKey}`] as any; - stringArray.raw = [`${textKey}`]; - let textString: string = getTextWithDialogueTokens(scene, stringArray); - - // Can only color the text if a Primary Style is defined - // primaryStyle is applied to all text that does not have its own specified style - if (primaryStyle) { - textString = getTextWithColors(textString, primaryStyle, uiTheme); - } - - return textString; -} - -function getTextWithDialogueTokens(scene: BattleScene, textKey: TemplateStringsArray): string { - if (isNullOrUndefined(textKey)) { - return null; - } - - return i18next.t(textKey, scene.currentBattle?.mysteryEncounter?.dialogueTokens); -} - -/** - * Will queue a message in UI with injected encounter data tokens - * @param scene - * @param contentKey - */ -export function queueEncounterMessage(scene: BattleScene, contentKey: string): void { - const text: string = getEncounterText(scene, contentKey); - scene.queueMessage(text, null, true); -} - -/** - * Will display a message in UI with injected encounter data tokens - * @param scene - * @param contentKey - */ -export function showEncounterText(scene: BattleScene, contentKey: string): Promise { - return new Promise(resolve => { - const text: string = getEncounterText(scene, contentKey); - scene.ui.showText(text, null, () => resolve(), 0, true); - }); -} - -/** - * Will display a dialogue (with speaker title) in UI with injected encounter data tokens - * @param scene - * @param textContentKey - * @param speakerContentKey - * @param callback - */ -export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callback?: Function) { - const text: string = getEncounterText(scene, textContentKey); - const speaker: string = getEncounterText(scene, speakerContentKey); - scene.ui.showDialogue(text, speaker, null, callback, 0, 0); -} +import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; export class EnemyPokemonConfig { species: PokemonSpecies; @@ -525,8 +331,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p if (!textPromptKey) { displayOptions(); } else { - const secondOptionSelectPrompt = getEncounterText(scene, textPromptKey, TextStyle.MESSAGE); - scene.ui.showText(secondOptionSelectPrompt, null, displayOptions, null, true); + showEncounterText(scene, textPromptKey).then(() => displayOptions()); } }); }); @@ -680,14 +485,24 @@ export function setEncounterExp(scene: BattleScene, participantIds: integer[], b }; } +export class OptionSelectSettings { + hideDescription?: boolean; + slideInDescription?: boolean; + overrideTitle?: string; + overrideDescription?: string; + overrideQuery?: string; + overrideOptions?: MysteryEncounterOption[]; + startingCursorIndex?: number; +} + /** - * Can be used to exit an encounter without any battles or followup - * Will skip any shops and rewards, and queue the next encounter phase as normal + * Can be used to queue a new series of Options to select for an Encounter + * MUST be used only in onOptionPhase, will not work in onPreOptionPhase or onPostOptionPhase * @param scene - * @param followupOptionSelectSettings + * @param optionSelectSettings */ -export function initFollowupOptionSelect(scene: BattleScene, followupOptionSelectSettings: MysteryEncounterUiSettings) { - scene.pushPhase(new MysteryEncounterPhase(scene, followupOptionSelectSettings)); +export function initSubsequentOptionSelect(scene: BattleScene, optionSelectSettings: OptionSelectSettings) { + scene.pushPhase(new MysteryEncounterPhase(scene, optionSelectSettings)); } /** @@ -721,6 +536,32 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: } } +export function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { + return new Promise(resolve => { + const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; + if (introVisuals) { + // Hide + scene.tweens.add({ + targets: introVisuals, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(introVisuals); + introVisuals.setVisible(false); + introVisuals.destroy(); + scene.currentBattle.mysteryEncounter.introVisuals = null; + resolve(true); + } + }); + } else { + resolve(true); + } + }); +} + /** * TODO: remove once encounter spawn rate is finalized * Just a helper function to calculate aggregate stats for MEs in a Classic run diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts new file mode 100644 index 00000000000..fdbb957161f --- /dev/null +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -0,0 +1,451 @@ +import BattleScene from "#app/battle-scene"; +import i18next from "i18next"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { VictoryPhase } from "#app/phases"; +import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { PlayerGender } from "#enums/player-gender"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; +import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect"; +import { BattlerIndex } from "#app/battle"; +import { achvs } from "#app/system/achv"; +import { Mode } from "#app/ui/ui"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { Species } from "#enums/species"; +import { Type } from "#app/data/type"; +import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; +import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; + +/** + * + * Will never remove the player's last non-fainted Pokemon (if they only have 1) + * Otherwise, picks a Pokemon completely at random and removes from the party + * @param scene + * @param isAllowedInBattle - default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon + * @param doNotReturnLastAbleMon - If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) + * @returns + */ +export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let chosenIndex: number; + let chosenPokemon: PlayerPokemon; + const unfaintedMons = party.filter(p => p.isAllowedInBattle()); + const faintedMons = party.filter(p => !p.isAllowedInBattle()); + + if (doNotReturnLastAbleMon && unfaintedMons.length === 1) { + chosenIndex = randSeedInt(faintedMons.length); + chosenPokemon = faintedMons.at(chosenIndex); + } else if (isAllowedInBattle) { + chosenIndex = randSeedInt(unfaintedMons.length); + chosenPokemon = unfaintedMons.at(chosenIndex); + } else { + chosenIndex = randSeedInt(party.length); + chosenPokemon = party.at(chosenIndex); + } + + return chosenPokemon; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param unfainted - default false. If true, only picks from unfainted mons. + * @returns + */ +export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon; + party.every(p => { + if (unfainted && p.isFainted()) { + return true; + } + + pokemon = pokemon ? pokemon?.level < p?.level ? p : pokemon : p; + return true; + }); + + return pokemon; +} + +/** + * Ties are broken by whatever mon is closer to the front of the party + * @param scene + * @param unfainted - default false. If true, only picks from unfainted mons. + * @returns + */ +export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { + const party = scene.getParty(); + let pokemon: PlayerPokemon; + party.every(p => { + if (unfainted && p.isFainted()) { + return true; + } + + pokemon = pokemon ? pokemon?.level > p?.level ? p : pokemon : p; + return true; + }); + + return pokemon; +} + +/** + * + * NOTE: This returns ANY random species, including those locked behind eggs, etc. + * @param starterTiers + * @param excludedSpecies + * @param types + * @returns + */ +export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species { + let min = starterTiers instanceof Array ? starterTiers[0] : starterTiers; + let max = starterTiers instanceof Array ? starterTiers[1] : starterTiers; + + let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters) + .map(s => [parseInt(s) as Species, speciesStarters[s] as number]) + .filter(s => getPokemonSpecies(s[0]) && (!excludedSpecies || !excludedSpecies.includes(s[0]))) + .map(s => [getPokemonSpecies(s[0]), s[1]]); + + if (!isNullOrUndefined(types) && types.length > 0) { + filteredSpecies = filteredSpecies.filter(s => types.includes(s[0].type1) || types.includes(s[0].type2)); + } + + // If no filtered mons exist at specified starter tiers, will expand starter search range until there are + // Starts by decrementing starter tier min until it is 0, then increments tier max up to 10 + let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max)); + while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) { + if (min > 0) { + min--; + } else { + max++; + } + + tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max); + } + + if (tryFilterStarterTiers.length > 0) { + const index = randSeedInt(tryFilterStarterTiers.length); + return Phaser.Math.RND.shuffle(tryFilterStarterTiers)[index][0].speciesId; + } + + return Species.BULBASAUR; +} + +export function koPlayerPokemon(pokemon: PlayerPokemon) { + pokemon.hp = 0; + pokemon.trySetStatus(StatusEffect.FAINT); + pokemon.updateInfo(); +} + +/** + * Alternative to using AttemptCapturePhase + * Assumes player sprite is visible on the screen (this is intended for non-combat uses) + * + * Can await returned promise to wait for throw animation completion before continuing + * + * @param scene + * @param pokemon + * @param pokeballType + * @param ballTwitchRate - can pass custom ball catch rates (for special events, like safari) + */ +export function trainerThrowPokeball(scene: BattleScene, pokemon: EnemyPokemon, pokeballType: PokeballType, ballTwitchRate?: number): Promise { + const originalY: number = pokemon.y; + + if (!ballTwitchRate) { + const _3m = 3 * pokemon.getMaxHp(); + const _2h = 2 * pokemon.hp; + const catchRate = pokemon.species.catchRate; + const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType); + const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1; + const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier); + ballTwitchRate = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x))); + } + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + scene.playSound("pb_throw"); + scene.time.delayedCall(300, () => { + scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); + }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: pokeball, + x: { value: 236 + fpOffset[0], ease: "Linear" }, + y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("pb_rel"); + pokemon.tint(getPokeballTintColor(pokeballType)); + + addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + + scene.tweens.add({ + targets: pokemon, + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + y: 20, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + pokemon.setVisible(false); + scene.playSound("pb_catch"); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); + + const doShake = () => { + let shakeCount = 0; + const pbX = pokeball.x; + const shakeCounter = scene.tweens.addCounter({ + from: 0, + to: 1, + repeat: 4, + yoyo: true, + ease: "Cubic.easeOut", + duration: 250, + repeatDelay: 500, + onUpdate: t => { + if (shakeCount && shakeCount < 4) { + const value = t.getValue(); + const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; + pokeball.setX(pbX + value * 4 * directionMultiplier); + pokeball.setAngle(value * 27.5 * directionMultiplier); + } + }, + onRepeat: () => { + if (!pokemon.species.isObtainable()) { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } else if (shakeCount++ < 3) { + if (randSeedInt(65536) < ballTwitchRate) { + scene.playSound("pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("pb_lock"); + addPokeballCaptureStars(scene, pokeball); + + const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); + pbTint.setOrigin(pokeball.originX, pokeball.originY); + pbTint.setTintFill(0); + pbTint.setAlpha(0); + scene.field.add(pbTint); + scene.tweens.add({ + targets: pbTint, + alpha: 0.375, + duration: 200, + easing: "Sine.easeOut", + onComplete: () => { + scene.tweens.add({ + targets: pbTint, + alpha: 0, + duration: 200, + easing: "Sine.easeIn", + onComplete: () => pbTint.destroy() + }); + } + }); + } + }, + onComplete: () => { + catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); + } + }); + }; + + scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); + } + }); + } + }); + }); + }); +} + +function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("pb_rel"); + pokemon.setY(originalY); + if (pokemon.status?.effect !== StatusEffect.SLEEP) { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + } + pokemon.tint(getPokeballTintColor(pokeballType)); + pokemon.setVisible(true); + pokemon.untint(250, "Sine.easeOut"); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + + scene.currentBattle.lastUsedPokeball = pokeballType; + removePb(scene, pokeball); + + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); + }); +} + +function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise { + scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); + + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.pokemonInfoContainer.show(pokemon, true); + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + + return new Promise(resolve => { + scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + const end = () => { + scene.pokemonInfoContainer.hide(); + removePb(scene, pokeball); + resolve(); + }; + const removePokemon = () => { + scene.field.remove(pokemon, true); + }; + const addToParty = () => { + const newPokemon = pokemon.addToParty(pokeballType); + const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + if (scene.getParty().filter(p => p.isShiny()).length === 6) { + scene.validateAchv(achvs.SHINY_PARTY); + } + Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { + scene.updateModifiers(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } + }); + }; + Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { + if (scene.getParty().length === 6) { + const promptRelease = () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { + scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.ui.setMode(Mode.CONFIRM, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (slotIndex < 6) { + addToParty(); + } else { + promptRelease(); + } + }); + }); + }, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + removePokemon(); + end(); + }); + }); + }); + }; + promptRelease(); + } else { + addToParty(); + } + }); + }, 0, true); + }); +} + +function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => pokeball.destroy() + }); +} + +export function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + return new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} + +export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + return new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 600, false) + .then(() => { + resolve(); + }); + } + }); + }); +} diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index a13e35a4474..67b2a6c8579 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -146,13 +146,13 @@ export const mysteryEncounter: SimpleTranslationEntries = { "safari_zone_intro_message": "It's a safari zone!", "safari_zone_title": "The Safari Zone", - "safari_zone_description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\nBeware, though. These Pokémon may flee before you're able to catch them!", + "safari_zone_description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\n\nBeware, though. These Pokémon may flee before you're able to catch them!", "safari_zone_query": "Would you like to enter?", "safari_zone_option_1_label": "Enter", "safari_zone_option_1_tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", "safari_zone_option_2_label": "Leave", "safari_zone_option_2_tooltip": "(-) No Rewards", - "safari_zone_option_1_selected_message": "Time to test your luck.", + "safari_zone_option_1_selected_message": "Time to test your luck!", "safari_zone_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.", "safari_zone_pokeball_option_label": "Throw a Pokéball", "safari_zone_pokeball_option_tooltip": "(+) Throw a Pokéball", diff --git a/src/phases.ts b/src/phases.ts index 9bc61aa8454..f98c914e320 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -66,9 +66,10 @@ import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { MysteryEncounterVariant } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phase"; -import { getEncounterText, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import { isNullOrUndefined } from "./utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; const { t } = i18next; @@ -1680,9 +1681,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - const addPostSummonForEncounter = this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter?.disableAllPostSummon; - - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1 || addPostSummonForEncounter) { + if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1 || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phase.ts index 96acce6adcc..13856471e4d 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phase.ts @@ -2,9 +2,7 @@ import i18next from "i18next"; import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; -import { - getEncounterText -} from "../data/mystery-encounters/mystery-encounter-utils"; +import { hideMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; import { CheckSwitchPhase, NewBattlePhase, ReturnPhase, ScanIvsPhase, SelectModifierPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases"; import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterVariant } from "../data/mystery-encounters/mystery-encounter"; @@ -15,7 +13,7 @@ import { Tutorial, handleTutorial } from "../tutorial"; import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; -import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** * Will handle (in order): @@ -27,11 +25,17 @@ import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler * - Queuing of the MysteryEncounterOptionSelectedPhase */ export class MysteryEncounterPhase extends Phase { - followupOptionSelectSettings: MysteryEncounterUiSettings; + optionSelectSettings: OptionSelectSettings; - constructor(scene: BattleScene, followupOptionSelectSettings?: MysteryEncounterUiSettings) { + /** + * + * @param scene + * @param optionSelectSettings - allows overriding the typical options of an encounter with new ones + * Mostly useful for having repeated queries during a single encounter, where the queries and options may differ each time + */ + constructor(scene: BattleScene, optionSelectSettings?: OptionSelectSettings) { super(scene); - this.followupOptionSelectSettings = followupOptionSelectSettings; + this.optionSelectSettings = optionSelectSettings; } start() { @@ -45,14 +49,14 @@ export class MysteryEncounterPhase extends Phase { const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000; this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512; - if (!this.followupOptionSelectSettings) { + if (!this.optionSelectSettings) { // Sets flag that ME was encountered, only if this is not a followup option select phase // Can be used in later MEs to check for requirements to spawn, etc. this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]); } // Initiates encounter dialogue window and option select - this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.followupOptionSelectSettings); + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.optionSelectSettings); } handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { @@ -144,7 +148,7 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { start() { super.start(); if (this.scene.currentBattle.mysteryEncounter.hideIntroVisuals) { - this.hideMysteryEncounterIntroVisuals().then(() => { + hideMysteryEncounterIntroVisuals(this.scene).then(() => { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); @@ -159,32 +163,6 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { }, this.scene.currentBattle.mysteryEncounter.seedOffset); } } - - hideMysteryEncounterIntroVisuals(): Promise { - return new Promise(resolve => { - const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals; - if (introVisuals) { - // Hide - this.scene.tweens.add({ - targets: introVisuals, - x: "+=16", - y: "-=16", - alpha: 0, - ease: "Sine.easeInOut", - duration: 750, - onComplete: () => { - this.scene.field.remove(introVisuals); - introVisuals.setVisible(false); - introVisuals.destroy(); - this.scene.currentBattle.mysteryEncounter.introVisuals = null; - resolve(true); - } - }); - } else { - resolve(true); - } - }); - } } /** diff --git a/src/pipelines/sprite.ts b/src/pipelines/sprite.ts index e36765f0d4c..79da2ad9a39 100644 --- a/src/pipelines/sprite.ts +++ b/src/pipelines/sprite.ts @@ -452,7 +452,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set1f("vCutoff", v1); const hasShadow = sprite.pipelineData["hasShadow"] as boolean; - const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number; + const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number ?? 0; if (hasShadow) { const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 31f930319bf..22582f8a8bc 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -1,11 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import GameManager from "#app/test/utils/gameManager"; import Phaser from "phaser"; -import { - getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, - getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getEncounterText, - koPlayerPokemon, queueEncounterMessage, showEncounterDialogue, showEncounterText, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; @@ -14,6 +9,8 @@ import IMysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MessagePhase } from "#app/phases"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Type } from "#app/data/type"; +import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, getRandomPlayerPokemon, getRandomSpeciesByStarterTier, koPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { getEncounterText, queueEncounterMessage, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; describe("Mystery Encounter Utils", () => { let phaserGame: Phaser.Game; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 800dd551dc4..d1ce9fb5d04 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -10,17 +10,8 @@ import MysteryEncounterOption, { EncounterOptionMode } from "../data/mystery-enc import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; -import { getEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; - -export class MysteryEncounterUiSettings { - hideDescription?: boolean; - slideInDescription?: boolean; - overrideTitle?: string; - overrideDescription?: string; - overrideQuery?: string; - overrideOptions?: MysteryEncounterOption[]; - startingCursorIndex?: number; -} +import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -37,7 +28,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { private descriptionScrollTween: Phaser.Tweens.Tween; private rarityBall: Phaser.GameObjects.Sprite; - private overrideSettings: MysteryEncounterUiSettings; + private overrideSettings: OptionSelectSettings; private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; @@ -81,7 +72,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { show(args: any[]): boolean { super.show(args); - this.overrideSettings = args[0] as MysteryEncounterUiSettings ?? {}; + this.overrideSettings = args[0] as OptionSelectSettings ?? {}; const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription; const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription; const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; @@ -100,7 +91,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (this.blockInput) { setTimeout(() => { this.unblockInput(); - }, 1500); + }, 1000); } this.displayOptionTooltip(); @@ -120,7 +111,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (cursor === this.viewPartyIndex) { // Handle view party success = true; - const overrideSettings: MysteryEncounterUiSettings = { + const overrideSettings: OptionSelectSettings = { ...this.overrideSettings, slideInDescription: false }; @@ -387,7 +378,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { descriptionTextMaskRect.setScale(6); descriptionTextMaskRect.fillStyle(0xFFFFFF); descriptionTextMaskRect.beginPath(); - descriptionTextMaskRect.fillRect(6, 54, 206, 60); + descriptionTextMaskRect.fillRect(6, 53, 206, 57); const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask();