From c5a66326dda0795692aac3c79565e0cee865d04d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:23:16 -0700 Subject: [PATCH] [QoL][Feature] Hall of Fame + Run History (#3251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fixed SaveSessionData issue + Added loss details + removed Modifiers * Final changes * Updated code with the current repo + mode localization * Final touches before moving to a clean branch * Manual merging * Some more merging + updating * Fixed import * TypeDocs issues * Fixed relevant typedoc issues * Manual merge * More fixes * So many commits for so little * Localization Updates * Very barebones implementation of 'favorite' runs - not planning to implement any time soon though * menu-ui-handler.ts localizations * Update src/locales/ko/run-history-ui-handler.ts Thank you! Co-authored-by: Enoch * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/menu-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * The German name for Squirtle is silly * Fixed Run-Entry Def * Commented out networking functionality * Commenting out network functionality pt2 * Update src/locales/pt_BR/menu-ui-handler.ts Co-authored-by: José Ricardo Fleury Oliveira * Update src/locales/pt_BR/run-history-ui-handler.ts Co-authored-by: José Ricardo Fleury Oliveira * Update src/locales/ko/menu-ui-handler.ts Co-authored-by: Enoch * Update src/locales/zh_CN/menu-ui-handler.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/zh_CN/run-history-ui-handler.ts Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Update src/locales/fr/menu-ui-handler.ts Co-authored-by: Lugiad' * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Update src/locales/it/menu-ui-handler.ts Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Update src/locales/it/run-history-ui-handler.ts Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Fixed SaveSessionData issue + Added loss details + removed Modifiers * Final changes * Updated code with the current repo + mode localization * Final touches before moving to a clean branch * Manual merging * Some more merging + updating * Fixed import * TypeDocs issues * Fixed relevant typedoc issues * Manual merge * More fixes * So many commits for so little * Localization Updates * Very barebones implementation of 'favorite' runs - not planning to implement any time soon though * menu-ui-handler.ts localizations * Update src/locales/ko/run-history-ui-handler.ts Thank you! Co-authored-by: Enoch * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/menu-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * The German name for Squirtle is silly * Fixed Run-Entry Def * Commented out networking functionality * Commenting out network functionality pt2 * Fixed cursor-close out freeze and replaced hall of fame background * Removed console.log * Fixed cursor freeze bug + changed hall of fame background * cursor freeze bug fix pt 2 * Revert "Disable egg gacha in rewards (#3304)" This reverts commit 3a87c8657fa8c65b306a7b7bc883a834002485c2. * Merging menu-ui-handler.ts * Merging phases.ts * Manual merge for game-data.ts * Manual merge locales/en/config.ts * Manual merge of menu-ui-handler.ts * Manual ui.ts merge * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Revert "Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into runHistoryNew" This reverts commit 5c6fcf6ec485ab0a967fd714d95928b04435a7ea, reversing changes made to 751bf4a43327118b24bc55cba3a5230f96eb6d27. * Revert "Revert "Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into runHistoryNew"" This reverts commit f6c3580ad096b587f9b30f5d0aeb4bc9d630f726. * Added ending art to victorious runs * Added darker overlay instead * Hall of Fame art * Actual BG Images * some bug fixing * some bug fixing p2 * some minor changes * some minor changes * Changed order of runs displayed to newest --> oldest * console.log for debugging * Export/Import Run History function * added import validation * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Felt coder's guilt for bad buttons implementation * strict-null changes * New Localizations * Update src/locales/fr/menu-ui-handler.ts Co-authored-by: Lugiad' * Apply suggestions from code review Thank you! Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Apply suggestions from code review run-info-ui-handler comments fix Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/locales/es/menu-ui-handler.ts Co-authored-by: Asdar * Update src/locales/es/menu-ui-handler.ts Co-authored-by: Asdar * Update src/locales/es/run-history-ui-handler.ts Co-authored-by: Asdar * Lost this file somehow * Added do not delete comments * Apply suggestions from code review Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * flx Changes * Localizations * Fixing Git test issues * Fixed issues found by Starkrieg * removed console log * Fixed cursor bugs * github pages issue * Update src/ui/run-info-ui-handler.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Update src/ui/run-history-ui-handler.ts Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Corrade's comments * The things I do for Github PAges * Preventing menu freeze * Double trainer battles and fresh start challenge * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Update src/locales/ko/menu-ui-handler.ts Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> * Update src/locales/pt_BR/run-history-ui-handler.ts Co-authored-by: José Ricardo Fleury Oliveira * Update src/locales/pt_BR/menu-ui-handler.ts Co-authored-by: José Ricardo Fleury Oliveira * Update src/locales/de/menu-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/de/run-history-ui-handler.ts Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> * Update src/locales/it/run-history-ui-handler.ts Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Update src/locales/it/menu-ui-handler.ts Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Update src/locales/ko/run-history-ui-handler.ts Co-authored-by: Enoch * Apply suggestions from code review Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> * Localizations + Error Message Update Co-authored-by: protimita Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * small fixes * flx-sta suggestions + Localizations Co-authored-by: Asdar Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> * Update src/locales/it/run-history-ui-handler.ts Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> * Added dynamic text positioning based on container size + small fixes * Thanks Adri1 ! Quick Fix for localizing wave in RunInfo * Transfered defeat parsing to smaller functions and added page modes * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Run History UI handler documentation * Fixed rival names * some comments * Apply suggestions from code review Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: Asdar Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira * Finished documentation * Fixed incorrect rival name handling * Corrected limit-handling * Update src/locales/fr/run-history-ui-handler.ts Co-authored-by: Lugiad' * Cleaned up getrunhistorydata() per flx-sta's suggestions * Added some override tags? * Added scopes/override notes to classes/class variables/class functions * Moved code from phases.ts to game-over-phase.ts * Fixing game-data whoops * ughhhhh * Update src/ui/run-history-ui-handler.ts Co-authored-by: Adrián T. <99520451+Vassiat@users.noreply.github.com> * Fixed cursor and updated money. Note - need to fix money handling for Asian languages * Money appears according to settings * typedocs blah blah * cleaning up manage data options * Final flx fixes + No Run History handling * Translation update. --------- Co-authored-by: Frutescens Co-authored-by: Enoch Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com> Co-authored-by: José Ricardo Fleury Oliveira Co-authored-by: Yonmaru40 <47717431+40chyan@users.noreply.github.com> Co-authored-by: Lugiad' Co-authored-by: Niccolò <123510358+NicusPulcis@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Asdar Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com> Co-authored-by: sodam <66295123+sodaMelon@users.noreply.github.com> Co-authored-by: protimita Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com> Co-authored-by: Adrián T. <99520451+Vassiat@users.noreply.github.com> --- public/images/ui/hall_of_fame_blue.png | Bin 0 -> 1531 bytes public/images/ui/hall_of_fame_red.png | Bin 0 -> 1531 bytes public/images/ui/legacy/hall_of_fame_blue.png | Bin 0 -> 1531 bytes public/images/ui/legacy/hall_of_fame_red.png | Bin 0 -> 1531 bytes src/enums/game-data-type.ts | 3 +- src/locales/ca_ES/config.ts | 4 +- src/locales/ca_ES/menu-ui-handler.ts | 3 + src/locales/ca_ES/run-history-ui-handler.ts | 42 + src/locales/de/config.ts | 4 +- src/locales/de/menu-ui-handler.ts | 3 + src/locales/de/run-history-ui-handler.ts | 42 + src/locales/en/config.ts | 2 + src/locales/en/menu-ui-handler.json | 3 + src/locales/en/run-history.json | 37 + src/locales/es/config.ts | 2 + src/locales/es/menu-ui-handler.json | 3 + src/locales/es/run-history.json | 37 + src/locales/fr/config.ts | 4 +- src/locales/fr/menu-ui-handler.ts | 3 + src/locales/fr/run-history-ui-handler.ts | 42 + src/locales/it/config.ts | 4 +- src/locales/it/menu-ui-handler.ts | 3 + src/locales/it/run-history-ui-handler.ts | 42 + src/locales/ja/config.ts | 4 +- src/locales/ja/menu-ui-handler.ts | 3 + src/locales/ja/run-history-ui-handler.ts | 42 + src/locales/ko/config.ts | 4 +- src/locales/ko/menu-ui-handler.ts | 3 + src/locales/ko/run-history-ui-handler.ts | 42 + src/locales/pt_BR/config.ts | 4 +- src/locales/pt_BR/menu-ui-handler.ts | 3 + src/locales/pt_BR/run-history-ui-handler.ts | 42 + src/locales/zh_CN/config.ts | 4 +- src/locales/zh_CN/menu-ui-handler.ts | 3 + src/locales/zh_CN/run-history-ui-handler.ts | 42 + src/locales/zh_TW/config.ts | 4 +- src/locales/zh_TW/menu-ui-handler.ts | 3 + src/locales/zh_TW/run-history-ui-handler.ts | 42 + src/phases/game-over-phase.ts | 7 + src/system/game-data.ts | 122 +++ src/ui-inputs.ts | 3 +- src/ui/menu-ui-handler.ts | 25 +- src/ui/run-history-ui-handler.ts | 388 ++++++++ src/ui/run-info-ui-handler.ts | 850 ++++++++++++++++++ src/ui/ui.ts | 11 +- 45 files changed, 1920 insertions(+), 14 deletions(-) create mode 100644 public/images/ui/hall_of_fame_blue.png create mode 100644 public/images/ui/hall_of_fame_red.png create mode 100644 public/images/ui/legacy/hall_of_fame_blue.png create mode 100644 public/images/ui/legacy/hall_of_fame_red.png create mode 100644 src/locales/ca_ES/run-history-ui-handler.ts create mode 100644 src/locales/de/run-history-ui-handler.ts create mode 100644 src/locales/en/run-history.json create mode 100644 src/locales/es/run-history.json create mode 100644 src/locales/fr/run-history-ui-handler.ts create mode 100644 src/locales/it/run-history-ui-handler.ts create mode 100644 src/locales/ja/run-history-ui-handler.ts create mode 100644 src/locales/ko/run-history-ui-handler.ts create mode 100644 src/locales/pt_BR/run-history-ui-handler.ts create mode 100644 src/locales/zh_CN/run-history-ui-handler.ts create mode 100644 src/locales/zh_TW/run-history-ui-handler.ts create mode 100644 src/ui/run-history-ui-handler.ts create mode 100644 src/ui/run-info-ui-handler.ts diff --git a/public/images/ui/hall_of_fame_blue.png b/public/images/ui/hall_of_fame_blue.png new file mode 100644 index 0000000000000000000000000000000000000000..87fadf565fdd6e20d33672f16599253ca7b85963 GIT binary patch literal 1531 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yow{Wll$)``g3IZv{;vjb?hIQv;UNSJS&h~V1 z45^5Fd)HC$fP#qg!9}9%CmbDQuej-6oO!FFyE*6IlT$OY=bu(~XmAKfkT{U=T1i;p z1Gj`jJ5V6(07Kep@i`2P%rDqD1Ws^C9LV|FB&?9YaF>CRDUy+S#@e_7CKjFr(h3d= z+6fGsuX6h}G%%Jhvv8!a@+54lI>;$t(2&=_(72|7@l2S$1tSyN1zrJ#38DrKH(#-; zI~-ux#mL06iHXf1dhY`k4haWyESC2CuB`2vvNpbeDVLG?#=H0`rc9vC>*6a$`J>@6 znjVPB4~>S`?J^l3at>4$9*}Zl&|7L3{(*6~{x0rTV42OA8sVAd>&u`8WOD#92wV!D P45B<;{an^LB{Ts58K?I> literal 0 HcmV?d00001 diff --git a/public/images/ui/hall_of_fame_red.png b/public/images/ui/hall_of_fame_red.png new file mode 100644 index 0000000000000000000000000000000000000000..5d4d5e41e9c0534689439afa962e02405a9e311e GIT binary patch literal 1531 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yow{Wll$)``g3IZv{;vjb?hIQv;UNSJS&h~V1 z45^5Fd)HC$fP#qg!AGLf5{|C5A`lHxXZxE6v@auV{Kdk6ARA*X$1!b z?F5F+SGoNf8W>BMSvXQyc@nl&9pn@+Xvk||Xk634cqUBWf{}^s0nb@3IW{L%0j zO%KH6hsKT1tuh%Oat>4$9*}Zl&|7L(ogCRDUy+S#@e_7CKjFr(h3d= z+6fGsuX6h}G%%Jhvv8!a@+54lI>;$t(2&=_(72|7@l2S$1tSyN1zrJ#38DrKH(#-; zI~-ux#mL06iHXf1dhY`k4haWyESC2CuB`2vvNpbeDVLG?#=H0`rc9vC>*6a$`J>@6 znjVPB4~>S`?J^l3at>4$9*}Zl&|7L3{(*6~{x0rTV42OA8sVAd>&u`8WOD#92wV!D P45B<;{an^LB{Ts58K?I> literal 0 HcmV?d00001 diff --git a/public/images/ui/legacy/hall_of_fame_red.png b/public/images/ui/legacy/hall_of_fame_red.png new file mode 100644 index 0000000000000000000000000000000000000000..5d4d5e41e9c0534689439afa962e02405a9e311e GIT binary patch literal 1531 zcmeAS@N?(olHy`uVBq!ia0y~yU~~Yow{Wll$)``g3IZv{;vjb?hIQv;UNSJS&h~V1 z45^5Fd)HC$fP#qg!AGLf5{|C5A`lHxXZxE6v@auV{Kdk6ARA*X$1!b z?F5F+SGoNf8W>BMSvXQyc@nl&9pn@+Xvk||Xk634cqUBWf{}^s0nb@3IW{L%0j zO%KH6hsKT1tuh%Oat>4$9*}Zl&|7L(og { + if (sessionData) { + this.scene.gameData.saveRunHistory(this.scene, sessionData, this.victory); + } + }).catch(err => { + console.error("Failed to save run to history.", err); + }); const fadeDuration = this.victory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index be890505654..e06eb5e4b74 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -44,6 +44,7 @@ import { WeatherType } from "#app/enums/weather-type.js"; import { TerrainType } from "#app/data/terrain.js"; import { OutdatedPhase } from "#app/phases/outdated-phase.js"; import { ReloadSessionPhase } from "#app/phases/reload-session-phase.js"; +import { RUN_HISTORY_LIMIT } from "#app/ui/run-history-ui-handler"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -75,6 +76,8 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str return "tutorials"; case GameDataType.SEEN_DIALOGUES: return "seenDialogues"; + case GameDataType.RUN_HISTORY: + return "runHistoryData"; } } @@ -182,6 +185,15 @@ export const AbilityAttr = { ABILITY_HIDDEN: 4 }; +export type RunHistoryData = Record; + +export interface RunEntry { + entry: SessionSaveData; + isVictory: boolean; + /*Automatically set to false at the moment - implementation TBD*/ + isFavorite: boolean; +} + export type StarterMoveset = [ Moves ] | [ Moves, Moves ] | [ Moves, Moves, Moves ] | [ Moves, Moves, Moves, Moves ]; export interface StarterFormMoveData { @@ -290,6 +302,7 @@ export class GameData { public starterData: StarterData; public gameStats: GameStats; + public runHistory: RunHistoryData; public unlocks: Unlocks; @@ -310,6 +323,7 @@ export class GameData { this.secretId = Utils.randInt(65536); this.starterData = {}; this.gameStats = new GameStats(); + this.runHistory = {}; this.unlocks = { [Unlockables.ENDLESS_MODE]: false, [Unlockables.MINI_BLACK_HOLE]: false, @@ -445,6 +459,11 @@ export class GameData { if (versions[0] !== versions[1]) { const [ versionNumbers, oldVersionNumbers ] = versions.map(ver => ver.split('.').map(v => parseInt(v))); }*/ + const lsItemKey = `runHistoryData_${loggedInUser?.username}`; + const lsItem = localStorage.getItem(lsItemKey); + if (!lsItem) { + localStorage.setItem(lsItemKey, encrypt("", true)); + } this.trainerId = systemData.trainerId; this.secretId = systemData.secretId; @@ -556,6 +575,98 @@ export class GameData { }); } + /** + * Retrieves current run history data, organized by time stamp. + * At the moment, only retrievable from locale cache + */ + async getRunHistoryData(scene: BattleScene): Promise { + if (!Utils.isLocal) { + /** + * Networking Code DO NOT DELETE! + * + const response = await Utils.apiFetch("savedata/runHistory", true); + const data = await response.json(); + */ + const lsItemKey = `runHistoryData_${loggedInUser?.username}`; + const lsItem = localStorage.getItem(lsItemKey); + if (lsItem) { + const cachedResponse = lsItem; + if (cachedResponse) { + const runHistory = JSON.parse(decrypt(cachedResponse, true)); + return runHistory; + } + return {}; + // check to see whether cachedData or serverData is more up-to-date + /** + * Networking Code DO NOT DELETE! + * + if ( Object.keys(cachedRHData).length >= Object.keys(data).length ) { + return cachedRHData; + } + */ + } else { + localStorage.setItem(`runHistoryData_${loggedInUser?.username}`, ""); + return {}; + } + } else { + const lsItemKey = `runHistoryData_${loggedInUser?.username}`; + const lsItem = localStorage.getItem(lsItemKey); + if (lsItem) { + const cachedResponse = lsItem; + if (cachedResponse) { + const runHistory : RunHistoryData = JSON.parse(decrypt(cachedResponse, true)); + return runHistory; + } + return {}; + } else { + localStorage.setItem(`runHistoryData_${loggedInUser?.username}`, ""); + return {}; + } + } + } + + /** + * Saves a new entry to Run History + * @param scene: BattleScene object + * @param runEntry: most recent SessionSaveData of the run + * @param isVictory: result of the run + * Arbitrary limit of 25 runs per player - Will delete runs, starting with the oldest one, if needed + */ + async saveRunHistory(scene: BattleScene, runEntry : SessionSaveData, isVictory: boolean): Promise { + const runHistoryData = await this.getRunHistoryData(scene); + // runHistoryData should always return run history or {} empty object + const timestamps = Object.keys(runHistoryData); + const timestampsNo = timestamps.map(Number); + + // Arbitrary limit of 25 entries per user --> Can increase or decrease + while (timestamps.length >= RUN_HISTORY_LIMIT ) { + const oldestTimestamp = Math.min.apply(Math, timestampsNo); + delete runHistoryData[oldestTimestamp]; + } + + const timestamp = (runEntry.timestamp).toString(); + runHistoryData[timestamp] = { + entry: runEntry, + isVictory: isVictory, + isFavorite: false, + }; + localStorage.setItem(`runHistoryData_${loggedInUser?.username}`, encrypt(JSON.stringify(runHistoryData), true)); + /** + * Networking Code DO NOT DELETE + * + if (!Utils.isLocal) { + try { + await Utils.apiPost("savedata/runHistory", JSON.stringify(runHistoryData), undefined, true); + return true; + } catch (err) { + console.log("savedata/runHistory POST failed : ", err); + return false; + } + } + */ + return true; + } + parseSystemData(dataStr: string): SystemSaveData { return JSON.parse(dataStr, (k: string, v: any) => { if (k === "gameStats") { @@ -1296,6 +1407,17 @@ export class GameData { const sessionData = this.parseSessionData(dataStr); valid = !!sessionData.party && !!sessionData.enemyParty && !!sessionData.timestamp; break; + case GameDataType.RUN_HISTORY: + const data = JSON.parse(dataStr); + const keys = Object.keys(data); + keys.forEach((key) => { + const entryKeys = Object.keys(data[key]); + valid = ["isFavorite", "isVictory", "entry"].every(v => entryKeys.includes(v)) && entryKeys.length === 3; + }); + if (valid) { + localStorage.setItem(`runHistoryData_${loggedInUser?.username}`, dataStr); + } + break; case GameDataType.SETTINGS: case GameDataType.TUTORIALS: valid = true; diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index d514ddb7823..a8ecc860aab 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -11,6 +11,7 @@ import SettingsKeyboardUiHandler from "#app/ui/settings/settings-keyboard-ui-han import BattleScene from "./battle-scene"; import SettingsDisplayUiHandler from "./ui/settings/settings-display-ui-handler"; import SettingsAudioUiHandler from "./ui/settings/settings-audio-ui-handler"; +import RunInfoUiHandler from "./ui/run-info-ui-handler"; type ActionKeys = Record void>; @@ -189,7 +190,7 @@ export class UiInputs { } buttonCycleOption(button: Button): void { - const whitelist = [StarterSelectUiHandler, SettingsUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; + const whitelist = [StarterSelectUiHandler, SettingsUiHandler, RunInfoUiHandler, SettingsDisplayUiHandler, SettingsAudioUiHandler, SettingsGamepadUiHandler, SettingsKeyboardUiHandler]; const uiHandler = this.scene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { this.scene.ui.processInput(button); diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index dd1c2e3c805..8adf9eee094 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -16,6 +16,7 @@ enum MenuOptions { GAME_SETTINGS, ACHIEVEMENTS, STATS, + RUN_HISTORY, VOUCHERS, EGG_LIST, EGG_GACHA, @@ -209,6 +210,22 @@ export default class MenuUiHandler extends MessageUiHandler { }, keepOpen: true }); + manageDataOptions.push({ + label: i18next.t("menuUiHandler:importRunHistory"), + handler: () => { + this.scene.gameData.importData(GameDataType.RUN_HISTORY); + return true; + }, + keepOpen: true + }); + manageDataOptions.push({ + label: i18next.t("menuUiHandler:exportRunHistory"), + handler: () => { + this.scene.gameData.tryExportData(GameDataType.RUN_HISTORY); + return true; + }, + keepOpen: true + }); if (Utils.isLocal || Utils.isBeta) { manageDataOptions.push({ label: i18next.t("menuUiHandler:importData"), @@ -252,9 +269,11 @@ export default class MenuUiHandler extends MessageUiHandler { keepOpen: true }); + //Thank you Vassiat this.manageDataConfig = { xOffset: 98, - options: manageDataOptions + options: manageDataOptions, + maxOptions: 7 }; const communityOptions: OptionSelectItem[] = [ @@ -365,6 +384,10 @@ export default class MenuUiHandler extends MessageUiHandler { ui.setOverlayMode(Mode.GAME_STATS); success = true; break; + case MenuOptions.RUN_HISTORY: + ui.setOverlayMode(Mode.RUN_HISTORY); + success = true; + break; case MenuOptions.VOUCHERS: ui.setOverlayMode(Mode.VOUCHERS); success = true; diff --git a/src/ui/run-history-ui-handler.ts b/src/ui/run-history-ui-handler.ts new file mode 100644 index 00000000000..253c49cd6ce --- /dev/null +++ b/src/ui/run-history-ui-handler.ts @@ -0,0 +1,388 @@ +import BattleScene from "../battle-scene"; +import { GameModes } from "../game-mode"; +import { TextStyle, addTextObject } from "./text"; +import { Mode } from "./ui"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import PokemonData from "../system/pokemon-data"; +import MessageUiHandler from "./message-ui-handler"; +import i18next from "i18next"; +import {Button} from "../enums/buttons"; +import { BattleType } from "../battle"; +import { RunEntry } from "../system/game-data"; +import { PlayerGender } from "#enums/player-gender"; +import { TrainerVariant } from "../field/trainer"; + +export type RunSelectCallback = (cursor: integer) => void; + +export const RUN_HISTORY_LIMIT: number = 25; + +/** + * RunHistoryUiHandler handles the UI of the Run History Menu + * Run History itself is broken into an array of RunEntryContainer objects that can show the user basic details about their run and allow them to access more details about their run through cursor action. + * It navigates similarly to the UI of the save slot select menu. + * The only valid input buttons are Button.ACTION and Button.CANCEL. + */ +export default class RunHistoryUiHandler extends MessageUiHandler { + + private runSelectContainer: Phaser.GameObjects.Container; + private runsContainer: Phaser.GameObjects.Container; + private runSelectMessageBox: Phaser.GameObjects.NineSlice; + private runSelectMessageBoxContainer: Phaser.GameObjects.Container; + private runs: RunEntryContainer[]; + + private runSelectCallback: RunSelectCallback | null; + + private scrollCursor: integer = 0; + + private cursorObj: Phaser.GameObjects.NineSlice | null; + + private runContainerInitialY: number; + + constructor(scene: BattleScene) { + super(scene, Mode.RUN_HISTORY); + } + + override setup() { + const ui = this.getUi(); + + this.runSelectContainer = this.scene.add.container(0, 0); + this.runSelectContainer.setVisible(false); + ui.add(this.runSelectContainer); + + const loadSessionBg = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width / 6, -this.scene.game.canvas.height / 6, 0x006860); + loadSessionBg.setOrigin(0, 0); + this.runSelectContainer.add(loadSessionBg); + + this.runContainerInitialY = -this.scene.game.canvas.height / 6 + 8; + + this.runsContainer = this.scene.add.container(8, this.runContainerInitialY); + this.runSelectContainer.add(this.runsContainer); + + this.runs = []; + + this.scene.loadImage("hall_of_fame_red", "ui"); + this.scene.loadImage("hall_of_fame_blue", "ui"); + // For some reason, the game deletes/unloads the rival sprites. As a result, Run Info cannot access the rival sprites. + // The rivals are loaded here to have some way of accessing those sprites. + this.scene.loadAtlas("rival_f", "trainer"); + this.scene.loadAtlas("rival_m", "trainer"); + } + + override show(args: any[]): boolean { + super.show(args); + + this.getUi().bringToTop(this.runSelectContainer); + this.runSelectContainer.setVisible(true); + this.populateRuns(this.scene); + + this.setScrollCursor(0); + this.setCursor(0); + + //Destroys the cursor if there are no runs saved so far. + if (this.runs.length === 0) { + this.clearCursor(); + } + + return true; + } + + /** + * Performs a certain action based on the button pressed by the user + * @param button + * The user can navigate through the runs with Button.UP/Button.DOWN. + * Button.ACTION allows the user to access more information about their runs. + * Button.CANCEL allows the user to go back. + */ + override processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + const error = false; + + if ([Button.ACTION, Button.CANCEL].includes(button)) { + if (button === Button.ACTION) { + const cursor = this.cursor + this.scrollCursor; + if (this.runs[cursor]) { + this.scene.ui.setOverlayMode(Mode.RUN_INFO, this.runs[cursor].entryData, true); + } else { + return false; + } + success = true; + return success; + } else { + this.runSelectCallback = null; + success = true; + this.scene.ui.revertMode(); + } + } else if (this.runs.length > 0) { + switch (button) { + case Button.UP: + if (this.cursor) { + success = this.setCursor(this.cursor - 1); + } else if (this.scrollCursor) { + success = this.setScrollCursor(this.scrollCursor - 1); + } + break; + case Button.DOWN: + if (this.cursor < 2) { + success = this.setCursor(this.cursor + 1); + } else if (this.scrollCursor < this.runs.length - 3) { + success = this.setScrollCursor(this.scrollCursor + 1); + } + break; + } + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + return success || error; + } + + /** + * This retrieves the player's run history and facilitates the processes necessary for the output display. + * @param scene: BattleScene + * Runs are displayed from newest --> oldest in descending order. + * In the for loop, each run is processed to create an RunEntryContainer used to display and store the run's unique information + */ + private async populateRuns(scene: BattleScene) { + const response = await this.scene.gameData.getRunHistoryData(this.scene); + const timestamps = Object.keys(response); + if (timestamps.length === 0) { + this.showEmpty(); + return; + } + const timestampsNo = timestamps.map(Number); + if (timestamps.length > 1) { + timestampsNo.sort((a, b) => b - a); + } + const entryCount = timestamps.length; + for (let s = 0; s < entryCount; s++) { + const entry = new RunEntryContainer(this.scene, response[timestampsNo[s]], s); + this.scene.add.existing(entry); + this.runsContainer.add(entry); + this.runs.push(entry); + } + if (this.cursorObj && timestamps.length > 0) { + this.runsContainer.bringToTop(this.cursorObj); + } + } + + /** + * If the player has no runs saved so far, this creates a giant window labeled empty instead. + */ + private async showEmpty() { + const emptyWindow = addWindow(this.scene, 0, 0, 304, 165); + this.runsContainer.add(emptyWindow); + const emptyWindowCoordinates = emptyWindow.getCenter(); + const emptyText = addTextObject(this.scene, 0, 0, i18next.t("saveSlotSelectUiHandler:empty"), TextStyle.WINDOW, {fontSize: "128px"}); + emptyText.setPosition(emptyWindowCoordinates.x-18, emptyWindowCoordinates.y-15); + this.runsContainer.add(emptyText); + } + + override setCursor(cursor: number): boolean { + const changed = super.setCursor(cursor); + + if (!this.cursorObj) { + this.cursorObj = this.scene.add.nineslice(0, 0, "select_cursor_highlight_thick", undefined, 296, 46, 6, 6, 6, 6); + this.cursorObj.setOrigin(0, 0); + this.runsContainer.add(this.cursorObj); + } + this.cursorObj.setPosition(4, 4 + (cursor + this.scrollCursor) * 56); + return changed; + } + + private setScrollCursor(scrollCursor: number): boolean { + const changed = scrollCursor !== this.scrollCursor; + + if (changed) { + this.scrollCursor = scrollCursor; + this.setCursor(this.cursor); + this.scene.tweens.add({ + targets: this.runsContainer, + y: this.runContainerInitialY - 56 * scrollCursor, + duration: Utils.fixedInt(325), + ease: "Sine.easeInOut" + }); + } + return changed; + } + + /** + * Called when the player returns back to the menu + * Uses the functions clearCursor() and clearRuns() + */ + override clear() { + super.clear(); + this.runSelectContainer.setVisible(false); + this.clearCursor(); + this.runSelectCallback = null; + this.clearRuns(); + } + + private clearCursor() { + if (this.cursorObj) { + this.cursorObj.destroy(); + } + this.cursorObj = null; + } + + private clearRuns() { + this.runs.splice(0, this.runs.length); + this.runsContainer.removeAll(true); + } +} + +/** + * RunEntryContainer : stores/displays an individual run + * slotId: necessary for positioning + * entryData: the data of an individual run + */ +class RunEntryContainer extends Phaser.GameObjects.Container { + private slotId: number; + public entryData: RunEntry; + + constructor(scene: BattleScene, entryData: RunEntry, slotId: number) { + super(scene, 0, slotId*56); + + this.slotId = slotId; + this.entryData = entryData; + + this.setup(this.entryData); + + } + + /** + * This processes the individual run's data for display. + * + * Each RunEntryContainer displayed should have the following information: + * Run Result: Victory || Defeat + * Game Mode + Final Wave + * Time Stamp + * + * The player's party and their levels at the time of the last wave of the run are also displayed. + */ + private setup(run: RunEntry) { + + const victory = run.isVictory; + const data = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); + + const slotWindow = addWindow(this.scene, 0, 0, 304, 52); + this.add(slotWindow); + + // Run Result: Victory + if (victory) { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:victory")}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } else { // Run Result: Defeats + const genderLabel = (this.scene.gameData.gender === PlayerGender.FEMALE) ? "F" : "M"; + // Defeats from wild Pokemon battles will show the Pokemon responsible by the text of the run result. + if (data.battleType === BattleType.WILD) { + const enemyContainer = this.scene.add.container(8, 5); + const gameOutcomeLabel = addTextObject(this.scene, 0, 0, `${i18next.t("runHistory:defeatedWild"+genderLabel)}`, TextStyle.WINDOW); + enemyContainer.add(gameOutcomeLabel); + data.enemyParty.forEach((enemyData, e) => { + const enemyIconContainer = this.scene.add.container(65+(e*25), -8); + enemyIconContainer.setScale(0.75); + enemyData.boss = false; + enemyData["player"] = true; + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevel = addTextObject(this.scene, 32, 20, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, undefined); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + this.add(enemyContainer); + } else if (data.battleType === BattleType.TRAINER) { // Defeats from Trainers show the trainer's title and name + const tObj = data.trainer.toTrainer(this.scene); + // Because of the interesting mechanics behind rival names, the rival name and title have to be retrieved differently + const RIVAL_TRAINER_ID_THRESHOLD = 375; + if (data.trainer.trainerType >= RIVAL_TRAINER_ID_THRESHOLD) { + const rivalName = (tObj.variant === TrainerVariant.FEMALE) ? "trainerNames:rival_female" : "trainerNames:rival"; + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:defeatedRival"+genderLabel)} ${i18next.t(rivalName)}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } else { + const gameOutcomeLabel = addTextObject(this.scene, 8, 5, `${i18next.t("runHistory:defeatedTrainer"+genderLabel)}${tObj.getName(0, true)}`, TextStyle.WINDOW); + this.add(gameOutcomeLabel); + } + } + } + + // Game Mode + Waves + // Because Endless (Spliced) tends to have the longest name across languages, the line tends to spill into the party icons. + // To fix this, the Spliced icon is used to indicate an Endless Spliced run + const gameModeLabel = addTextObject(this.scene, 8, 19, "", TextStyle.WINDOW); + let mode = ""; + switch (data.gameMode) { + case GameModes.DAILY: + mode = i18next.t("gameMode:dailyRun"); + break; + case GameModes.SPLICED_ENDLESS: + case GameModes.ENDLESS: + mode = i18next.t("gameMode:endless"); + break; + case GameModes.CLASSIC: + mode = i18next.t("gameMode:classic"); + break; + case GameModes.CHALLENGE: + mode = i18next.t("gameMode:challenge"); + break; + } + gameModeLabel.appendText(mode, false); + if (data.gameMode === GameModes.SPLICED_ENDLESS) { + const splicedIcon = this.scene.add.image(0, 0, "icon_spliced"); + splicedIcon.setScale(0.75); + const coords = gameModeLabel.getTopRight(); + splicedIcon.setPosition(coords.x+5, 27); + this.add(splicedIcon); + // 4 spaces of room for the Spliced icon + gameModeLabel.appendText(" - ", false); + } else { + gameModeLabel.appendText(" - ", false); + } + gameModeLabel.appendText(i18next.t("saveSlotSelectUiHandler:wave")+" "+data.waveIndex, false); + this.add(gameModeLabel); + + const timestampLabel = addTextObject(this.scene, 8, 33, new Date(data.timestamp).toLocaleString(), TextStyle.WINDOW); + this.add(timestampLabel); + + // pokemonIconsContainer holds the run's party Pokemon icons and levels + // Icons should be level with each other here, but there are significant number of icons that have a center axis / position far from the norm. + // The code here does not account for icon weirdness. + const pokemonIconsContainer = this.scene.add.container(140, 17); + + data.party.forEach((p: PokemonData, i: integer) => { + const iconContainer = this.scene.add.container(26 * i, 0); + iconContainer.setScale(0.75); + const pokemon = p.toPokemon(this.scene); + const icon = this.scene.addPokemonIcon(pokemon, 0, 0, 0, 0); + + const text = addTextObject(this.scene, 32, 20, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(pokemon.level, 1000)}`, TextStyle.PARTY, { fontSize: "54px", color: "#f8f8f8" }); + text.setShadow(0, 0, undefined); + text.setStroke("#424242", 14); + text.setOrigin(1, 0); + + iconContainer.add(icon); + iconContainer.add(text); + + pokemonIconsContainer.add(iconContainer); + + pokemon.destroy(); + }); + + this.add(pokemonIconsContainer); + } +} + +interface RunEntryContainer { + scene: BattleScene; +} + diff --git a/src/ui/run-info-ui-handler.ts b/src/ui/run-info-ui-handler.ts new file mode 100644 index 00000000000..79fc61596a0 --- /dev/null +++ b/src/ui/run-info-ui-handler.ts @@ -0,0 +1,850 @@ +import BattleScene from "../battle-scene"; +import { GameModes } from "../game-mode"; +import UiHandler from "./ui-handler"; +import { SessionSaveData } from "../system/game-data"; +import { TextStyle, addTextObject, addBBCodeTextObject, getTextColor } from "./text"; +import { Mode } from "./ui"; +import { addWindow } from "./ui-theme"; +import * as Utils from "../utils"; +import PokemonData from "../system/pokemon-data"; +import i18next from "i18next"; +import {Button} from "../enums/buttons"; +import { BattleType } from "../battle"; +import { TrainerVariant } from "../field/trainer"; +import { Challenges } from "#enums/challenges"; +import { getLuckString, getLuckTextTint } from "../modifier/modifier-type"; +import RoundRectangle from "phaser3-rex-plugins/plugins/roundrectangle.js"; +import { Type, getTypeRgb } from "../data/type"; +import { getNatureStatMultiplier, getNatureName } from "../data/nature"; +import { getVariantTint } from "#app/data/variant"; +import { PokemonHeldItemModifier, TerastallizeModifier } from "../modifier/modifier"; +import {modifierSortFunc} from "../modifier/modifier"; +import { Species } from "#enums/species"; +import { PlayerGender } from "#enums/player-gender"; + +/** + * RunInfoUiMode indicates possible overlays of RunInfoUiHandler. + * MAIN <-- default overlay that can return back to RunHistoryUiHandler + should eventually have its own enum once more pages are added to RunInfoUiHandler + * HALL_OF_FAME, ENDING_ART, etc. <-- overlays that should return back to MAIN + */ +enum RunInfoUiMode { + MAIN, + HALL_OF_FAME, + ENDING_ART +} + +/** + * Some variables are protected because this UI class will most likely be extended in the future to display more information. + * These variables will most likely be shared across 'classes' aka pages. + * I believe that it is possible that the contents/methods of the first page will be placed in their own class that is an extension of RunInfoUiHandler as more pages are added. + * For now, I leave as is. + */ +export default class RunInfoUiHandler extends UiHandler { + protected runInfo: SessionSaveData; + protected isVictory: boolean; + protected isPGF: boolean; + protected pageMode: RunInfoUiMode; + protected runContainer: Phaser.GameObjects.Container; + + private runResultContainer: Phaser.GameObjects.Container; + private runInfoContainer: Phaser.GameObjects.Container; + private partyContainer: Phaser.GameObjects.Container; + private partyHeldItemsContainer: Phaser.GameObjects.Container; + private statsBgWidth: integer; + private partyContainerHeight: integer; + private partyContainerWidth: integer; + + private hallofFameContainer: Phaser.GameObjects.Container; + private endCardContainer: Phaser.GameObjects.Container; + + private partyInfo: Phaser.GameObjects.Container[]; + private partyVisibility: Boolean; + private modifiersModule: any; + + constructor(scene: BattleScene) { + super(scene, Mode.RUN_INFO); + } + + override async setup() { + this.runContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + // The import of the modifiersModule is loaded here to sidestep async/await issues. + this.modifiersModule = await import("../modifier/modifier"); + this.runContainer.setVisible(false); + } + + /** + * This takes a run's RunEntry and uses the information provided to display essential information about the player's run. + * @param args[0] : a RunEntry object + * + * show() creates these UI objects in order - + * A solid-color background used to hide RunHistoryUiHandler + * Header: Page Title + Option to Display Modifiers + * Run Result Container: + * Party Container: + * this.isVictory === true --> Hall of Fame Container: + */ + override show(args: any[]): boolean { + super.show(args); + + const gameStatsBg = this.scene.add.rectangle(0, 0, this.scene.game.canvas.width, this.scene.game.canvas.height, 0x006860); + gameStatsBg.setOrigin(0, 0); + this.runContainer.add(gameStatsBg); + + const run = args[0]; + // Assigning information necessary for the UI's creation + this.runInfo = this.scene.gameData.parseSessionData(JSON.stringify(run.entry)); + this.isVictory = run.isVictory; + this.isPGF = this.scene.gameData.gender === PlayerGender.FEMALE; + this.pageMode = RunInfoUiMode.MAIN; + + // Creates Header and adds to this.runContainer + this.addHeader(); + + this.statsBgWidth = ((this.scene.game.canvas.width / 6) - 2) / 3; + + // Creates Run Result Container + this.runResultContainer = this.scene.add.container(0, 24); + const runResultWindow = addWindow(this.scene, 0, 0, this.statsBgWidth-11, 65); + runResultWindow.setOrigin(0, 0); + this.runResultContainer.add(runResultWindow); + this.parseRunResult(); + + // Creates Run Info Container + this.runInfoContainer = this.scene.add.container(0, 89); + const runInfoWindow = addWindow(this.scene, 0, 0, this.statsBgWidth-11, 90); + const runInfoWindowCoords = runInfoWindow.getBottomRight(); + this.runInfoContainer.add(runInfoWindow); + this.parseRunInfo(runInfoWindowCoords.x, runInfoWindowCoords.y); + + // Creates Player Party Container + this.partyContainer = this.scene.add.container(this.statsBgWidth-10, 23); + this.parsePartyInfo(); + this.showParty(true); + + this.runContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + this.getUi().bringToTop(this.runContainer); + this.runContainer.setVisible(true); + + // Creates Hall of Fame if the run entry contains a victory + if (this.isVictory) { + this.createHallofFame(); + this.getUi().bringToTop(this.hallofFameContainer); + } + + this.setCursor(0); + + this.getUi().add(this.runContainer); + + this.getUi().hideTooltip(); + + return true; + } + + /** + * Creates and adds the header background, title text, and important buttons to RunInfoUiHandler + * It does check if the run has modifiers before adding a button for the user to display their party's held items + * It does not check if the run has any PokemonHeldItemModifiers though. + */ + private addHeader() { + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); + headerBg.setOrigin(0, 0); + this.runContainer.add(headerBg); + if (this.runInfo.modifiers.length !== 0) { + const headerBgCoords = headerBg.getTopRight(); + const abilityButtonContainer = this.scene.add.container(0, 0); + const abilityButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHeldItems"), TextStyle.WINDOW, {fontSize:"34px"}); + const abilityButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 2, "keyboard", "E.png"); + abilityButtonContainer.add([abilityButtonText, abilityButtonElement]); + abilityButtonContainer.setPosition(headerBgCoords.x - abilityButtonText.displayWidth - abilityButtonElement.displayWidth - 8, 10); + this.runContainer.add(abilityButtonContainer); + } + const headerText = addTextObject(this.scene, 0, 0, i18next.t("runHistory:runInfo"), TextStyle.SETTINGS_LABEL); + headerText.setOrigin(0, 0); + headerText.setPositionRelative(headerBg, 8, 4); + this.runContainer.add(headerText); + } + + /** + * Shows the run's end result + * + * Victory : The run will display options to allow the player to view the Hall of Fame + Ending Art + * Defeat : The run will show the opposing Pokemon (+ Trainer) that the trainer was defeated by. + * Defeat can call either parseWildSingleDefeat(), parseWildDoubleDefeat(), or parseTrainerDefeat() + * + */ + private async parseRunResult() { + const runResultTextStyle = this.isVictory ? TextStyle.SUMMARY : TextStyle.SUMMARY_RED; + const runResultTitle = this.isVictory ? i18next.t("runHistory:victory") : (this.isPGF ? i18next.t("runHistory:defeatedF") : i18next.t("runHistory:defeatedM")); + const runResultText = addBBCodeTextObject(this.scene, 6, 5, `${runResultTitle} - ${i18next.t("saveSlotSelectUiHandler:wave")} ${this.runInfo.waveIndex}`, runResultTextStyle, {fontSize : "65px", lineSpacing: 0.1}); + + if (this.isVictory) { + const hallofFameInstructionContainer = this.scene.add.container(0, 0); + const shinyButtonText = addTextObject(this.scene, 8, 0, i18next.t("runHistory:viewHallOfFame"), TextStyle.WINDOW, {fontSize:"65px"}); + const shinyButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 4, "keyboard", "R.png"); + hallofFameInstructionContainer.add([shinyButtonText, shinyButtonElement]); + + const formButtonText = addTextObject(this.scene, 8, 12, i18next.t("runHistory:viewEndingSplash"), TextStyle.WINDOW, {fontSize:"65px"}); + const formButtonElement = new Phaser.GameObjects.Sprite(this.scene, 0, 16, "keyboard", "F.png"); + hallofFameInstructionContainer.add([formButtonText, formButtonElement]); + + hallofFameInstructionContainer.setPosition(12, 25); + this.runResultContainer.add(hallofFameInstructionContainer); + } + + this.runResultContainer.add(runResultText); + + if (!this.isVictory) { + const enemyContainer = this.scene.add.container(0, 0); + // Wild - Single and Doubles + if (this.runInfo.battleType === BattleType.WILD) { + switch (this.runInfo.enemyParty.length) { + case 1: + // Wild - Singles + this.parseWildSingleDefeat(enemyContainer); + break; + case 2: + //Wild - Doubles + this.parseWildDoubleDefeat(enemyContainer); + break; + } + } else if (this.runInfo.battleType === BattleType.TRAINER) { + this.parseTrainerDefeat(enemyContainer); + } + this.runResultContainer.add(enemyContainer); + } + this.runContainer.add(this.runResultContainer); + } + + /** + * This function is called to edit an enemyContainer to represent a loss from a defeat by a wild single Pokemon battle. + * @param enemyContainer - container holding enemy visual and level information + */ + private parseWildSingleDefeat(enemyContainer: Phaser.GameObjects.Container) { + const enemyIconContainer = this.scene.add.container(0, 0); + const enemyData = this.runInfo.enemyParty[0]; + const bossStatus = enemyData.boss; + enemyData.boss = false; + enemyData["player"] = true; + //addPokemonIcon() throws an error if the Pokemon used is a boss + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevelStyle = bossStatus ? TextStyle.PARTY_RED : TextStyle.PARTY; + const enemyLevel = addTextObject(this.scene, 36, 26, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, enemyLevelStyle, { fontSize: "44px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, undefined); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyContainer.add(enemyIconContainer); + enemyContainer.setPosition(27, 12); + enemy.destroy(); + } + + /** + * This function is called to edit a container to represent a loss from a defeat by a wild double Pokemon battle. + * This function and parseWildSingleDefeat can technically be merged, but I find it tricky to manipulate the different 'centers' a single battle / double battle container will hold. + * @param enemyContainer - container holding enemy visuals and level information + */ + private parseWildDoubleDefeat(enemyContainer: Phaser.GameObjects.Container) { + this.runInfo.enemyParty.forEach((enemyData, e) => { + const enemyIconContainer = this.scene.add.container(0, 0); + const bossStatus = enemyData.boss; + enemyData.boss = false; + enemyData["player"] = true; + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + const enemyLevel = addTextObject(this.scene, 36, 26, `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, bossStatus ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "44px", color: "#f8f8f8" }); + enemyLevel.setShadow(0, 0, undefined); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(1, 0); + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyIconContainer.setPosition(e*35, 0); + enemyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + enemyContainer.setPosition(8, 14); + } + + /** + * This edits a container to represent a loss from a defeat by a trainer battle. + * @param enemyContainer - container holding enemy visuals and level information + * The trainers are placed to the left of their party. + * Depending on the trainer icon, there may be overlap between the edges of the box or their party. (Capes...) + * + * Party Pokemon have their icons, terastalization status, and level shown. + */ + private parseTrainerDefeat(enemyContainer: Phaser.GameObjects.Container) { + // Creating the trainer sprite and adding it to enemyContainer + const tObj = this.runInfo.trainer.toTrainer(this.scene); + const tObjSpriteKey = tObj.config.getSpriteKey(this.runInfo.trainer.variant === TrainerVariant.FEMALE, false); + const tObjSprite = this.scene.add.sprite(0, 5, tObjSpriteKey); + if (this.runInfo.trainer.variant === TrainerVariant.DOUBLE) { + const doubleContainer = this.scene.add.container(5, 8); + tObjSprite.setPosition(-3, -3); + const tObjPartnerSpriteKey = tObj.config.getSpriteKey(true, true); + const tObjPartnerSprite = this.scene.add.sprite(5, -3, tObjPartnerSpriteKey); + // Double Trainers have smaller sprites than Single Trainers + tObjPartnerSprite.setScale(0.20); + tObjSprite.setScale(0.20); + doubleContainer.add(tObjSprite); + doubleContainer.add(tObjPartnerSprite); + doubleContainer.setPosition(12, 38); + enemyContainer.add(doubleContainer); + } else { + tObjSprite.setScale(0.35, 0.35); + tObjSprite.setPosition(12, 28); + enemyContainer.add(tObjSprite); + } + + // Determining which Terastallize Modifier belongs to which Pokemon + // Creates a dictionary {PokemonId: TeraShardType} + const teraPokemon = {}; + this.runInfo.enemyModifiers.forEach((m) => { + const modifier = m.toModifier(this.scene, this.modifiersModule[m.className]); + if (modifier instanceof TerastallizeModifier) { + const teraDetails = modifier?.getArgs(); + const pkmnId = teraDetails[0]; + teraPokemon[pkmnId] = teraDetails[1]; + } + }); + + // Creates the Pokemon icons + level information and adds it to enemyContainer + // 2 Rows x 3 Columns + const enemyPartyContainer = this.scene.add.container(0, 0); + this.runInfo.enemyParty.forEach((enemyData, e) => { + const pokemonRowHeight = Math.floor(e/3); + const enemyIconContainer = this.scene.add.container(0, 0); + enemyIconContainer.setScale(0.6); + const isBoss = enemyData.boss; + enemyData.boss = false; + enemyData["player"] = true; + const enemy = enemyData.toPokemon(this.scene); + const enemyIcon = this.scene.addPokemonIcon(enemy, 0, 0, 0, 0); + // Applying Terastallizing Type tint to Pokemon icon + // If the Pokemon is a fusion, it has two sprites and so, the tint has to be applied to each icon separately + const enemySprite1 = enemyIcon.list[0] as Phaser.GameObjects.Sprite; + const enemySprite2 = (enemyIcon.list.length > 1) ? enemyIcon.list[1] as Phaser.GameObjects.Sprite : undefined; + if (teraPokemon[enemyData.id]) { + const teraTint = getTypeRgb(teraPokemon[enemyData.id]); + const teraColor = new Phaser.Display.Color(teraTint[0], teraTint[1], teraTint[2]); + enemySprite1.setTint(teraColor.color); + if (enemySprite2) { + enemySprite2.setTint(teraColor.color); + } + } + enemyIcon.setPosition(39*(e%3)+5, (35*pokemonRowHeight)); + const enemyLevel = addTextObject(this.scene, 43*(e%3), (27*(pokemonRowHeight+1)), `${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatLargeNumber(enemy.level, 1000)}`, isBoss ? TextStyle.PARTY_RED : TextStyle.PARTY, { fontSize: "54px" }); + enemyLevel.setShadow(0, 0, undefined); + enemyLevel.setStroke("#424242", 14); + enemyLevel.setOrigin(0, 0); + + enemyIconContainer.add(enemyIcon); + enemyIconContainer.add(enemyLevel); + enemyPartyContainer.add(enemyIconContainer); + enemy.destroy(); + }); + enemyPartyContainer.setPosition(25, 15); + enemyContainer.add(enemyPartyContainer); + } + + /** + * Shows information about the run like the run's mode, duration, luck, money, and player held items + * The values for luck and money are from the end of the run, not the player's luck at the start of the run. + * @param windowX + * @param windowY These two params are the coordinates of the window's bottom right corner. This is used to dynamically position Luck based on its length, creating a nice layout regardless of language / luck value. + */ + private async parseRunInfo(windowX: number, windowY: number) { + // Parsing and displaying the mode. + // In the future, parsing Challenges + Challenge Rules may have to be reworked as PokeRogue adds additional challenges and users can stack these challenges in various ways. + const modeText = addBBCodeTextObject(this.scene, 7, 0, "", TextStyle.WINDOW, {fontSize : "50px", lineSpacing:3}); + modeText.setPosition(7, 5); + modeText.appendText(i18next.t("runHistory:mode")+": ", false); + switch (this.runInfo.gameMode) { + case GameModes.DAILY: + modeText.appendText(`${i18next.t("gameMode:dailyRun")}`, false); + break; + case GameModes.SPLICED_ENDLESS: + modeText.appendText(`${i18next.t("gameMode:endlessSpliced")}`, false); + if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { + modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); + modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + break; + case GameModes.CHALLENGE: + modeText.appendText(`${i18next.t("gameMode:challenge")}`, false); + modeText.appendText(`\t\t${i18next.t("runHistory:challengeRules")}: `); + const runChallenges = this.runInfo.challenges; + const rules: string[] = []; + for (let i = 0; i < runChallenges.length; i++) { + if (runChallenges[i].id === Challenges.SINGLE_GENERATION && runChallenges[i].value !== 0) { + rules.push(i18next.t(`runHistory:challengeMonoGen${runChallenges[i].value}`)); + } else if (runChallenges[i].id === Challenges.SINGLE_TYPE && runChallenges[i].value !== 0) { + rules.push(i18next.t(`pokemonInfo:Type.${Type[runChallenges[i].value-1]}` as const)); + } else if (runChallenges[i].id === Challenges.FRESH_START && runChallenges[i].value !== 0) { + rules.push(i18next.t("challenges:freshStart.name")); + } + } + if (rules) { + for (let i = 0; i < rules.length; i++) { + if (i > 0) { + modeText.appendText(" + ", false); + } + modeText.appendText(rules[i], false); + } + } + break; + case GameModes.ENDLESS: + modeText.appendText(`${i18next.t("gameMode:endless")}`, false); + // If the player achieves a personal best in Endless, the mode text will be tinted similarly to SSS luck to celebrate their achievement. + if (this.runInfo.waveIndex === this.scene.gameData.gameStats.highestEndlessWave) { + modeText.appendText(` [${i18next.t("runHistory:personalBest")}]`, false); + modeText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + break; + case GameModes.CLASSIC: + modeText.appendText(`${i18next.t("gameMode:classic")}`, false); + break; + } + + // Duration + Money + const runInfoTextContainer = this.scene.add.container(0, 0); + const runInfoText = addBBCodeTextObject(this.scene, 7, 0, "", TextStyle.WINDOW, {fontSize : "50px", lineSpacing:3}); + const runTime = Utils.getPlayTimeString(this.runInfo.playTime); + runInfoText.appendText(`${i18next.t("runHistory:runLength")}: ${runTime}`, false); + const runMoney = Utils.formatMoney(this.runInfo.money, 1000); + runInfoText.appendText(`[color=${getTextColor(TextStyle.MONEY)}]${i18next.t("battleScene:moneyOwned", {formattedMoney : runMoney})}[/color]`); + runInfoText.setPosition(7, 70); + runInfoTextContainer.add(runInfoText); + // Luck + // Uses the parameters windowX and windowY to dynamically position the luck value neatly into the bottom right corner + const luckText = addBBCodeTextObject(this.scene, 0, 0, "", TextStyle.WINDOW, {fontSize: "55px"}); + const luckValue = Phaser.Math.Clamp(this.runInfo.party.map(p => p.toPokemon(this.scene).getLuck()).reduce((total: integer, value: integer) => total += value, 0), 0, 14); + let luckInfo = i18next.t("runHistory:luck")+": "+getLuckString(luckValue); + if (luckValue < 14) { + luckInfo = "[color=#"+(getLuckTextTint(luckValue)).toString(16)+"]"+luckInfo+"[/color]"; + } else { + luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969); + } + luckText.appendText("[align=right]"+luckInfo+"[/align]", false); + luckText.setPosition(windowX-luckText.displayWidth-5, windowY-13); + runInfoTextContainer.add(luckText); + + // Player Held Items + // A max of 20 items can be displayed. A + sign will be added if the run's held items pushes past this maximum to show the user that there are more. + if (this.runInfo.modifiers.length) { + let visibleModifierIndex = 0; + + const modifierIconsContainer = this.scene.add.container(8, (this.runInfo.gameMode === GameModes.CHALLENGE) ? 20 : 15); + modifierIconsContainer.setScale(0.45); + for (const m of this.runInfo.modifiers) { + const modifier = m.toModifier(this.scene, this.modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier) { + continue; + } + const icon = modifier?.getIcon(this.scene, false); + if (icon) { + const rowHeightModifier = Math.floor(visibleModifierIndex/7); + icon.setPosition(24 * (visibleModifierIndex%7), 20 + (35 * rowHeightModifier)); + modifierIconsContainer.add(icon); + } + + if (++visibleModifierIndex === 20) { + const maxItems = addTextObject(this.scene, 45, 90, "+", TextStyle.WINDOW); + maxItems.setPositionRelative(modifierIconsContainer, 70, 45); + this.runInfoContainer.add(maxItems); + break; + } + } + this.runInfoContainer.add(modifierIconsContainer); + } + + this.runInfoContainer.add(modeText); + this.runInfoContainer.add(runInfoTextContainer); + this.runContainer.add(this.runInfoContainer); + } + + /** + * Parses and displays the run's player party. + * Default Information: Icon, Level, Nature, Ability, Passive, Shiny Status, Fusion Status, Stats, and Moves. + * B-Side Information: Icon + Held Items (Can be displayed to the user through pressing the abilityButton) + */ + private parsePartyInfo(): void { + const party = this.runInfo.party; + const currentLanguage = i18next.resolvedLanguage ?? "en"; + const windowHeight = ((this.scene.game.canvas.height / 6) - 23)/6; + + party.forEach((p: PokemonData, i: integer) => { + const pokemonInfoWindow = new RoundRectangle(this.scene, 0, 14, (this.statsBgWidth*2)+10, windowHeight-2, 3); + + const pokemon = p.toPokemon(this.scene); + const pokemonInfoContainer = this.scene.add.container(this.statsBgWidth+5, (windowHeight-0.5)*i); + + const types = pokemon.getTypes(); + const type1 = getTypeRgb(types[0]); + const type1Color = new Phaser.Display.Color(type1[0], type1[1], type1[2]); + + const bgColor = type1Color.clone().darken(45); + pokemonInfoWindow.setFillStyle(bgColor.color); + + const iconContainer = this.scene.add.container(0, 0); + const icon = this.scene.addPokemonIcon(pokemon, 0, 0, 0, 0); + icon.setScale(0.75); + icon.setPosition(-99, 1); + const type2 = types[1] ? getTypeRgb(types[1]) : undefined; + const type2Color = type2 ? new Phaser.Display.Color(type2[0], type2[1], type2[2]) : undefined; + type2Color ? pokemonInfoWindow.setStrokeStyle(1, type2Color.color, 0.95) : pokemonInfoWindow.setStrokeStyle(1, type1Color.color, 0.95); + + this.getUi().bringToTop(icon); + + // Contains Name, Level + Nature, Ability, Passive + const pokeInfoTextContainer = this.scene.add.container(-85, 3.5); + const textContainerFontSize = "34px"; + const pNature = getNatureName(pokemon.nature); + const pName = pokemon.getNameToRender(); + //With the exception of Korean/Traditional Chinese/Simplified Chinese, the code shortens the terms for ability and passive to their first letter. + //These languages are exempted because they are already short enough. + const exemptedLanguages = ["ko", "zh_CN", "zh_TW"]; + let passiveLabel = i18next.t("starterSelectUiHandler:passive") ?? "-"; + let abilityLabel = i18next.t("starterSelectUiHandler:ability") ?? "-"; + if (!exemptedLanguages.includes(currentLanguage)) { + passiveLabel = passiveLabel.charAt(0); + abilityLabel = abilityLabel.charAt(0); + } + const pPassiveInfo = pokemon.passive ? passiveLabel+": "+pokemon.getPassiveAbility().name : ""; + const pAbilityInfo = abilityLabel + ": " + pokemon.getAbility().name; + const pokeInfoText = addBBCodeTextObject(this.scene, 0, 0, pName, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeInfoText.appendText(`${i18next.t("saveSlotSelectUiHandler:lv")}${Utils.formatFancyLargeNumber(pokemon.level, 1)} - ${pNature}`); + pokeInfoText.appendText(pAbilityInfo); + pokeInfoText.appendText(pPassiveInfo); + pokeInfoTextContainer.add(pokeInfoText); + + // Pokemon Stats + // Colored Arrows (Red/Blue) are placed by stats that are boosted from natures + const pokeStatTextContainer = this.scene.add.container(-35, 6); + const pStats : string[]= []; + pokemon.stats.forEach((element) => pStats.push(Utils.formatFancyLargeNumber(element, 1))); + for (let i = 0; i < pStats.length; i++) { + const isMult = getNatureStatMultiplier(pokemon.nature, i); + pStats[i] = (isMult < 1) ? pStats[i] + "[color=#40c8f8]↓[/color]" : pStats[i]; + pStats[i] = (isMult > 1) ? pStats[i] + "[color=#f89890]↑[/color]" : pStats[i]; + } + const hp = i18next.t("pokemonInfo:Stat.HPshortened")+": "+pStats[0]; + const atk = i18next.t("pokemonInfo:Stat.ATKshortened")+": "+pStats[1]; + const def = i18next.t("pokemonInfo:Stat.DEFshortened")+": "+pStats[2]; + const spatk = i18next.t("pokemonInfo:Stat.SPATKshortened")+": "+pStats[3]; + const spdef = i18next.t("pokemonInfo:Stat.SPDEFshortened")+": "+pStats[4]; + const speedLabel = (currentLanguage==="es"||currentLanguage==="pt_BR") ? i18next.t("runHistory:SPDshortened") : i18next.t("pokemonInfo:Stat.SPDshortened"); + const speed = speedLabel+": "+pStats[5]; + // Column 1: HP Atk Def + const pokeStatText1 = addBBCodeTextObject(this.scene, -5, 0, hp, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeStatText1.appendText(atk); + pokeStatText1.appendText(def); + pokeStatTextContainer.add(pokeStatText1); + // Column 2: SpAtk SpDef Speed + const pokeStatText2 = addBBCodeTextObject(this.scene, 25, 0, spatk, TextStyle.SUMMARY, {fontSize: textContainerFontSize, lineSpacing:3}); + pokeStatText2.appendText(spdef); + pokeStatText2.appendText(speed); + pokeStatTextContainer.add(pokeStatText2); + + // Shiny + Fusion Status + const marksContainer = this.scene.add.container(0, 0); + if (pokemon.fusionSpecies) { + const splicedIcon = this.scene.add.image(0, 0, "icon_spliced"); + splicedIcon.setScale(0.35); + splicedIcon.setOrigin(0, 0); + pokemon.isShiny() ? splicedIcon.setPositionRelative(pokeInfoTextContainer, 35, 0) : splicedIcon.setPositionRelative(pokeInfoTextContainer, 28, 0); + marksContainer.add(splicedIcon); + this.getUi().bringToTop(splicedIcon); + } + if (pokemon.isShiny()) { + const doubleShiny = pokemon.isFusion() && pokemon.shiny && pokemon.fusionShiny; + const shinyStar = this.scene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`); + shinyStar.setOrigin(0, 0); + shinyStar.setScale(0.65); + shinyStar.setPositionRelative(pokeInfoTextContainer, 28, 0); + shinyStar.setTint(getVariantTint(!doubleShiny ? pokemon.getVariant() : pokemon.variant)); + marksContainer.add(shinyStar); + this.getUi().bringToTop(shinyStar); + if (doubleShiny) { + const fusionShinyStar = this.scene.add.image(0, 0, "shiny_star_small_2"); + fusionShinyStar.setOrigin(0, 0); + fusionShinyStar.setScale(0.5); + fusionShinyStar.setPosition(shinyStar.x+1, shinyStar.y+1); + fusionShinyStar.setTint(getVariantTint(pokemon.fusionVariant)); + marksContainer.add(fusionShinyStar); + this.getUi().bringToTop(fusionShinyStar); + } + } + + // Pokemon Moveset + // Need to check if dynamically typed moves + const pokemonMoveset = pokemon.getMoveset(); + const movesetContainer = this.scene.add.container(70, -29); + const pokemonMoveBgs : Phaser.GameObjects.NineSlice[] = []; + const pokemonMoveLabels : Phaser.GameObjects.Text[] = []; + const movePos = [[-6.5, 35.5], [37, 35.5], [-6.5, 43.5], [37, 43.5]]; + for (let m = 0; m < pokemonMoveset?.length; m++) { + const moveContainer = this.scene.add.container(movePos[m][0], movePos[m][1]); + moveContainer.setScale(0.5); + const moveBg = this.scene.add.nineslice(0, 0, "type_bgs", "unknown", 85, 15, 2, 2, 2, 2); + moveBg.setOrigin(1, 0); + const moveLabel = addTextObject(this.scene, -moveBg.width / 2, 2, "-", TextStyle.PARTY); + moveLabel.setOrigin(0.5, 0); + moveLabel.setName("text-move-label"); + pokemonMoveBgs.push(moveBg); + pokemonMoveLabels.push(moveLabel); + moveContainer.add(moveBg); + moveContainer.add(moveLabel); + movesetContainer.add(moveContainer); + const move = pokemonMoveset[m]?.getMove(); + pokemonMoveBgs[m].setFrame(Type[move ? move.type : Type.UNKNOWN].toString().toLowerCase()); + pokemonMoveLabels[m].setText(move ? move.name : "-"); + } + + // Pokemon Held Items - not displayed by default + // Endless/Endless Spliced have a different scale because Pokemon tend to accumulate more items in these runs. + const heldItemsScale = (this.runInfo.gameMode === GameModes.SPLICED_ENDLESS || this.runInfo.gameMode === GameModes.ENDLESS) ? 0.25 : 0.5; + const heldItemsContainer = this.scene.add.container(-82, 6); + const heldItemsList : PokemonHeldItemModifier[] = []; + if (this.runInfo.modifiers.length) { + for (const m of this.runInfo.modifiers) { + const modifier = m.toModifier(this.scene, this.modifiersModule[m.className]); + if (modifier instanceof PokemonHeldItemModifier && modifier.pokemonId === pokemon.id) { + modifier.stackCount = m["stackCount"]; + heldItemsList.push(modifier); + } + } + if (heldItemsList.length > 0) { + (heldItemsList as PokemonHeldItemModifier[]).sort(modifierSortFunc); + let row = 0; + for (const [index, item] of heldItemsList.entries()) { + if ( index > 36 ) { + const overflowIcon = addTextObject(this.scene, 182, 4, "+", TextStyle.WINDOW); + heldItemsContainer.add(overflowIcon); + break; + } + const itemIcon = item?.getIcon(this.scene, true); + itemIcon.setScale(heldItemsScale); + itemIcon.setPosition((index%19) * 10, row * 10); + heldItemsContainer.add(itemIcon); + if (index !== 0 && index % 18 === 0) { + row++; + } + } + } + } + heldItemsContainer.setName("heldItems"); + heldItemsContainer.setVisible(false); + + // Labels are applied for future differentiation in showParty() + pokemonInfoContainer.add(pokemonInfoWindow); + iconContainer.add(icon); + pokemonInfoContainer.add(iconContainer); + marksContainer.setName("PkmnMarks"); + pokemonInfoContainer.add(marksContainer); + movesetContainer.setName("PkmnMoves"); + pokemonInfoContainer.add(movesetContainer); + pokeInfoTextContainer.setName("PkmnInfoText"); + pokemonInfoContainer.add(pokeInfoTextContainer); + pokeStatTextContainer.setName("PkmnStatsText"); + pokemonInfoContainer.add(pokeStatTextContainer); + pokemonInfoContainer.add(heldItemsContainer); + pokemonInfoContainer.setName("PkmnInfo"); + this.partyContainer.add(pokemonInfoContainer); + pokemon.destroy(); + }); + this.runContainer.add(this.partyContainer); + } + + /** + * Changes what is displayed of the Pokemon's held items + * @param partyVisible {boolean} + * True -> Shows the Pokemon's default information and hides held items + * False -> Shows the Pokemon's held items and hides default information + */ + private showParty(partyVisible: boolean): void { + const allContainers = this.partyContainer.getAll("name", "PkmnInfo"); + allContainers.forEach((c: Phaser.GameObjects.Container) => { + c.getByName("PkmnMoves").setVisible(partyVisible); + c.getByName("PkmnInfoText").setVisible(partyVisible); + c.getByName("PkmnStatsText").setVisible(partyVisible); + c.getByName("PkmnMarks").setVisible(partyVisible); + c.getByName("heldItems").setVisible(!partyVisible); + this.partyVisibility = partyVisible; + }); + } + + /** + * Shows the ending art. + */ + private createVictorySplash(): void { + this.endCardContainer = this.scene.add.container(0, 0); + const endCard = this.scene.add.image(0, 0, `end_${this.isPGF ? "f" : "m"}`); + endCard.setOrigin(0); + endCard.setScale(0.5); + const text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" }); + text.setOrigin(0.5); + this.endCardContainer.add(endCard); + this.endCardContainer.add(text); + } + + /** createHallofFame() - if the run is victorious, this creates a hall of fame image for the player to view + * Overlay created by Koda (Thank you!) + * This could be adapted into a public-facing method for victory screens. Perhaps. + */ + private createHallofFame(): void { + // Issue Note (08-05-2024): It seems as if fused pokemon do not appear with the averaged color b/c pokemonData's loadAsset requires there to be some active battle? + // As an alternative, the icons of the second/bottom fused Pokemon have been placed next to their fellow fused Pokemon in Hall of Fame + this.hallofFameContainer = this.scene.add.container(0, 0); + // Thank you Hayuna for the code + const endCard = this.scene.add.image(0, 0, `end_${this.isPGF ? "f" : "m"}`); + endCard.setOrigin(0); + endCard.setPosition(-1, -1); + endCard.setScale(0.5); + const endCardCoords = endCard.getBottomCenter(); + const overlayColor = this.isPGF ? "red" : "blue"; + const hallofFameBg = this.scene.add.image(0, 0, "hall_of_fame_"+overlayColor); + hallofFameBg.setPosition(159, 89); + hallofFameBg.setSize(this.scene.game.canvas.width, this.scene.game.canvas.height+10); + hallofFameBg.setAlpha(0.8); + this.hallofFameContainer.add(endCard); + this.hallofFameContainer.add(hallofFameBg); + + const hallofFameText = addTextObject(this.scene, 0, 0, i18next.t("runHistory:hallofFameText"+(this.isPGF ? "F" : "M")), TextStyle.WINDOW); + hallofFameText.setPosition(endCardCoords.x-(hallofFameText.displayWidth/2), 164); + this.hallofFameContainer.add(hallofFameText); + this.runInfo.party.forEach((p, i) => { + const pkmn = p.toPokemon(this.scene); + const row = i % 2; + const id = pkmn.id; + const shiny = pkmn.shiny; + const formIndex = pkmn.formIndex; + const variant = pkmn.variant; + const species = pkmn.getSpeciesForm(); + const pokemonSprite: Phaser.GameObjects.Sprite = this.scene.add.sprite(60 + 40 * i, 40 + row * 80, "pkmn__sub"); + pokemonSprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true }); + this.hallofFameContainer.add(pokemonSprite); + const speciesLoaded: Map = new Map(); + speciesLoaded.set(id, false); + + const female = pkmn.gender === 1; + species.loadAssets(this.scene, female, formIndex, shiny, variant, true).then(() => { + speciesLoaded.set(id, true); + pokemonSprite.play(species.getSpriteKey(female, formIndex, shiny, variant)); + pokemonSprite.setPipelineData("shiny", shiny); + pokemonSprite.setPipelineData("variant", variant); + pokemonSprite.setPipelineData("spriteKey", species.getSpriteKey(female, formIndex, shiny, variant)); + pokemonSprite.setVisible(true); + }); + if (pkmn.isFusion()) { + const fusionIcon = this.scene.add.sprite(80 + 40 * i, 50 + row * 80, pkmn.getFusionIconAtlasKey()); + fusionIcon.setName("sprite-fusion-icon"); + fusionIcon.setOrigin(0.5, 0); + fusionIcon.setFrame(pkmn.getFusionIconId(true)); + this.hallofFameContainer.add(fusionIcon); + } + pkmn.destroy(); + }); + this.hallofFameContainer.setVisible(false); + this.runContainer.add(this.hallofFameContainer); + } + + /** + * Takes input from the user to perform a desired action. + * @param button - Button object to be processed + * Button.CANCEL - removes all containers related to RunInfo and returns the user to Run History + * Button.CYCLE_FORM, Button.CYCLE_SHINY, Button.CYCLE_ABILITY - runs the function buttonCycleOption() + */ + override processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + const error = false; + + switch (button) { + case Button.CANCEL: + success = true; + if (this.pageMode === RunInfoUiMode.MAIN) { + this.runInfoContainer.removeAll(true); + this.runResultContainer.removeAll(true); + this.partyContainer.removeAll(true); + this.runContainer.removeAll(true); + if (this.isVictory) { + this.hallofFameContainer.removeAll(true); + } + super.clear(); + this.runContainer.setVisible(false); + ui.revertMode(); + } else if (this.pageMode === RunInfoUiMode.HALL_OF_FAME) { + this.hallofFameContainer.setVisible(false); + this.pageMode = RunInfoUiMode.MAIN; + } else if (this.pageMode === RunInfoUiMode.ENDING_ART) { + this.endCardContainer.setVisible(false); + this.runContainer.remove(this.endCardContainer); + this.pageMode = RunInfoUiMode.MAIN; + } + break; + case Button.DOWN: + case Button.UP: + break; + case Button.CYCLE_FORM: + case Button.CYCLE_SHINY: + case Button.CYCLE_ABILITY: + this.buttonCycleOption(button); + break; + } + + if (success) { + ui.playSelect(); + } else if (error) { + ui.playError(); + } + return success || error; + } + + /** + * buttonCycleOption : takes a parameter button to execute different actions in the run-info page + * The use of non-directional / A / B buttons is named in relation to functions used during starter-select. + * Button.CYCLE_FORM (F key) --> displays ending art (victory only) + * Button.CYCLE_SHINY (R key) --> displays hall of fame (victory only) + * Button.CYCLE_ABILITY (E key) --> shows pokemon held items + */ + private buttonCycleOption(button: Button) { + switch (button) { + case Button.CYCLE_FORM: + if (this.isVictory) { + if (!this.endCardContainer || !this.endCardContainer.visible) { + this.createVictorySplash(); + this.endCardContainer.setVisible(true); + this.runContainer.add(this.endCardContainer); + this.pageMode = RunInfoUiMode.ENDING_ART; + } else { + this.endCardContainer.setVisible(false); + this.runContainer.remove(this.endCardContainer); + this.pageMode = RunInfoUiMode.MAIN; + } + } + break; + case Button.CYCLE_SHINY: + if (this.isVictory) { + if (!this.hallofFameContainer.visible) { + this.hallofFameContainer.setVisible(true); + this.pageMode = RunInfoUiMode.HALL_OF_FAME; + } else { + this.hallofFameContainer.setVisible(false); + this.pageMode = RunInfoUiMode.MAIN; + } + } + break; + case Button.CYCLE_ABILITY: + if (this.partyVisibility) { + this.showParty(false); + } else { + this.showParty(true); + } + break; + } + } +} + diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 67002e32283..1f4a0b3a51e 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -47,6 +47,8 @@ import SettingsAudioUiHandler from "./settings/settings-audio-ui-handler"; import { PlayerGender } from "#enums/player-gender"; import BgmBar from "#app/ui/bgm-bar"; import RenameFormUiHandler from "./rename-form-ui-handler"; +import RunHistoryUiHandler from "./run-history-ui-handler"; +import RunInfoUiHandler from "./run-info-ui-handler"; export enum Mode { MESSAGE, @@ -85,7 +87,9 @@ export enum Mode { UNAVAILABLE, OUTDATED, CHALLENGE_SELECT, - RENAME_POKEMON + RENAME_POKEMON, + RUN_HISTORY, + RUN_INFO, } const transitionModes = [ @@ -97,7 +101,8 @@ const transitionModes = [ Mode.EGG_HATCH_SCENE, Mode.EGG_LIST, Mode.EGG_GACHA, - Mode.CHALLENGE_SELECT + Mode.CHALLENGE_SELECT, + Mode.RUN_HISTORY, ]; const noTransitionModes = [ @@ -185,6 +190,8 @@ export default class UI extends Phaser.GameObjects.Container { new OutdatedModalUiHandler(scene), new GameChallengesUiHandler(scene), new RenameFormUiHandler(scene), + new RunHistoryUiHandler(scene), + new RunInfoUiHandler(scene), ]; }