From e822c91a1641a17b6c5da3e49d38a6f1440efe01 Mon Sep 17 00:00:00 2001 From: James Diefenbach <105332964+j-diefenbach@users.noreply.github.com> Date: Thu, 5 Sep 2024 02:59:25 +1000 Subject: [PATCH] [Enhancement] Skip egg hatching and show summary (#3726) * cherry picked commits / manual copy * better dex tracking for summary after regular egg hatching * ui changes * updated egg hatch bg, added candy tracker, icon anims for new shiny or new form unlock * added i18 line, reset overrides * touchup * code cleanup, documentation and slight refactor * sprite display fix * load interrupts, simple sfx and no summary for small egg amounts * Garbage Collection + Eslint/Docs approved. * time logging and optimisation * skip redundant load * more time logs and fix pre-load issues * more detailed loading logs * changed loading to be on demand from cursor nav * fix missing variant icon fallback * removing redundant time logs and code touchup * code cleanup * Comments so developer doesn't get bugged about garbage collecton * remove logs n stuff * lang settings touchup and final touchup plus uploading blank egg summary bg * fix nits, js imports, extra docs, magic numbers changed * extra docs and spacing nits * Update Github --------- Co-authored-by: James Diefenbach Co-authored-by: Frederico Santos Co-authored-by: frutescens Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com> --- public/images/ui/egg_summary_bg.png | Bin 0 -> 2160 bytes public/images/ui/egg_summary_bg_blank.png | Bin 0 -> 1628 bytes public/images/ui/icon_egg_move.png | Bin 0 -> 237 bytes public/images/ui/legacy/egg_summary_bg.png | Bin 0 -> 2160 bytes public/images/ui/legacy/icon_egg_move.png | Bin 0 -> 237 bytes src/battle-scene.ts | 23 ++ src/data/egg-hatch-data.ts | 98 +++++++ src/loading-scene.ts | 2 + src/locales/en/battle.json | 1 + src/phases/egg-hatch-phase.ts | 52 ++-- src/phases/egg-lapse-phase.ts | 129 ++++++++- src/phases/egg-summary-phase.ts | 50 ++++ src/system/game-data.ts | 24 +- src/ui/egg-summary-ui-handler.ts | 320 +++++++++++++++++++++ src/ui/pokemon-hatch-info-container.ts | 189 ++++++++++++ src/ui/pokemon-info-container.ts | 98 +++++-- src/ui/ui.ts | 3 + 17 files changed, 913 insertions(+), 76 deletions(-) create mode 100644 public/images/ui/egg_summary_bg.png create mode 100644 public/images/ui/egg_summary_bg_blank.png create mode 100644 public/images/ui/icon_egg_move.png create mode 100644 public/images/ui/legacy/egg_summary_bg.png create mode 100644 public/images/ui/legacy/icon_egg_move.png create mode 100644 src/data/egg-hatch-data.ts create mode 100644 src/phases/egg-summary-phase.ts create mode 100644 src/ui/egg-summary-ui-handler.ts create mode 100644 src/ui/pokemon-hatch-info-container.ts diff --git a/public/images/ui/egg_summary_bg.png b/public/images/ui/egg_summary_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..27e367212aaae20dc43f3f03c9fc122b9d99e8fe GIT binary patch literal 2160 zcmeHJX;e~a7``Bh6l&PyQaYxh)>L$7YKn0IQ%r3!+cZrAZ9A0)R-zP0SW{yV3(EE!?`+RAd-t_+{oGP=b#ZlJd*uMGw+m3z2H7H?3iM=#aKx4ySq z<|boL+7i{0bE`dp;m?=%D3ygno=<7z?WDHk93mpBcte&^oL;IRtmY+-Nn7>A?tY^y z29uPO)H+tduDQw7_chQ=2NjhxlTmK!FqE(<(|fgyy?e{{>BVw3sjjgxL;ih^?$r+B zXkT+a-rrwCd z=$u|JG^r4)qpzs|m6oYqvGW?s<_?Q;`*9-QTV6@TGstZf;v;MB(xFeu6%`B+}SUtLEo5jW%>rL}GO4fTpIi$6zotcmxdJG%%bn7(GW(nW%@=geC*w zZrqWPku4mKM7K?febv0ZTrKAlE5+Fq9|9q}O-N}lnbKl6o5o0--}QpS^s3l99FuOK zf$h#Ea5?V@0Z(333y4!`)dog8@ide*&euH}o)eUr+??*na3BG#VT9-q1j#tCr-jD4ZU!Ia?RJ0%!E#l1e$U=nWu2WK!2ywI;OOL;y zy)7yqaDND!&E=lM($nX2FW^w;H>z1Bnpgjpnis<2>_5YW@;uRx4)rA0y>^x1bKjJE zg6O56>O-6R?QRG-pyF&unvK8)>_KY#FJpEov-*6`i0U0G^;9k9O?L2W3D00q`HTxV+Vy>*S3> zCx0M!O z&Ws4Zo=kYmcuP*5?s#?{>ii*Sx?ZnTLIIdA(7Ec-_#C%-4<5LcT_2ChtiMRKg7L}- zTn~5q%wQ7fdgR;_@DLI_hzxNwI0!=iEpZY~tSZ$y7aq1fYbTrlMLY7b)ElHSVeynZ zjm5)Y7J=I|SDGc?RdGrhaq<(G(HJVV3`>VY>{XwUQY2}KOn<|qiIES4Tjccqd>@tS zvFJhXhL8Q%hr@+!yJ^*vykwQK?`^lH8bv_Ws^XxM>QJBEs zS2~n4#+ZL0s`yL@#Qu0~R%)ta@xyDKSyAJ*=j$F{75kci>{nV`u7gTJeW=Z6!}I5L zGcPRMHQsj!DV9uTdMpB0MMXu4T_P>TF7^*}Z=Vhfb=PMOAeAfvjqPs;Y)i&qy2R-X z>S|n{_T32;U~6MzZnG59=$;y(GO1dib)E&1Z33ue?;V!LP^h4 literal 0 HcmV?d00001 diff --git a/public/images/ui/egg_summary_bg_blank.png b/public/images/ui/egg_summary_bg_blank.png new file mode 100644 index 0000000000000000000000000000000000000000..09bcb63cfa3b515afe0207812b0b46774f79ad80 GIT binary patch literal 1628 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yow{Wll$)``g3IZv{;vjb?hIQv;UNSJSiFvv> zhE&XXd)IK1l)DJqgKgan0!Ak+m}DFm3bpfb)(XfvD^5H$@s#*M_Zd8!ZWb6>7rc#7 zzM3W8xvX-}`|f$GME0y*`}eB7?X15~|Nj2|U!Rr_|b z>|CCHX5YH9n_3_GPyFL&oAT+~;W^O{7(Oq3_fds^d(lZng%E~MW#3CCwMXy#;$#rQ zu3*ztfq|Iuya$;3DV7l@EW7>kg0QCdcLPJ>FjtglqFs}1t2dzP==S##->b+2#b6*X3`SgK`k#C#{~zIJK<&Cv-vrUvvMWQe)^MC^y$ zt9d_fORjJRJ95PVhbXYiM!CQ!!xb9IHjH{=-Md#*KYOXev2dOzLs5NA{DOI&toL%v z9&P@eOKem_rL3YXV|ebQgdm|j}HsiTz?HR@$$=%Khs@z*ojJbcr!3%aylri^<0bv%B@r)9I|OztT6~+;X=oyfil4OY`V= zuDZDErKy{5{;6k*(VMRO{h{@*ee?g^1d96s#U+z_o6MEe4-&sVJ=CfKVDNPHb6Mw<&;$Sif4F%}28J29*~C-V}>S?=lL7@`rJ zoZ`TI?bx;dj!y39C`Mb@JW4}PIASEAB=}Dhx;GQI>6#Fao1Y;h(-l_ z{uy=uKXT8ww=+j5!Z2dtmKk5_zpt>_(6YIgamKysCrqoHCS)4C?kNeCI2XH*IjrGK zoZ;TyM#0ELe`hgg&%-7goKnmp6n9BK|2Dl*R873>VYzbL`2#jAimdKI;Vst04~^DCIA2c literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/egg_summary_bg.png b/public/images/ui/legacy/egg_summary_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..27e367212aaae20dc43f3f03c9fc122b9d99e8fe GIT binary patch literal 2160 zcmeHJX;e~a7``Bh6l&PyQaYxh)>L$7YKn0IQ%r3!+cZrAZ9A0)R-zP0SW{yV3(EE!?`+RAd-t_+{oGP=b#ZlJd*uMGw+m3z2H7H?3iM=#aKx4ySq z<|boL+7i{0bE`dp;m?=%D3ygno=<7z?WDHk93mpBcte&^oL;IRtmY+-Nn7>A?tY^y z29uPO)H+tduDQw7_chQ=2NjhxlTmK!FqE(<(|fgyy?e{{>BVw3sjjgxL;ih^?$r+B zXkT+a-rrwCd z=$u|JG^r4)qpzs|m6oYqvGW?s<_?Q;`*9-QTV6@TGstZf;v;MB(xFeu6%`B+}SUtLEo5jW%>rL}GO4fTpIi$6zotcmxdJG%%bn7(GW(nW%@=geC*w zZrqWPku4mKM7K?febv0ZTrKAlE5+Fq9|9q}O-N}lnbKl6o5o0--}QpS^s3l99FuOK zf$h#Ea5?V@0Z(333y4!`)dog8@ide*&euH}o)eUr+??*na3BG#VT9-q1j#tCr-jD4ZU!Ia?RJ0%!E#l1e$U=nWu2WK!2ywI;OOL;y zy)7yqaDND!&E=lM($nX2FW^w;H>z1Bnpgjpnis<2>_5YW@;uRx4)rA0y>^x1bKjJE zg6O56>O-6R?QRG-pyF&unvK8)>_KY#FJpEov-*6`i0U0G^;9k9O?L2W3D00q`HTxV+Vy>*S3> zCx0M!O z&Ws4Zo=kYmcuP*5?s#?{>ii*Sx?ZnTLIIdA(7Ec-_#C%-4<5LcT_2ChtiMRKg7L}- zTn~5q%wQ7fdgR;_@DLI_hzxNwI0!=iEpZY~tSZ$y7aq1fYbTrlMLY7b)ElHSVeynZ zjm5)Y7J=I|SDGc?RdGrhaq<(G(HJVV3`>VY>{XwUQY2}KOn<|qiIES4Tjccqd>@tS zvFJhXhL8Q%hr@+!yJ^*vykwQK?`^lH8bv_Ws^XxM>QJBEs zS2~n4#+ZL0s`yL@#Qu0~R%)ta@xyDKSyAJ*=j$F{75kci>{nV`u7gTJeW=Z6!}I5L zGcPRMHQsj!DV9uTdMpB0MMXu4T_P>TF7^*}Z=Vhfb=PMOAeAfvjqPs;Y)i&qy2R-X z>S|n{_T32;U~6MzZnG59=$;y(GO1dib)E&1Z33ue?;V!LP^h4 literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/icon_egg_move.png b/public/images/ui/legacy/icon_egg_move.png new file mode 100644 index 0000000000000000000000000000000000000000..6af186e9b0c21a3952b80fc00ed2a7494cee18eb GIT binary patch literal 237 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>S?=lL7@`rJ zoZ`TI?bx;dj!y39C`Mb@JW4}PIASEAB=}Dhx;GQI>6#Fao1Y;h(-l_ z{uy=uKXT8ww=+j5!Z2dtmKk5_zpt>_(6YIgamKysCrqoHCS)4C?kNeCI2XH*IjrGK zoZ;TyM#0ELe`hgg&%-7goKnmp6n9BK|2Dl*R873>VYzbL`2#jAimdKI;Vst04~^DCIA2c literal 0 HcmV?d00001 diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4cc3f69ebee..c8100e0d3b9 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2742,6 +2742,29 @@ export default class BattleScene extends SceneBase { (window as any).gameInfo = gameInfo; } + /** + * This function retrieves the sprite and audio keys for active Pokemon. + * Active Pokemon include both enemy and player Pokemon of the current wave. + * Note: Questions on garbage collection go to @frutescens + * @returns a string array of active sprite and audio keys that should not be deleted + */ + getActiveKeys(): string[] { + const keys: string[] = []; + const playerParty = this.getParty(); + playerParty.forEach(p => { + keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant)); + keys.push("pkmn__" + p.species.getSpriteId(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant, true)); + keys.push("cry/" + p.species.getCryKey(p.species.formIndex)); + }); + // enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon + const enemyParty = this.getEnemyParty(); + enemyParty.forEach(p => { + keys.push(p.species.getSpriteKey(p.gender === Gender.FEMALE, p.species.formIndex, p.shiny, p.variant)); + keys.push("cry/" + p.species.getCryKey(p.species.formIndex)); + }); + return keys; + } + /** * Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus) * @param pokemon The (enemy) pokemon diff --git a/src/data/egg-hatch-data.ts b/src/data/egg-hatch-data.ts new file mode 100644 index 00000000000..e754a9205c4 --- /dev/null +++ b/src/data/egg-hatch-data.ts @@ -0,0 +1,98 @@ +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { DexEntry, StarterDataEntry } from "#app/system/game-data"; + +/** + * Stores data associated with a specific egg and the hatched pokemon + * Allows hatch info to be stored at hatch then retrieved for display during egg summary + */ +export class EggHatchData { + /** the pokemon that hatched from the file (including shiny, IVs, ability) */ + public pokemon: PlayerPokemon; + /** index of the egg move from the hatched pokemon (not stored in PlayerPokemon) */ + public eggMoveIndex: number; + /** boolean indicating if the egg move for the hatch is new */ + public eggMoveUnlocked: boolean; + /** stored copy of the hatched pokemon's dex entry before it was updated due to hatch */ + public dexEntryBeforeUpdate: DexEntry; + /** stored copy of the hatched pokemon's starter entry before it was updated due to hatch */ + public starterDataEntryBeforeUpdate: StarterDataEntry; + /** reference to the battle scene to get gamedata and update dex */ + private scene: BattleScene; + + constructor(scene: BattleScene, pokemon: PlayerPokemon, eggMoveIndex: number) { + this.scene = scene; + this.pokemon = pokemon; + this.eggMoveIndex = eggMoveIndex; + } + + /** + * Sets the boolean for if the egg move for the hatch is a new unlock + * @param unlocked True if the EM is new + */ + setEggMoveUnlocked(unlocked: boolean) { + this.eggMoveUnlocked = unlocked; + } + + /** + * Stores a copy of the current DexEntry of the pokemon and StarterDataEntry of its starter + * Used before updating the dex, so comparing the pokemon to these entries will show the new attributes + */ + setDex() { + const currDexEntry = this.scene.gameData.dexData[this.pokemon.species.speciesId]; + const currStarterDataEntry = this.scene.gameData.starterData[this.pokemon.species.getRootSpeciesId()]; + this.dexEntryBeforeUpdate = { + seenAttr: currDexEntry.seenAttr, + caughtAttr: currDexEntry.caughtAttr, + natureAttr: currDexEntry.natureAttr, + seenCount: currDexEntry.seenCount, + caughtCount: currDexEntry.caughtCount, + hatchedCount: currDexEntry.hatchedCount, + ivs: [...currDexEntry.ivs] + }; + this.starterDataEntryBeforeUpdate = { + moveset: currStarterDataEntry.moveset, + eggMoves: currStarterDataEntry.eggMoves, + candyCount: currStarterDataEntry.candyCount, + friendship: currStarterDataEntry.friendship, + abilityAttr: currStarterDataEntry.abilityAttr, + passiveAttr: currStarterDataEntry.passiveAttr, + valueReduction: currStarterDataEntry.valueReduction, + classicWinCount: currStarterDataEntry.classicWinCount + }; + } + + /** + * Gets the dex entry before update + * @returns Dex Entry corresponding to this pokemon before the pokemon was added / updated to dex + */ + getDex(): DexEntry { + return this.dexEntryBeforeUpdate; + } + + /** + * Gets the starter dex entry before update + * @returns Starter Dex Entry corresponding to this pokemon before the pokemon was added / updated to dex + */ + getStarterEntry(): StarterDataEntry { + return this.starterDataEntryBeforeUpdate; + } + + /** + * Update the pokedex data corresponding with the new hatch's pokemon data + * Also sets whether the egg move is a new unlock or not + * @param showMessage boolean to show messages for the new catches and egg moves (false by default) + * @returns + */ + updatePokemon(showMessage : boolean = false) { + return new Promise(resolve => { + this.scene.gameData.setPokemonCaught(this.pokemon, true, true, showMessage).then(() => { + this.scene.gameData.updateSpeciesDexIvs(this.pokemon.species.speciesId, this.pokemon.ivs); + this.scene.gameData.setEggMoveUnlocked(this.pokemon.species, this.eggMoveIndex, showMessage).then((value) => { + this.setEggMoveUnlocked(value); + resolve(); + }); + }); + }); + } +} diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 432dbcd7469..f6bc41f744d 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -78,6 +78,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("overlay_hp_boss", "ui"); this.loadImage("overlay_exp", "ui"); this.loadImage("icon_owned", "ui"); + this.loadImage("icon_egg_move", "ui"); this.loadImage("ability_bar_left", "ui"); this.loadImage("bgm_bar", "ui"); this.loadImage("party_exp_bar", "ui"); @@ -272,6 +273,7 @@ export class LoadingScene extends SceneBase { this.loadImage("gacha_knob", "egg"); this.loadImage("egg_list_bg", "ui"); + this.loadImage("egg_summary_bg", "ui"); this.loadImage("end_m", "cg"); this.loadImage("end_f", "cg"); diff --git a/src/locales/en/battle.json b/src/locales/en/battle.json index 662678e7673..918fb38b520 100644 --- a/src/locales/en/battle.json +++ b/src/locales/en/battle.json @@ -61,6 +61,7 @@ "skipItemQuestion": "Are you sure you want to skip taking an item?", "itemStackFull": "The stack for {{fullItemName}} is full.\nYou will receive {{itemName}} instead.", "eggHatching": "Oh?", + "eggSkipPrompt": "Skip to egg summary?", "ivScannerUseQuestion": "Use IV Scanner on {{pokemonName}}?", "wildPokemonWithAffix": "Wild {{pokemonName}}", "foePokemonWithAffix": "Foe {{pokemonName}}", diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index a5b0252d4de..4b03aa62f02 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -1,23 +1,29 @@ -import BattleScene, { AnySound } from "#app/battle-scene.js"; -import { Egg, EGG_SEED } from "#app/data/egg.js"; -import { EggCountChangedEvent } from "#app/events/egg.js"; -import { PlayerPokemon } from "#app/field/pokemon.js"; -import { getPokemonNameWithAffix } from "#app/messages.js"; -import { Phase } from "#app/phase.js"; -import { achvs } from "#app/system/achv.js"; -import EggCounterContainer from "#app/ui/egg-counter-container.js"; -import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler.js"; -import PokemonInfoContainer from "#app/ui/pokemon-info-container.js"; -import { Mode } from "#app/ui/ui.js"; +import BattleScene, { AnySound } from "#app/battle-scene"; +import { Egg } from "#app/data/egg"; +import { EggCountChangedEvent } from "#app/events/egg"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { Phase } from "#app/phase"; +import { achvs } from "#app/system/achv"; +import EggCounterContainer from "#app/ui/egg-counter-container"; +import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler"; +import PokemonInfoContainer from "#app/ui/pokemon-info-container"; +import { Mode } from "#app/ui/ui"; import i18next from "i18next"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -import * as Utils from "#app/utils.js"; +import * as Utils from "#app/utils"; +import { EggLapsePhase } from "./egg-lapse-phase"; +import { EggHatchData } from "#app/data/egg-hatch-data"; + + /** * Class that represents egg hatching */ export class EggHatchPhase extends Phase { /** The egg that is hatching */ private egg: Egg; + /** The new EggHatchData for the egg/pokemon that hatches */ + private eggHatchData: EggHatchData; /** The number of eggs that are hatching */ private eggsToHatchCount: integer; @@ -58,10 +64,11 @@ export class EggHatchPhase extends Phase { private skipped: boolean; /** The sound effect being played when the egg is hatched */ private evolutionBgm: AnySound; + private eggLapsePhase: EggLapsePhase; - constructor(scene: BattleScene, egg: Egg, eggsToHatchCount: integer) { + constructor(scene: BattleScene, hatchScene: EggLapsePhase, egg: Egg, eggsToHatchCount: integer) { super(scene); - + this.eggLapsePhase = hatchScene; this.egg = egg; this.eggsToHatchCount = eggsToHatchCount; } @@ -307,6 +314,7 @@ export class EggHatchPhase extends Phase { * Function to do the logic and animation of completing a hatch and revealing the Pokemon */ doReveal(): void { + // set the previous dex data so info container can show new unlocks in egg summary const isShiny = this.pokemon.isShiny(); if (this.pokemon.species.subLegendary) { this.scene.validateAchv(achvs.HATCH_SUB_LEGENDARY); @@ -345,13 +353,13 @@ export class EggHatchPhase extends Phase { this.scene.ui.showText(i18next.t("egg:hatchFromTheEgg", { pokemonName: getPokemonNameWithAffix(this.pokemon) }), null, () => { this.scene.gameData.updateSpeciesDexIvs(this.pokemon.species.speciesId, this.pokemon.ivs); this.scene.gameData.setPokemonCaught(this.pokemon, true, true).then(() => { - this.scene.gameData.setEggMoveUnlocked(this.pokemon.species, this.eggMoveIndex).then(() => { + this.scene.gameData.setEggMoveUnlocked(this.pokemon.species, this.eggMoveIndex).then((value) => { + this.eggHatchData.setEggMoveUnlocked(value); this.scene.ui.showText("", 0); this.end(); }); }); }, null, true, 3000); - //this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm()); }); }); this.scene.tweens.add({ @@ -435,17 +443,11 @@ export class EggHatchPhase extends Phase { /** * Generates a Pokemon to be hatched by the egg + * Also stores the generated pokemon in this.eggHatchData * @returns the hatched PlayerPokemon */ generatePokemon(): PlayerPokemon { - let ret: PlayerPokemon; - - this.scene.executeWithSeedOffset(() => { - ret = this.egg.generatePlayerPokemon(this.scene); - this.eggMoveIndex = this.egg.eggMoveIndex; - - }, this.egg.id, EGG_SEED.toString()); - - return ret!; + this.eggHatchData = this.eggLapsePhase.generatePokemon(this.egg); + return this.eggHatchData.pokemon; } } diff --git a/src/phases/egg-lapse-phase.ts b/src/phases/egg-lapse-phase.ts index 50d7106f229..1adb1568166 100644 --- a/src/phases/egg-lapse-phase.ts +++ b/src/phases/egg-lapse-phase.ts @@ -1,11 +1,23 @@ -import BattleScene from "#app/battle-scene.js"; -import { Egg } from "#app/data/egg.js"; -import { Phase } from "#app/phase.js"; +import BattleScene from "#app/battle-scene"; +import { Egg, EGG_SEED } from "#app/data/egg"; +import { Phase } from "#app/phase"; import i18next from "i18next"; import Overrides from "#app/overrides"; import { EggHatchPhase } from "./egg-hatch-phase"; +import { Mode } from "#app/ui/ui"; +import { achvs } from "#app/system/achv"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { EggSummaryPhase } from "./egg-summary-phase"; +import { EggHatchData } from "#app/data/egg-hatch-data"; +/** + * Phase that handles updating eggs, and hatching any ready eggs + * Also handles prompts for skipping animation, and calling the egg summary phase + */ export class EggLapsePhase extends Phase { + + private eggHatchData: EggHatchData[] = []; + private readonly minEggsToPromptSkip: number = 5; constructor(scene: BattleScene) { super(scene); } @@ -16,20 +28,111 @@ export class EggLapsePhase extends Phase { const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => { return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1; }); + const eggsToHatchCount: number = eggsToHatch.length; + this.eggHatchData= []; - let eggCount: integer = eggsToHatch.length; + if (eggsToHatchCount > 0) { - if (eggCount) { - this.scene.queueMessage(i18next.t("battle:eggHatching")); - - for (const egg of eggsToHatch) { - this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount)); - if (eggCount > 0) { - eggCount--; - } + if (eggsToHatchCount >= this.minEggsToPromptSkip) { + this.scene.ui.showText(i18next.t("battle:eggHatching"), 0, () => { + // show prompt for skip + this.scene.ui.showText(i18next.t("battle:eggSkipPrompt"), 0); + this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => { + this.hatchEggsSkipped(eggsToHatch); + this.showSummary(); + }, () => { + this.hatchEggsRegular(eggsToHatch); + this.showSummary(); + } + ); + }, 100, true); + } else { + // regular hatches, no summary + this.scene.queueMessage(i18next.t("battle:eggHatching")); + this.hatchEggsRegular(eggsToHatch); + this.end(); } - + } else { + this.end(); } + } + + /** + * Hatches eggs normally one by one, showing animations + * @param eggsToHatch list of eggs to hatch + */ + hatchEggsRegular(eggsToHatch: Egg[]) { + let eggsToHatchCount: number = eggsToHatch.length; + for (const egg of eggsToHatch) { + this.scene.unshiftPhase(new EggHatchPhase(this.scene, this, egg, eggsToHatchCount)); + eggsToHatchCount--; + } + } + + /** + * Hatches eggs with no animations + * @param eggsToHatch list of eggs to hatch + */ + hatchEggsSkipped(eggsToHatch: Egg[]) { + for (const egg of eggsToHatch) { + this.hatchEggSilently(egg); + } + } + + showSummary() { + this.scene.unshiftPhase(new EggSummaryPhase(this.scene, this.eggHatchData)); this.end(); } + + /** + * Hatches an egg and stores it in the local EggHatchData array without animations + * Also validates the achievements for the hatched pokemon and removes the egg + * @param egg egg to hatch + */ + hatchEggSilently(egg: Egg) { + const eggIndex = this.scene.gameData.eggs.findIndex(e => e.id === egg.id); + if (eggIndex === -1) { + return this.end(); + } + this.scene.gameData.eggs.splice(eggIndex, 1); + + const data = this.generatePokemon(egg); + const pokemon = data.pokemon; + if (pokemon.fusionSpecies) { + pokemon.clearFusionSpecies(); + } + + if (pokemon.species.subLegendary) { + this.scene.validateAchv(achvs.HATCH_SUB_LEGENDARY); + } + if (pokemon.species.legendary) { + this.scene.validateAchv(achvs.HATCH_LEGENDARY); + } + if (pokemon.species.mythical) { + this.scene.validateAchv(achvs.HATCH_MYTHICAL); + } + if (pokemon.isShiny()) { + this.scene.validateAchv(achvs.HATCH_SHINY); + } + + } + + /** + * Generates a Pokemon and creates a new EggHatchData instance for the given egg + * @param egg the egg to hatch + * @returns the hatched PlayerPokemon + */ + generatePokemon(egg: Egg): EggHatchData { + let ret: PlayerPokemon; + let newHatchData: EggHatchData; + this.scene.executeWithSeedOffset(() => { + ret = egg.generatePlayerPokemon(this.scene); + newHatchData = new EggHatchData(this.scene, ret, egg.eggMoveIndex); + newHatchData.setDex(); + this.eggHatchData.push(newHatchData); + + }, egg.id, EGG_SEED.toString()); + return newHatchData!; + } + } diff --git a/src/phases/egg-summary-phase.ts b/src/phases/egg-summary-phase.ts new file mode 100644 index 00000000000..190af17c724 --- /dev/null +++ b/src/phases/egg-summary-phase.ts @@ -0,0 +1,50 @@ +import BattleScene from "#app/battle-scene"; +import { Phase } from "#app/phase"; +import { Mode } from "#app/ui/ui"; +import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler"; +import { EggHatchData } from "#app/data/egg-hatch-data"; + +/** + * Class that represents the egg summary phase + * It does some of the function for updating egg data + * Phase is handled mostly by the egg-hatch-scene-handler UI + */ +export class EggSummaryPhase extends Phase { + private eggHatchData: EggHatchData[]; + private eggHatchHandler: EggHatchSceneHandler; + + constructor(scene: BattleScene, eggHatchData: EggHatchData[]) { + super(scene); + this.eggHatchData = eggHatchData; + } + + start() { + super.start(); + + // updates next pokemon once the current update has been completed + const updateNextPokemon = (i: number) => { + if (i >= this.eggHatchData.length) { + this.scene.ui.setModeForceTransition(Mode.EGG_HATCH_SUMMARY, this.eggHatchData).then(() => { + this.scene.fadeOutBgm(undefined, false); + this.eggHatchHandler = this.scene.ui.getHandler() as EggHatchSceneHandler; + }); + + } else { + this.eggHatchData[i].setDex(); + this.eggHatchData[i].updatePokemon().then(() => { + if (i < this.eggHatchData.length) { + updateNextPokemon(i + 1); + } + }); + } + }; + updateNextPokemon(0); + + } + + end() { + this.eggHatchHandler.clear(); + this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => {}); + super.end(); + } +} diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 50cc6177a84..1a47294906e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1553,11 +1553,11 @@ export class GameData { } } - setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false): Promise { - return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg); + setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { + return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg, showMessage); } - setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false): Promise { + setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showMessage: boolean = true): Promise { return new Promise(resolve => { const dexEntry = this.dexData[species.speciesId]; const caughtAttr = dexEntry.caughtAttr; @@ -1616,13 +1616,17 @@ export class GameData { const checkPrevolution = () => { if (hasPrevolution) { const prevolutionSpecies = pokemonPrevolutions[species.speciesId]; - return this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg).then(() => resolve()); + this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showMessage).then(() => resolve()); } else { resolve(); } }; if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) { + if (!showMessage) { + resolve(); + return; + } this.scene.playSound("level_up_fanfare"); this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true); } else { @@ -1668,7 +1672,7 @@ export class GameData { this.starterData[species.speciesId].candyCount += count; } - setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer): Promise { + setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, showMessage: boolean = true): Promise { return new Promise(resolve => { const speciesId = species.speciesId; if (!speciesEggMoves.hasOwnProperty(speciesId) || !speciesEggMoves[speciesId][eggMoveIndex]) { @@ -1688,11 +1692,15 @@ export class GameData { } this.starterData[speciesId].eggMoves |= value; - + if (!showMessage) { + resolve(true); + return; + } this.scene.playSound("level_up_fanfare"); - const moveName = allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name; - this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, () => resolve(true), null, true); + this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, (() => { + resolve(true); + }), null, true); }); } diff --git a/src/ui/egg-summary-ui-handler.ts b/src/ui/egg-summary-ui-handler.ts new file mode 100644 index 00000000000..af82ab33438 --- /dev/null +++ b/src/ui/egg-summary-ui-handler.ts @@ -0,0 +1,320 @@ +import BattleScene from "../battle-scene"; +import { Mode } from "./ui"; +import PokemonIconAnimHandler, { PokemonIconAnimMode } from "./pokemon-icon-anim-handler"; +import MessageUiHandler from "./message-ui-handler"; +import { getEggTierForSpecies } from "../data/egg"; +import {Button} from "#enums/buttons"; +import { Gender } from "#app/data/gender"; +import { getVariantTint } from "#app/data/variant"; +import { EggTier } from "#app/enums/egg-type"; +import PokemonHatchInfoContainer from "./pokemon-hatch-info-container"; +import { EggSummaryPhase } from "#app/phases/egg-summary-phase"; +import { DexAttr } from "#app/system/game-data"; +import { EggHatchData } from "#app/data/egg-hatch-data"; + +const iconContainerX = 115; +const iconContainerY = 9; +const numCols = 11; +const iconSize = 18; + +/** + * UI Handler for the egg summary. + * Handles navigation and display of each pokemon as a list + * Also handles display of the pokemon-hatch-info-container + */ +export default class EggSummaryUiHandler extends MessageUiHandler { + /** holds all elements in the scene */ + private eggHatchContainer: Phaser.GameObjects.Container; + /** holds the icon containers and info container */ + private summaryContainer: Phaser.GameObjects.Container; + /** container for the mini pokemon sprites */ + private pokemonIconSpritesContainer: Phaser.GameObjects.Container; + /** container for the icons displayed alongside the mini icons (e.g. shiny, HA capsule) */ + private pokemonIconsContainer: Phaser.GameObjects.Container; + /** hatch info container that displays the current pokemon / hatch (main element on left hand side) */ + private infoContainer: PokemonHatchInfoContainer; + /** handles jumping animations for the pokemon sprite icons */ + private iconAnimHandler: PokemonIconAnimHandler; + private eggHatchBg: Phaser.GameObjects.Image; + private cursorObj: Phaser.GameObjects.Image; + private eggHatchData: EggHatchData[]; + + + /** + * Allows subscribers to listen for events + * + * Current Events: + * - {@linkcode EggEventType.EGG_COUNT_CHANGED} {@linkcode EggCountChangedEvent} + */ + public readonly eventTarget: EventTarget = new EventTarget(); + + constructor(scene: BattleScene) { + super(scene, Mode.EGG_HATCH_SUMMARY); + } + + + setup() { + const ui = this.getUi(); + + this.summaryContainer = this.scene.add.container(0, -this.scene.game.canvas.height / 6); + this.summaryContainer.setVisible(false); + ui.add(this.summaryContainer); + + this.eggHatchContainer = this.scene.add.container(0, -this.scene.game.canvas.height / 6); + this.eggHatchContainer.setVisible(false); + ui.add(this.eggHatchContainer); + + this.iconAnimHandler = new PokemonIconAnimHandler(); + this.iconAnimHandler.setup(this.scene); + + this.eggHatchBg = this.scene.add.image(0, 0, "egg_summary_bg"); + this.eggHatchBg.setOrigin(0, 0); + this.eggHatchContainer.add(this.eggHatchBg); + + this.pokemonIconsContainer = this.scene.add.container(iconContainerX, iconContainerY); + this.pokemonIconSpritesContainer = this.scene.add.container(iconContainerX, iconContainerY); + this.summaryContainer.add(this.pokemonIconsContainer); + this.summaryContainer.add(this.pokemonIconSpritesContainer); + + this.cursorObj = this.scene.add.image(0, 0, "select_cursor"); + this.cursorObj.setOrigin(0, 0); + this.summaryContainer.add(this.cursorObj); + + this.infoContainer = new PokemonHatchInfoContainer(this.scene, this.summaryContainer); + this.infoContainer.setup(); + this.infoContainer.changeToEggSummaryLayout(); + this.infoContainer.setVisible(true); + this.summaryContainer.add(this.infoContainer); + + this.cursor = -1; + } + + clear() { + super.clear(); + this.cursor = -1; + this.summaryContainer.setVisible(false); + this.pokemonIconSpritesContainer.removeAll(true); + this.pokemonIconsContainer.removeAll(true); + this.eggHatchBg.setVisible(false); + this.getUi().hideTooltip(); + // Note: Questions on garbage collection go to @frutescens + const activeKeys = this.scene.getActiveKeys(); + // Removing unnecessary sprites from animation manager + const animKeys = Object.keys(this.scene.anims["anims"]["entries"]); + animKeys.forEach(key => { + if (key.startsWith("pkmn__") && !activeKeys.includes(key)) { + this.scene.anims.remove(key); + } + }); + // Removing unnecessary cries from audio cache + const audioKeys = Object.keys(this.scene.cache.audio.entries.entries); + audioKeys.forEach(key => { + if (key.startsWith("cry/") && !activeKeys.includes(key)) { + delete this.scene.cache.audio.entries.entries[key]; + } + }); + // Clears eggHatchData in EggSummaryUiHandler + this.eggHatchData.length = 0; + // Removes Pokemon icons in EggSummaryUiHandler + this.iconAnimHandler.removeAll(); + console.log("Egg Summary Handler cleared"); + } + + /** + * @param args EggHatchData[][] + * args[0]: list of EggHatchData for each egg/pokemon hatched + */ + show(args: EggHatchData[][]): boolean { + super.show(args); + if (args.length >= 1) { + // sort the egg hatch data by egg tier then by species number (then by order hatched) + this.eggHatchData = args[0].sort(function sortHatchData(a: EggHatchData, b: EggHatchData) { + const speciesA = a.pokemon.species; + const speciesB = b.pokemon.species; + if (getEggTierForSpecies(speciesA) < getEggTierForSpecies(speciesB)) { + return -1; + } else if (getEggTierForSpecies(speciesA) > getEggTierForSpecies(speciesB)) { + return 1; + } else { + if (speciesA.speciesId < speciesB.speciesId) { + return -1; + } else if (speciesA.speciesId > speciesB.speciesId) { + return 1; + } else { + return 0; + } + } + } + + ); + } + + this.getUi().bringToTop(this.summaryContainer); + this.summaryContainer.setVisible(true); + this.eggHatchContainer.setVisible(true); + this.pokemonIconsContainer.setVisible(true); + this.eggHatchBg.setVisible(true); + this.infoContainer.hideDisplayPokemon(); + + this.eggHatchData.forEach( (value: EggHatchData, i: number) => { + const x = (i % numCols) * iconSize; + const y = Math.floor(i / numCols) * iconSize; + + const displayPokemon = value.pokemon; + const offset = 2; + const rightSideX = 12; + + const bg = this.scene.add.image(x+2, y+5, "passive_bg"); + bg.setOrigin(0, 0); + bg.setScale(0.75); + bg.setVisible(true); + this.pokemonIconsContainer.add(bg); + + // set tint for passive bg + switch (getEggTierForSpecies(displayPokemon.species)) { + case EggTier.COMMON: + bg.setVisible(false); + break; + case EggTier.GREAT: + bg.setTint(0xabafff); + break; + case EggTier.ULTRA: + bg.setTint(0xffffaa); + break; + case EggTier.MASTER: + bg.setTint(0xdfffaf); + break; + } + const species = displayPokemon.species; + const female = displayPokemon.gender === Gender.FEMALE; + const formIndex = displayPokemon.formIndex; + const variant = displayPokemon.variant; + const isShiny = displayPokemon.shiny; + + // set pokemon icon (and replace with base sprite if there is a mismatch) + const icon = this.scene.add.sprite(x - offset, y + offset, species.getIconAtlasKey(formIndex, isShiny, variant)); + icon.setScale(0.5); + icon.setOrigin(0, 0); + icon.setFrame(species.getIconId(female, formIndex, isShiny, variant)); + + if (icon.frame.name !== species.getIconId(female, formIndex, isShiny, variant)) { + console.log(`${species.name}'s variant icon does not exist. Replacing with default.`); + icon.setTexture(species.getIconAtlasKey(formIndex, false, variant)); + icon.setFrame(species.getIconId(female, formIndex, false, variant)); + } + this.pokemonIconSpritesContainer.add(icon); + this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.NONE); + + const shiny = this.scene.add.image(x + rightSideX, y + offset * 2, "shiny_star_small"); + shiny.setScale(0.5); + shiny.setVisible(displayPokemon.shiny); + shiny.setTint(getVariantTint(displayPokemon.variant)); + this.pokemonIconsContainer.add(shiny); + + const ha = this.scene.add.image(x + rightSideX, y + 7, "ha_capsule"); + ha.setScale(0.5); + ha.setVisible((displayPokemon.hasAbility(displayPokemon.species.abilityHidden))); + this.pokemonIconsContainer.add(ha); + + const pb = this.scene.add.image(x + rightSideX, y + offset * 7, "icon_owned"); + pb.setOrigin(0, 0); + pb.setScale(0.5); + + // add animation for new unlocks (new catch or new shiny or new form) + const dexEntry = value.dexEntryBeforeUpdate; + const caughtAttr = dexEntry.caughtAttr; + const newShiny = BigInt(1 << (displayPokemon.shiny ? 1 : 0)); + const newVariant = BigInt(1 << (displayPokemon.variant + 4)); + const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0)); + const newForm = (BigInt(1 << displayPokemon.formIndex) * DexAttr.DEFAULT_FORM & caughtAttr) === BigInt(0); + + pb.setVisible(!caughtAttr || newForm); + if (!caughtAttr || newShinyOrVariant || newForm) { + this.iconAnimHandler.addOrUpdate(icon, PokemonIconAnimMode.PASSIVE); + } + this.pokemonIconsContainer.add(pb); + + const em = this.scene.add.image(x, y + offset, "icon_egg_move"); + em.setOrigin(0, 0); + em.setScale(0.5); + em.setVisible(value.eggMoveUnlocked); + this.pokemonIconsContainer.add(em); + }); + + this.setCursor(0); + this.scene.playSoundWithoutBgm("evolution_fanfare"); + return true; + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + const error = false; + if (button === Button.CANCEL) { + const phase = this.scene.getCurrentPhase(); + if (phase instanceof EggSummaryPhase) { + phase.end(); + } + ui.revertMode(); + success = true; + } else { + const count = this.eggHatchData.length; + const rows = Math.ceil(count / numCols); + const row = Math.floor(this.cursor / numCols); + switch (button) { + case Button.UP: + if (row) { + success = this.setCursor(this.cursor - numCols); + } + break; + case Button.DOWN: + if (row < rows - 2 || (row < rows - 1 && this.cursor % numCols <= (count - 1) % numCols)) { + success = this.setCursor(this.cursor + numCols); + } + break; + case Button.LEFT: + if (this.cursor % numCols) { + success = this.setCursor(this.cursor - 1); + } + break; + case Button.RIGHT: + if (this.cursor % numCols < (row < rows - 1 ? 10 : (count - 1) % numCols)) { + success = this.setCursor(this.cursor + 1); + } + break; + } + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + + return success || error; + } + + setCursor(cursor: number): boolean { + let changed = false; + + const lastCursor = this.cursor; + + changed = super.setCursor(cursor); + + if (changed) { + this.cursorObj.setPosition(iconContainerX - 1 + iconSize * (cursor % numCols), iconContainerY + 1 + iconSize * Math.floor(cursor / numCols)); + + if (lastCursor > -1) { + this.iconAnimHandler.addOrUpdate(this.pokemonIconSpritesContainer.getAt(lastCursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.NONE); + } + this.iconAnimHandler.addOrUpdate(this.pokemonIconSpritesContainer.getAt(cursor) as Phaser.GameObjects.Sprite, PokemonIconAnimMode.ACTIVE); + + this.infoContainer.showHatchInfo(this.eggHatchData[cursor]); + + } + + return changed; + } + +} diff --git a/src/ui/pokemon-hatch-info-container.ts b/src/ui/pokemon-hatch-info-container.ts new file mode 100644 index 00000000000..f8a9adced36 --- /dev/null +++ b/src/ui/pokemon-hatch-info-container.ts @@ -0,0 +1,189 @@ +import PokemonInfoContainer from "./pokemon-info-container"; +import BattleScene from "../battle-scene"; +import { Gender } from "../data/gender"; +import { Type } from "../data/type"; +import * as Utils from "../utils"; +import { TextStyle, addTextObject } from "./text"; +import { speciesEggMoves } from "#app/data/egg-moves"; +import { allMoves } from "#app/data/move"; +import { Species } from "#app/enums/species"; +import { getEggTierForSpecies } from "#app/data/egg"; +import { starterColors } from "../battle-scene"; +import { argbFromRgba } from "@material/material-color-utilities"; +import { EggHatchData } from "#app/data/egg-hatch-data"; +import { PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonSpeciesForm } from "#app/data/pokemon-species"; + +/** + * Class for the hatch info summary of each pokemon + * Holds an info container as well as an additional egg sprite, name, egg moves and main sprite + */ +export default class PokemonHatchInfoContainer extends PokemonInfoContainer { + private currentPokemonSprite: Phaser.GameObjects.Sprite; + private pokemonNumberText: Phaser.GameObjects.Text; + private pokemonNameText: Phaser.GameObjects.Text; + private pokemonEggMovesContainer: Phaser.GameObjects.Container; + private pokemonEggMoveContainers: Phaser.GameObjects.Container[]; + private pokemonEggMoveBgs: Phaser.GameObjects.NineSlice[]; + private pokemonEggMoveLabels: Phaser.GameObjects.Text[]; + private pokemonHatchedIcon : Phaser.GameObjects.Sprite; + private pokemonListContainer: Phaser.GameObjects.Container; + private pokemonCandyIcon: Phaser.GameObjects.Sprite; + private pokemonCandyOverlayIcon: Phaser.GameObjects.Sprite; + private pokemonCandyCountText: Phaser.GameObjects.Text; + + constructor(scene: BattleScene, listContainer : Phaser.GameObjects.Container, x: number = 115, y: number = 9,) { + super(scene, x, y); + this.pokemonListContainer = listContainer; + + } + setup(): void { + super.setup(); + super.changeToEggSummaryLayout(); + + this.currentPokemonSprite = this.scene.add.sprite(54, 80, "pkmn__sub"); + this.currentPokemonSprite.setScale(0.8); + this.currentPokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + this.pokemonListContainer.add(this.currentPokemonSprite); + + // setup name and number + this.pokemonNumberText = addTextObject(this.scene, 80, 107.5, "0000", TextStyle.SUMMARY, {fontSize: 74}); + this.pokemonNumberText.setOrigin(0, 0); + this.pokemonListContainer.add(this.pokemonNumberText); + + this.pokemonNameText = addTextObject(this.scene, 7, 107.5, "", TextStyle.SUMMARY, {fontSize: 74}); + this.pokemonNameText.setOrigin(0, 0); + this.pokemonListContainer.add(this.pokemonNameText); + + // setup egg icon and candy count + this.pokemonHatchedIcon = this.scene.add.sprite(-5, 90, "egg_icons"); + this.pokemonHatchedIcon.setOrigin(0, 0.2); + this.pokemonHatchedIcon.setScale(0.8); + this.pokemonListContainer.add(this.pokemonHatchedIcon); + + this.pokemonCandyIcon = this.scene.add.sprite(4.5, 40, "candy"); + this.pokemonCandyIcon.setScale(0.5); + this.pokemonCandyIcon.setOrigin(0, 0); + this.pokemonListContainer.add(this.pokemonCandyIcon); + + this.pokemonCandyOverlayIcon = this.scene.add.sprite(4.5, 40, "candy_overlay"); + this.pokemonCandyOverlayIcon.setScale(0.5); + this.pokemonCandyOverlayIcon.setOrigin(0, 0); + this.pokemonListContainer.add(this.pokemonCandyOverlayIcon); + + this.pokemonCandyCountText = addTextObject(this.scene, 14, 40, "x0", TextStyle.SUMMARY, { fontSize: "56px" }); + this.pokemonCandyCountText.setOrigin(0, 0); + this.pokemonListContainer.add(this.pokemonCandyCountText); + + // setup egg moves + this.pokemonEggMoveContainers = []; + this.pokemonEggMoveBgs = []; + this.pokemonEggMoveLabels = []; + this.pokemonEggMovesContainer = this.scene.add.container(0, 200); + this.pokemonEggMovesContainer.setVisible(false); + this.pokemonEggMovesContainer.setScale(0.5); + + for (let m = 0; m < 4; m++) { + const eggMoveContainer = this.scene.add.container(0, 0 + 6 * m); + + const eggMoveBg = this.scene.add.nineslice(70, 0, "type_bgs", "unknown", 92, 14, 2, 2, 2, 2); + eggMoveBg.setOrigin(1, 0); + + const eggMoveLabel = addTextObject(this.scene, 70 -eggMoveBg.width / 2, 0, "???", TextStyle.PARTY); + eggMoveLabel.setOrigin(0.5, 0); + + this.pokemonEggMoveBgs.push(eggMoveBg); + this.pokemonEggMoveLabels.push(eggMoveLabel); + + eggMoveContainer.add(eggMoveBg); + eggMoveContainer.add(eggMoveLabel); + eggMoveContainer.setScale(0.44); + + this.pokemonEggMoveContainers.push(eggMoveContainer); + + this.pokemonEggMovesContainer.add(eggMoveContainer); + } + + super.add(this.pokemonEggMoveContainers); + + } + + /** + * Disable the sprite (and replace with substitute) + */ + hideDisplayPokemon() { + this.currentPokemonSprite.setVisible(false); + } + + /** + * Display a given pokemon sprite with animations + * assumes the specific pokemon sprite has already been loaded + */ + displayPokemon(pokemon: PlayerPokemon) { + const species = pokemon.species; + const female = pokemon.gender === Gender.FEMALE; + const formIndex = pokemon.formIndex; + const shiny = pokemon.shiny; + const variant = pokemon.variant; + this.currentPokemonSprite.setVisible(false); + species.loadAssets(this.scene, female, formIndex, shiny, variant, true).then(() => { + + getPokemonSpeciesForm(species.speciesId, pokemon.formIndex).cry(this.scene); + this.currentPokemonSprite.play(species.getSpriteKey(female, formIndex, shiny, variant)); + this.currentPokemonSprite.setPipelineData("shiny", shiny); + this.currentPokemonSprite.setPipelineData("variant", variant); + this.currentPokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female, formIndex, shiny, variant)); + this.currentPokemonSprite.setVisible(true); + }); + } + + /** + * Updates the info container with the appropriate dex data and starter entry from the hatchInfo + * Also updates the displayed name, number, egg moves and main animated sprite for the pokemon + * @param hatchInfo The EggHatchData of the pokemon / new hatch to show + */ + showHatchInfo(hatchInfo: EggHatchData) { + this.pokemonEggMovesContainer.setVisible(true); + + const pokemon = hatchInfo.pokemon; + const species = pokemon.species; + this.displayPokemon(pokemon); + + super.show(pokemon, false, 1, hatchInfo.getDex(), hatchInfo.getStarterEntry(), true); + const colorScheme = starterColors[species.speciesId]; + + this.pokemonCandyIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[0]))); + this.pokemonCandyIcon.setVisible(true); + this.pokemonCandyOverlayIcon.setTint(argbFromRgba(Utils.rgbHexToRgba(colorScheme[1]))); + this.pokemonCandyOverlayIcon.setVisible(true); + this.pokemonCandyCountText.setText(`x${this.scene.gameData.starterData[species.speciesId].candyCount}`); + this.pokemonCandyCountText.setVisible(true); + + this.pokemonNumberText.setText(Utils.padInt(species.speciesId, 4)); + this.pokemonNameText.setText(species.name); + + const hasEggMoves = species && speciesEggMoves.hasOwnProperty(species.speciesId); + + for (let em = 0; em < 4; em++) { + const eggMove = hasEggMoves ? allMoves[speciesEggMoves[species.speciesId][em]] : null; + const eggMoveUnlocked = eggMove && this.scene.gameData.starterData[species.speciesId].eggMoves & Math.pow(2, em); + this.pokemonEggMoveBgs[em].setFrame(Type[eggMove ? eggMove.type : Type.UNKNOWN].toString().toLowerCase()); + + this.pokemonEggMoveLabels[em].setText(eggMove && eggMoveUnlocked ? eggMove.name : "???"); + if (!(eggMove && hatchInfo.starterDataEntryBeforeUpdate.eggMoves & Math.pow(2, em)) && eggMoveUnlocked) { + this.pokemonEggMoveLabels[em].setText("(+) " + eggMove.name); + } + } + + // will always have at least one egg move + this.pokemonEggMovesContainer.setVisible(true); + + if (species.speciesId === Species.MANAPHY || species.speciesId === Species.PHIONE) { + this.pokemonHatchedIcon.setFrame("manaphy"); + } else { + this.pokemonHatchedIcon.setFrame(getEggTierForSpecies(species)); + } + + } + +} diff --git a/src/ui/pokemon-info-container.ts b/src/ui/pokemon-info-container.ts index edb85ecff7a..49bfd4d7293 100644 --- a/src/ui/pokemon-info-container.ts +++ b/src/ui/pokemon-info-container.ts @@ -6,7 +6,7 @@ import { getNatureName } from "../data/nature"; import { Type } from "../data/type"; import Pokemon from "../field/pokemon"; import i18next from "i18next"; -import { DexAttr } from "../system/game-data"; +import { DexAttr, DexEntry, StarterDataEntry } from "../system/game-data"; import * as Utils from "../utils"; import ConfirmUiHandler from "./confirm-ui-handler"; import { StatsContainer } from "./stats-container"; @@ -24,7 +24,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { infoContainerTextSize: "64px" }, "de": { - infoContainerTextSize: "64px" + infoContainerTextSize: "64px", }, "es": { infoContainerTextSize: "64px" @@ -63,6 +63,7 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { private pokemonMovesContainers: Phaser.GameObjects.Container[]; private pokemonMoveBgs: Phaser.GameObjects.NineSlice[]; private pokemonMoveLabels: Phaser.GameObjects.Text[]; + private infoBg; private numCharsBeforeCutoff = 16; @@ -83,9 +84,9 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { const currentLanguage = i18next.resolvedLanguage!; // TODO: is this bang correct? const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage?.includes(lang))!; // TODO: is this bang correct? const textSettings = languageSettings[langSettingKey]; - const infoBg = addWindow(this.scene, 0, 0, this.infoWindowWidth, 132); - infoBg.setOrigin(0.5, 0.5); - infoBg.setName("window-info-bg"); + this.infoBg = addWindow(this.scene, 0, 0, this.infoWindowWidth, 132); + this.infoBg.setOrigin(0.5, 0.5); + this.infoBg.setName("window-info-bg"); this.pokemonMovesContainer = this.scene.add.container(6, 14); this.pokemonMovesContainer.setName("pkmn-moves"); @@ -133,7 +134,7 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.statsContainer = new StatsContainer(this.scene, -48, -64, true); - this.add(infoBg); + this.add(this.infoBg); this.add(this.statsContainer); // The position should be set per language @@ -207,9 +208,16 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.setVisible(false); } - show(pokemon: Pokemon, showMoves: boolean = false, speedMultiplier: number = 1): Promise { + show(pokemon: Pokemon, showMoves: boolean = false, speedMultiplier: number = 1, dexEntry?: DexEntry, starterEntry?: StarterDataEntry, eggInfo = false): Promise { return new Promise(resolve => { - const caughtAttr = BigInt(pokemon.scene.gameData.dexData[pokemon.species.speciesId].caughtAttr); + if (!dexEntry) { + dexEntry = pokemon.scene.gameData.dexData[pokemon.species.speciesId]; + } + if (!starterEntry) { + starterEntry = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()]; + } + + const caughtAttr = BigInt(dexEntry.caughtAttr); if (pokemon.gender > Gender.GENDERLESS) { this.pokemonGenderText.setText(getGenderSymbol(pokemon.gender)); this.pokemonGenderText.setColor(getGenderColor(pokemon.gender)); @@ -268,7 +276,7 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { const opponentPokemonAbilityIndex = (opponentPokemonOneNormalAbility && pokemon.abilityIndex === 1) ? 2 : pokemon.abilityIndex; const opponentPokemonAbilityAttr = 1 << opponentPokemonAbilityIndex; - const rootFormHasHiddenAbility = pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].abilityAttr & opponentPokemonAbilityAttr; + const rootFormHasHiddenAbility = starterEntry.abilityAttr & opponentPokemonAbilityAttr; if (!rootFormHasHiddenAbility) { this.pokemonAbilityLabelText.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme)); @@ -280,7 +288,7 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { this.pokemonNatureText.setText(getNatureName(pokemon.getNature(), true, false, false, this.scene.uiTheme)); - const dexNatures = pokemon.scene.gameData.dexData[pokemon.species.speciesId].natureAttr; + const dexNatures = dexEntry.natureAttr; const newNature = 1 << (pokemon.nature + 1); if (!(dexNatures & newNature)) { @@ -324,31 +332,31 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { } const starterSpeciesId = pokemon.species.getRootSpeciesId(); - const originalIvs: integer[] | null = this.scene.gameData.dexData[starterSpeciesId].caughtAttr - ? this.scene.gameData.dexData[starterSpeciesId].ivs - : null; + const originalIvs: integer[] | null = eggInfo ? (dexEntry.caughtAttr ? dexEntry.ivs : null) : (this.scene.gameData.dexData[starterSpeciesId].caughtAttr + ? this.scene.gameData.dexData[starterSpeciesId].ivs : null); this.statsContainer.updateIvs(pokemon.ivs, originalIvs!); // TODO: is this bang correct? - - this.scene.tweens.add({ - targets: this, - duration: Utils.fixedInt(Math.floor(750 / speedMultiplier)), - ease: "Cubic.easeInOut", - x: this.initialX - this.infoWindowWidth, - onComplete: () => { - resolve(); - } - }); - - if (showMoves) { + if (!eggInfo) { this.scene.tweens.add({ - delay: Utils.fixedInt(Math.floor(325 / speedMultiplier)), - targets: this.pokemonMovesContainer, - duration: Utils.fixedInt(Math.floor(325 / speedMultiplier)), + targets: this, + duration: Utils.fixedInt(Math.floor(750 / speedMultiplier)), ease: "Cubic.easeInOut", - x: this.movesContainerInitialX - 57, - onComplete: () => resolve() + x: this.initialX - this.infoWindowWidth, + onComplete: () => { + resolve(); + } }); + + if (showMoves) { + this.scene.tweens.add({ + delay: Utils.fixedInt(Math.floor(325 / speedMultiplier)), + targets: this.pokemonMovesContainer, + duration: Utils.fixedInt(Math.floor(325 / speedMultiplier)), + ease: "Cubic.easeInOut", + x: this.movesContainerInitialX - 57, + onComplete: () => resolve() + }); + } } for (let m = 0; m < 4; m++) { @@ -364,6 +372,36 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container { }); } + changeToEggSummaryLayout() { + // The position should be set per language (and shifted for new layout) + const currentLanguage = i18next.resolvedLanguage!; // TODO: is this bang correct? + const langSettingKey = Object.keys(languageSettings).find(lang => currentLanguage?.includes(lang))!; // TODO: is this bang correct? + const textSettings = languageSettings[langSettingKey]; + + const eggLabelTextOffset = 43; + const infoContainerLabelXPos = (textSettings?.infoContainerLabelXPos || -18) + eggLabelTextOffset; + const infoContainerTextXPos = (textSettings?.infoContainerTextXPos || -14) + eggLabelTextOffset; + + this.x = this.initialX - this.infoWindowWidth; + + this.pokemonGenderText.setPosition(89, -2); + this.pokemonGenderNewText.setPosition(79, -2); + this.pokemonShinyIcon.setPosition(82, 87); + this.pokemonShinyNewIcon.setPosition(72, 87); + + this.pokemonFormLabelText.setPosition(infoContainerLabelXPos, 152); + this.pokemonFormText.setPosition(infoContainerTextXPos, 152); + this.pokemonAbilityLabelText.setPosition(infoContainerLabelXPos, 110); + this.pokemonAbilityText.setPosition(infoContainerTextXPos, 110); + this.pokemonNatureLabelText.setPosition(infoContainerLabelXPos, 125); + this.pokemonNatureText.setPosition(infoContainerTextXPos, 125); + + this.statsContainer.setScale(0.7); + this.statsContainer.setPosition(30, -3); + this.infoBg.setVisible(false); + this.pokemonMovesContainer.setVisible(false); + } + makeRoomForConfirmUi(speedMultiplier: number = 1, fromCatch: boolean = false): Promise { const xPosition = fromCatch ? this.initialX - this.infoWindowWidth - 65 : this.initialX - this.infoWindowWidth - ConfirmUiHandler.windowWidth; return new Promise(resolve => { diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 8ec91b59480..6c988b43043 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -49,6 +49,7 @@ import RenameFormUiHandler from "./rename-form-ui-handler"; import AdminUiHandler from "./admin-ui-handler"; import RunHistoryUiHandler from "./run-history-ui-handler"; import RunInfoUiHandler from "./run-info-ui-handler"; +import EggSummaryUiHandler from "./egg-summary-ui-handler"; import TestDialogueUiHandler from "#app/ui/test-dialogue-ui-handler"; import AutoCompleteUiHandler from "./autocomplete-ui-handler"; @@ -66,6 +67,7 @@ export enum Mode { STARTER_SELECT, EVOLUTION_SCENE, EGG_HATCH_SCENE, + EGG_HATCH_SUMMARY, CONFIRM, OPTION_SELECT, MENU, @@ -171,6 +173,7 @@ export default class UI extends Phaser.GameObjects.Container { new StarterSelectUiHandler(scene), new EvolutionSceneHandler(scene), new EggHatchSceneHandler(scene), + new EggSummaryUiHandler(scene), new ConfirmUiHandler(scene), new OptionSelectUiHandler(scene), new MenuUiHandler(scene),