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 00000000000..87fadf565fd Binary files /dev/null and b/public/images/ui/hall_of_fame_blue.png differ 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 00000000000..5d4d5e41e9c Binary files /dev/null and b/public/images/ui/hall_of_fame_red.png differ diff --git a/public/images/ui/legacy/hall_of_fame_blue.png b/public/images/ui/legacy/hall_of_fame_blue.png new file mode 100644 index 00000000000..87fadf565fd Binary files /dev/null and b/public/images/ui/legacy/hall_of_fame_blue.png differ 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 00000000000..5d4d5e41e9c Binary files /dev/null and b/public/images/ui/legacy/hall_of_fame_red.png differ diff --git a/src/enums/game-data-type.ts b/src/enums/game-data-type.ts index 179817fe5be..d672253794a 100644 --- a/src/enums/game-data-type.ts +++ b/src/enums/game-data-type.ts @@ -6,5 +6,6 @@ export enum GameDataType { SESSION, SETTINGS, TUTORIALS, - SEEN_DIALOGUES + SEEN_DIALOGUES, + RUN_HISTORY } diff --git a/src/locales/ca_ES/config.ts b/src/locales/ca_ES/config.ts index 36aee87fc75..0a56c89fafb 100644 --- a/src/locales/ca_ES/config.ts +++ b/src/locales/ca_ES/config.ts @@ -54,6 +54,7 @@ import { voucher } from "./voucher"; import { terrain, weather } from "./weather"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const caESConfig = { ability: ability, @@ -114,5 +115,6 @@ export const caESConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/ca_ES/menu-ui-handler.ts b/src/locales/ca_ES/menu-ui-handler.ts index 1014c161f84..287ce056f8d 100644 --- a/src/locales/ca_ES/menu-ui-handler.ts +++ b/src/locales/ca_ES/menu-ui-handler.ts @@ -6,6 +6,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "STATS": "Stats", "VOUCHERS": "Vouchers", "EGG_LIST": "Egg List", + "RUN_HISTORY":"Run History", "EGG_GACHA": "Egg Gacha", "MANAGE_DATA": "Manage Data", "COMMUNITY": "Community", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "Select a slot to import to.", "exportSession": "Export Session", "exportSlotSelect": "Select a slot to export from.", + "importRunHistory":"Import Run History", + "exportRunHistory":"Export Run History", "importData": "Import Data", "exportData": "Export Data", "consentPreferences": "Consent Preferences", diff --git a/src/locales/ca_ES/run-history-ui-handler.ts b/src/locales/ca_ES/run-history-ui-handler.ts new file mode 100644 index 00000000000..304a2afd0b2 --- /dev/null +++ b/src/locales/ca_ES/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victory!", + "defeatedWildM": "Defeated by ", + "defeatedTrainerM": "Defeated by ", + "defeatedTrainerDoubleM": "Defeated by Duo", + "defeatedRivalM": "Defeated by Rival", + "defeatedM":"Defeated", + "defeatedWildF": "Defeated by ", + "defeatedTrainerF": "Defeated by ", + "defeatedTrainerDoubleF": "Defeated by Duo", + "defeatedRivalF": "Defeated by Rival", + "defeatedF":"Defeated", + "luck":"Luck", + "score":"Score", + "mode":"Mode", + "challengeRules":"Rule(s)", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Player Items", + "personalBest":"Personal Best!", + "SPDshortened":"Vel.", + "runInfo":"Run Info", + "money":"Money", + "runLength":"Run Length", + "viewHeldItems":"Held Items", + "hallofFameTextM":"Welcome to the Hall of Fame!", + "hallofFameTextF":"Welcome to the Hall of Fame!", + "viewHallOfFame":"View Hall of Fame!", + "viewEndingSplash":"View ending art!" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/de/config.ts b/src/locales/de/config.ts index 080c9ecc598..32e1bfa8e9c 100644 --- a/src/locales/de/config.ts +++ b/src/locales/de/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const deConfig = { ability: ability, @@ -114,5 +115,6 @@ export const deConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/de/menu-ui-handler.ts b/src/locales/de/menu-ui-handler.ts index 67d70e9a418..c2a720b82e4 100644 --- a/src/locales/de/menu-ui-handler.ts +++ b/src/locales/de/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Spieleinstellungen", "ACHIEVEMENTS": "Erfolge", "STATS": "Statistiken", + "RUN_HISTORY": "Laufhistorie", "VOUCHERS": "Gutscheine", "EGG_LIST": "Eierliste", "EGG_GACHA": "Eier-Gacha", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "Wähle einen Slot zum Importieren.", "exportSession": "Sitzung exportieren", "exportSlotSelect": "Wähle einen Slot zum Exportieren.", + "importRunHistory": "Laufhistorie importieren", + "exportRunHistory": "Laufhistorie exportieren", "importData": "Daten importieren", "exportData": "Daten exportieren", "consentPreferences": "Einwilligungspräferenzen", diff --git a/src/locales/de/run-history-ui-handler.ts b/src/locales/de/run-history-ui-handler.ts new file mode 100644 index 00000000000..7c9e037ac75 --- /dev/null +++ b/src/locales/de/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Sieg!", + "defeatedWildM": "Besiegt durch ", + "defeatedTrainerM": "Besiegt durch ", + "defeatedTrainerDoubleM": "Besiegt durch Doppelkampf", + "defeatedRivalM": "Besiegt durch Rivalin", + "defeatedM":"Besiegt", + "defeatedWildF": "Besiegt durch ", + "defeatedTrainerF": "Besiegt durch ", + "defeatedTrainerDoubleF": "Besiegt durch Doppelkampf", + "defeatedRivalF": "Besiegt durch Rivale", + "defeatedF":"Besiegt", + "luck":"Glück", + "score":"Punkte", + "mode":"Modus", + "challengeRules":"Regeln", + "challengeMonoGen1":"Gen I", + "challengeMonoGen2":"Gen II", + "challengeMonoGen3":"Gen III", + "challengeMonoGen4":"Gen IV", + "challengeMonoGen5":"Gen V", + "challengeMonoGen6":"Gen VI", + "challengeMonoGen7":"Gen VII", + "challengeMonoGen8":"Gen VIII", + "challengeMonoGen9":"Gen IX", + "playerItems":"Spielergegenstände", + "personalBest":"Persönlicher Bestwert!", + "SPDshortened":"Geschw.", + "runInfo":"Durchlauf Informationen", + "money":"Geld", + "runLength":"Durchlauf Dauer", + "viewHeldItems":"Getragene Items", + "hallofFameTextM":"Willkommen in der Ruhmeshalle", + "hallofFameTextF":"Willkommen in der Ruhmeshalle", + "viewHallOfFame":"Ruhmeshalle ansehen!", + "viewEndingSplash":"Endgrafik anzeigen!" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/en/config.ts b/src/locales/en/config.ts index 8061b89c9c3..f1827b5152d 100644 --- a/src/locales/en/config.ts +++ b/src/locales/en/config.ts @@ -57,6 +57,7 @@ import weather from "./weather.json"; import terrain from "./terrain.json"; import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import moveTriggers from "./move-trigger.json"; +import runHistory from "./run-history.json"; export const enConfig = { ability, @@ -118,4 +119,5 @@ export const enConfig = { partyUiHandler, modifierSelectUiHandler, moveTriggers, + runHistory }; diff --git a/src/locales/en/menu-ui-handler.json b/src/locales/en/menu-ui-handler.json index a1ca4a5619a..00bf89f127a 100644 --- a/src/locales/en/menu-ui-handler.json +++ b/src/locales/en/menu-ui-handler.json @@ -2,6 +2,7 @@ "GAME_SETTINGS": "Game Settings", "ACHIEVEMENTS": "Achievements", "STATS": "Stats", + "RUN_HISTORY": "Run History", "VOUCHERS": "Vouchers", "EGG_LIST": "Egg List", "EGG_GACHA": "Egg Gacha", @@ -14,6 +15,8 @@ "importSlotSelect": "Select a slot to import to.", "exportSession": "Export Session", "exportSlotSelect": "Select a slot to export from.", + "importRunHistory":"Import Run History", + "exportRunHistory":"Export Run History", "importData": "Import Data", "exportData": "Export Data", "consentPreferences": "Consent Preferences", diff --git a/src/locales/en/run-history.json b/src/locales/en/run-history.json new file mode 100644 index 00000000000..0099a4a3ff4 --- /dev/null +++ b/src/locales/en/run-history.json @@ -0,0 +1,37 @@ +{ + "victory": "Victory!", + "defeatedWildM": "Defeated by ", + "defeatedTrainerM": "Defeated by ", + "defeatedTrainerDoubleM": "Defeated by Duo", + "defeatedRivalM": "Defeated by Rival", + "defeatedM": "Defeated", + "defeatedWildF": "Defeated by ", + "defeatedTrainerF": "Defeated by ", + "defeatedTrainerDoubleF": "Defeated by Duo", + "defeatedRivalF": "Defeated by Rival", + "defeatedF": "Defeated", + "luck": "Luck", + "score": "Score", + "mode": "Mode", + "challengeRules": "Rule(s)", + "challengeMonoGen1": "Gen I", + "challengeMonoGen2": "Gen II", + "challengeMonoGen3": "Gen III", + "challengeMonoGen4": "Gen IV", + "challengeMonoGen5": "Gen V", + "challengeMonoGen6": "Gen VI", + "challengeMonoGen7": "Gen VII", + "challengeMonoGen8": "Gen VIII", + "challengeMonoGen9": "Gen IX", + "playerItems": "Player Items", + "personalBest": "Personal Best!", + "SPDshortened": "Vel.", + "runInfo": "Run Info", + "money": "Money", + "runLength": "Run Length", + "viewHeldItems": "Held Items", + "hallofFameTextM": "Welcome to the Hall of Fame!", + "hallofFameTextF": "Welcome to the Hall of Fame!", + "viewHallOfFame": "View Hall of Fame!", + "viewEndingSplash": "View ending art!" +} diff --git a/src/locales/es/config.ts b/src/locales/es/config.ts index 8e9d8c7440f..a34a05cecf0 100644 --- a/src/locales/es/config.ts +++ b/src/locales/es/config.ts @@ -57,6 +57,7 @@ import weather from "./weather.json"; import terrain from "./terrain.json"; import modifierSelectUiHandler from "./modifier-select-ui-handler.json"; import moveTriggers from "./move-trigger.json"; +import runHistory from "./run-history.json"; export const esConfig = { ability, @@ -118,4 +119,5 @@ export const esConfig = { partyUiHandler, modifierSelectUiHandler, moveTriggers, + runHistory }; diff --git a/src/locales/es/menu-ui-handler.json b/src/locales/es/menu-ui-handler.json index deb6ed2ccc4..0b18ba5d343 100644 --- a/src/locales/es/menu-ui-handler.json +++ b/src/locales/es/menu-ui-handler.json @@ -2,6 +2,7 @@ "GAME_SETTINGS": "Ajustes", "ACHIEVEMENTS": "Logros", "STATS": "Estadísticas", + "RUN_HISTORY": "Historial de partida", "VOUCHERS": "Vales", "EGG_LIST": "Lista de Huevos", "EGG_GACHA": "Gacha de Huevos", @@ -14,6 +15,8 @@ "importSlotSelect": "Selecciona una ranura para importar.", "exportSession": "Exportar Sesión", "exportSlotSelect": "Selecciona una ranura para exportar.", + "importRunHistory":"Importar Historial de partida", + "exportRunHistory":"Exportar Historial de partida", "importData": "Importar Datos", "exportData": "Exportar Datos", "consentPreferences": "Consentimiento de datos", diff --git a/src/locales/es/run-history.json b/src/locales/es/run-history.json new file mode 100644 index 00000000000..d5c52e10fd9 --- /dev/null +++ b/src/locales/es/run-history.json @@ -0,0 +1,37 @@ +{ + "victory": "¡Victoria!", + "defeatedWildM": "Derrotado por ", + "defeatedTrainerM": "Derrotado por ", + "defeatedTrainerDoubleM": "Derrotado por un dúo", + "defeatedRivalM": "Derrotado por el rival", + "defeatedM": "Derrotado", + "defeatedWildF": "Derrotada por ", + "defeatedTrainerF": "Derrotada por ", + "defeatedTrainerDoubleF": "Derrotada por un dúo", + "defeatedRivalF": "Derrotada por el rival", + "defeatedF": "Derrotada", + "luck": "Suerte", + "score": "Puntuación", + "mode": "Modo", + "challengeRules": "Regla(s)", + "challengeMonoGen1": "Gen I", + "challengeMonoGen2": "Gen II", + "challengeMonoGen3": "Gen III", + "challengeMonoGen4": "Gen IV", + "challengeMonoGen5": "Gen V", + "challengeMonoGen6": "Gen VI", + "challengeMonoGen7": "Gen VII", + "challengeMonoGen8": "Gen VIII", + "challengeMonoGen9": "Gen IX", + "playerItems": "Objetos del jugador", + "personalBest": "¡Récord personal!", + "SPDshortened": "Vel.", + "runInfo": "Info. de partida", + "money": "Dinero", + "runLength": "Duración de partida", + "viewHeldItems": "Objetos equipados", + "hallofFameTextM": "¡Bienvenido al Hall de la Fama!", + "hallofFameTextF": "¡Bienvenida al Hall de la Fama!", + "viewHallOfFame": "¡Ver Hall de la Fama!", + "viewEndingSplash": "¡Ver la imagen final!" +} \ No newline at end of file diff --git a/src/locales/fr/config.ts b/src/locales/fr/config.ts index a2ab67eefe0..206103ce971 100644 --- a/src/locales/fr/config.ts +++ b/src/locales/fr/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const frConfig = { ability: ability, @@ -114,5 +115,6 @@ export const frConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/fr/menu-ui-handler.ts b/src/locales/fr/menu-ui-handler.ts index 823001f9e94..d2446bc8d0d 100644 --- a/src/locales/fr/menu-ui-handler.ts +++ b/src/locales/fr/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Paramètres", "ACHIEVEMENTS": "Succès", "STATS": "Statistiques", + "RUN_HISTORY": "Historique", "VOUCHERS": "Coupons", "EGG_LIST": "Liste des Œufs", "EGG_GACHA": "Gacha-Œufs", @@ -18,6 +19,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "exportSlotSelect": "Sélectionnez l’emplacement depuis lequel exporter les données.", "importData": "Importer données", "exportData": "Exporter données", + "importRunHistory":"Importer historique", + "exportRunHistory":"Exporter historique", "consentPreferences": "Gérer les cookies", "linkDiscord": "Lier à Discord", "unlinkDiscord": "Délier Discord", diff --git a/src/locales/fr/run-history-ui-handler.ts b/src/locales/fr/run-history-ui-handler.ts new file mode 100644 index 00000000000..61f2992a542 --- /dev/null +++ b/src/locales/fr/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Victoire !", + "defeatedWildM": "Battu par ", + "defeatedTrainerM": "Battu par ", + "defeatedTrainerDoubleM": "Battu par Duo", + "defeatedRivalM": "Battu par Rivale", + "defeatedM":"Vaincu", + "defeatedWildF": "Battue par ", + "defeatedTrainerF": "Battue par ", + "defeatedTrainerDoubleF": "Battue par Duo", + "defeatedRivalF": "Battue par Rival", + "defeatedF":"Vaincue", + "luck":"Chance ", + "score":"Score", + "mode":"Mode ", + "challengeRules":"Règles ", + "challengeMonoGen1":"1G", + "challengeMonoGen2":"2G", + "challengeMonoGen3":"3G", + "challengeMonoGen4":"4G", + "challengeMonoGen5":"5G", + "challengeMonoGen6":"6G", + "challengeMonoGen7":"7G", + "challengeMonoGen8":"8G", + "challengeMonoGen9":"9G", + "playerItems":"Objets Dresseur", + "personalBest":"Record personnel !", + "SPDshortened":"Vit.", + "runInfo":"Infos session", + "money":"Argent", + "runLength":"Durée session ", + "viewHeldItems":"Objets tenus", + "hallofFameTextM":"Bienvenue au Panthéon !", + "hallofFameTextF":"Bienvenue au Panthéon !", + "viewHallOfFame":"Voir le Panthéon", + "viewEndingSplash":"Voir l’illustration\nde fin" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/it/config.ts b/src/locales/it/config.ts index d6265061a9f..d6e0422856a 100644 --- a/src/locales/it/config.ts +++ b/src/locales/it/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const itConfig = { ability: ability, @@ -114,5 +115,6 @@ export const itConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/it/menu-ui-handler.ts b/src/locales/it/menu-ui-handler.ts index 3454de24f87..5c47ad5646b 100644 --- a/src/locales/it/menu-ui-handler.ts +++ b/src/locales/it/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Impostazioni", "ACHIEVEMENTS": "Obiettivi", "STATS": "Statistiche", + "RUN_HISTORY": "Run precedenti", "VOUCHERS": "Biglietti", "EGG_LIST": "Lista uova", "EGG_GACHA": "Macchine uova", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "Seleziona uno slot in cui importare.", "exportSession": "Esporta sessione", "exportSlotSelect": "Seleziona uno slot da cui esportare.", + "importRunHistory":"Importa run precedenti", + "exportRunHistory":"Esporta run precedenti", "importData": "Importa dati", "exportData": "Esporta dati", "consentPreferences": "Consenti preferenze", diff --git a/src/locales/it/run-history-ui-handler.ts b/src/locales/it/run-history-ui-handler.ts new file mode 100644 index 00000000000..2c2718c061a --- /dev/null +++ b/src/locales/it/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Vittoria!", + "defeatedWildM": "Sconfitto da ", + "defeatedTrainerM": "Sconfitto da ", + "defeatedTrainerDoubleM": "Sconfitto dalla coppia ", + "defeatedRivalM": "Sconfitto dalla rivale", + "defeatedM":"Sconfitto", + "defeatedWildF": "Sconfitta da ", + "defeatedTrainerF": "Sconfitta da ", + "defeatedTrainerDoubleF": "Sconfitta dalla coppia ", + "defeatedRivalF": "Sconfitta dal rivale", + "defeatedF":"Sconfitta", + "luck":"Fortuna", + "score":"Punteggio", + "mode":"Modalità", + "challengeRules":"Regola/e", + "challengeMonoGen1":"1ª gen", + "challengeMonoGen2":"2ª gen", + "challengeMonoGen3":"3ª gen", + "challengeMonoGen4":"4ª gen", + "challengeMonoGen5":"5ª gen", + "challengeMonoGen6":"6ª gen", + "challengeMonoGen7":"7ª gen", + "challengeMonoGen8":"8ª gen", + "challengeMonoGen9":"9ª gen", + "playerItems":"Oggetti giocatore", + "personalBest":"Record personale!", + "SPDshortened":"Vel.", + "runInfo":"Info Run", + "money":"Patrimonio", + "runLength":"Durata Run", + "viewHeldItems":"Oggetti equip.", + "hallofFameTextM":"Benvenuto alla Sala d'Onore!", + "hallofFameTextF":"Benvenuto alla Sala d'Onore!", + "viewHallOfFame":"Vai alla Sala d'Onore!", + "viewEndingSplash":"Vai all'arte finale!" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/ja/config.ts b/src/locales/ja/config.ts index 61be36c08df..e4cc79972d6 100644 --- a/src/locales/ja/config.ts +++ b/src/locales/ja/config.ts @@ -55,6 +55,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const jaConfig = { ability: ability, @@ -115,5 +116,6 @@ export const jaConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/ja/menu-ui-handler.ts b/src/locales/ja/menu-ui-handler.ts index 5923052cb62..7a166d7bd81 100644 --- a/src/locales/ja/menu-ui-handler.ts +++ b/src/locales/ja/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "せってい", "ACHIEVEMENTS": "じっせき", "STATS": "とうけい", + "RUN_HISTORY":"ラン履歴", "VOUCHERS": "クーポン", "EGG_LIST": "タマゴリスト", "EGG_GACHA": "タマゴガチャ", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "Select a slot to import to.", "exportSession": "セッションのエクスポート", "exportSlotSelect": "Select a slot to export from.", + "importRunHistory":"ラン履歴インポート", + "exportRunHistory":"ラン履歴エクスポート", "importData": "データのインポート", "exportData": "データのエクスポート", "consentPreferences": "Consent Preferences", diff --git a/src/locales/ja/run-history-ui-handler.ts b/src/locales/ja/run-history-ui-handler.ts new file mode 100644 index 00000000000..f5331ccae91 --- /dev/null +++ b/src/locales/ja/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "勝利!", + "defeatedWild": "倒された相手:", + "defeatedTrainer": "倒された相手:", + "defeatedTrainerDouble": "倒された相手:", + "defeatedRival": "倒された相手:", + "defeated":"敗北", + "defeatedWildF": "倒された相手:", + "defeatedTrainerF": "倒された相手:", + "defeatedTrainerDoubleF": "倒された相手:", + "defeatedRivalF": "倒された相手:", + "defeatedF":"敗北", + "luck":"運", + "score":"スコア", + "mode":"モード", + "challengeRules":"チャレンジ", + "challengeMonoGen1":"I世代", + "challengeMonoGen2":"II世代", + "challengeMonoGen3":"III世代", + "challengeMonoGen4":"IV世代", + "challengeMonoGen5":"V世代", + "challengeMonoGen6":"VI世代", + "challengeMonoGen7":"VII世代", + "challengeMonoGen8":"VIII世代", + "challengeMonoGen9":"IX世代", + "playerItems":"プレイヤーアイテム", + "personalBest":"自己ベスト!", + "SPDshortened":"速さ", // + "runInfo":"ラン情報", + "money":"お金", + "runLength":"ラン最高ウェーブ", + "viewHeldItems":"手持ちアイテム", + "hallofFameTextM":"殿堂へようこそ!", + "hallofFameTextF":"殿堂へようこそ!", + "viewHallOfFame":"殿堂登録を見る!", + "viewEndingSplash":"クリア後のアートを見る!" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/ko/config.ts b/src/locales/ko/config.ts index 2bc60f04bef..44db41e47b5 100644 --- a/src/locales/ko/config.ts +++ b/src/locales/ko/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const koConfig = { ability: ability, @@ -114,5 +115,6 @@ export const koConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/ko/menu-ui-handler.ts b/src/locales/ko/menu-ui-handler.ts index f6d8244f6e3..872c715b0a4 100644 --- a/src/locales/ko/menu-ui-handler.ts +++ b/src/locales/ko/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "게임 설정", "ACHIEVEMENTS": "업적", "STATS": "통계", + "RUN_HISTORY": "플레이 이력", "VOUCHERS": "바우처", "EGG_LIST": "알 목록", "EGG_GACHA": "알 뽑기", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "불러올 슬롯을 골라주세요.", "exportSession": "세션 내보내기", "exportSlotSelect": "내보낼 슬롯을 골라주세요.", + "importRunHistory":"플레이 이력 불러오기", + "exportRunHistory":"플레이 이력 내보내기", "importData": "데이터 불러오기", "exportData": "데이터 내보내기", "consentPreferences": "쿠키 설정 동의", diff --git a/src/locales/ko/run-history-ui-handler.ts b/src/locales/ko/run-history-ui-handler.ts new file mode 100644 index 00000000000..b5490d0a118 --- /dev/null +++ b/src/locales/ko/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "승리!", + "defeatedWild": "야생에서 패배: ", + "defeatedTrainer": "트레이너에게 패배: ", + "defeatedTrainerDouble": "더블 배틀에서 패배", + "defeatedRival": "라이벌에게 패배", + "defeatedM":"패배", + "defeatedWildF": "야생에서 패배: ", + "defeatedTrainerF": "트레이너에게 패배: ", + "defeatedTrainerDoubleF": "더블 배틀에서 패배", + "defeatedRivalF": "라이벌에게 패배", + "defeatedF":"패배", + "luck":"행운", + "score":"점수", + "mode":"모드", + "challengeRules":"규칙", + "challengeMonoGen1":"1세대", + "challengeMonoGen2":"2세대", + "challengeMonoGen3":"3세대", + "challengeMonoGen4":"4세대", + "challengeMonoGen5":"5세대", + "challengeMonoGen6":"6세대", + "challengeMonoGen7":"7세대", + "challengeMonoGen8":"8세대", + "challengeMonoGen9":"9세대", + "playerItems":"플레이어 아이템", + "personalBest":"개인 최고기록!", + "SPDshortened":"스피드", + "runInfo":"플레이 정보", + "money":"소지금", + "runLength":"플레이 타임", + "viewHeldItems":"도구", + "hallofFameTextM":"전당 등록을 축하합니다!", + "hallofFameTextF":"전당 등록을 축하합니다!", + "viewHallOfFame":"전당 보기", + "viewEndingSplash":"엔딩 화면 보기" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/pt_BR/config.ts b/src/locales/pt_BR/config.ts index 9ebb4867ae4..2ad0bf3f2bc 100644 --- a/src/locales/pt_BR/config.ts +++ b/src/locales/pt_BR/config.ts @@ -54,6 +54,7 @@ import { titles, trainerClasses, trainerNames } from "./trainers"; import { tutorial } from "./tutorial"; import { voucher } from "./voucher"; import { terrain, weather } from "./weather"; +import { runHistory } from "./run-history-ui-handler"; export const ptBrConfig = { ability: ability, @@ -114,5 +115,6 @@ export const ptBrConfig = { trainerNames: trainerNames, tutorial: tutorial, voucher: voucher, - weather: weather + weather: weather, + runHistory: runHistory, }; diff --git a/src/locales/pt_BR/menu-ui-handler.ts b/src/locales/pt_BR/menu-ui-handler.ts index 431bb64310e..f6aa993ccda 100644 --- a/src/locales/pt_BR/menu-ui-handler.ts +++ b/src/locales/pt_BR/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "Configurações", "ACHIEVEMENTS": "Conquistas", "STATS": "Estatísticas", + "RUN_HISTORY": "Histórico de Jogos", "VOUCHERS": "Vouchers", "EGG_LIST": "Incubadora", "EGG_GACHA": "Gacha de ovos", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "Selecione um slot para importar.", "exportSession": "Exportar sessão", "exportSlotSelect": "Selecione um slot para exportar.", + "importRunHistory":"Importar Histórico de Jogos", + "exportRunHistory":"Exportar Histórico de Jogos", "importData": "Importar dados", "exportData": "Exportar dados", "consentPreferences": "Opções de Privacidade", diff --git a/src/locales/pt_BR/run-history-ui-handler.ts b/src/locales/pt_BR/run-history-ui-handler.ts new file mode 100644 index 00000000000..65a3844b0e8 --- /dev/null +++ b/src/locales/pt_BR/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "Vitória!", + "defeatedWildM": "Derrotado por ", + "defeatedTrainerM": "Derrotado por ", + "defeatedTrainerDoubleM": "Derrotado por Dupla", + "defeatedRivalM": "Derrotado por Rival", + "defeatedM": "Derrotado", + "defeatedWildF": "Derrotada por ", + "defeatedTrainerF": "Derrotada por ", + "defeatedTrainerDoubleF": "Derrotada por Dupla", + "defeatedRivalF": "Derrotada por Rival", + "defeatedF": "Derrotada", + "luck": "Sorte", + "score": "Pontuação", + "mode": "Modo", + "challengeRules": "Regra(s)", + "challengeMonoGen1": "Ger. 1", + "challengeMonoGen2": "Ger. 2", + "challengeMonoGen3": "Ger. 3", + "challengeMonoGen4": "Ger. 4", + "challengeMonoGen5": "Ger. 5", + "challengeMonoGen6": "Ger. 6", + "challengeMonoGen7": "Ger. 7", + "challengeMonoGen8": "Ger. 8", + "challengeMonoGen9": "Ger. 9", + "playerItems": "Itens do Jogador", + "personalBest": "Recorde Pessoal!", + "SPDshortened": "Vel.", + "runInfo": "Info. do Jogo", + "money": "Dinheiro", + "runLength": "Duração do Jogo", + "viewHeldItems": "Itens Segurados", + "hallofFameTextM": "Bem-vindo ao Hall da Fama!", + "hallofFameTextF": "Bem-vinda ao Hall da Fama!", + "viewHallOfFame": "Veja o Hall da Fama!", + "viewEndingSplash":"Veja a arte final!" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/zh_CN/config.ts b/src/locales/zh_CN/config.ts index 99b4e56ffc2..4a289b33dd9 100644 --- a/src/locales/zh_CN/config.ts +++ b/src/locales/zh_CN/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const zhCnConfig = { ability: ability, @@ -114,5 +115,6 @@ export const zhCnConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/zh_CN/menu-ui-handler.ts b/src/locales/zh_CN/menu-ui-handler.ts index 467099ddbed..82321291588 100644 --- a/src/locales/zh_CN/menu-ui-handler.ts +++ b/src/locales/zh_CN/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "游戏设置", "ACHIEVEMENTS": "成就", "STATS": "数据统计", + "RUN_HISTORY": "历史记录", "VOUCHERS": "兑换券", "EGG_LIST": "蛋列表", "EGG_GACHA": "扭蛋机", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "选择要导入到的存档位。", "exportSession": "导出存档", "exportSlotSelect": "选择要导出的存档位。", + "importRunHistory":"导入历史记录", + "exportRunHistory":"导出历史记录", "importData": "导入数据", "exportData": "导出数据", "consentPreferences": "同意偏好", diff --git a/src/locales/zh_CN/run-history-ui-handler.ts b/src/locales/zh_CN/run-history-ui-handler.ts new file mode 100644 index 00000000000..fa4a2ce5fcf --- /dev/null +++ b/src/locales/zh_CN/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "胜利!", + "defeatedWild": "被打败", + "defeatedTrainer": "被打败", + "defeatedTrainerDouble": "被组合打败", + "defeatedRival": "被劲敌打败", + "defeatedM":"被打败", + "defeatedWildF": "被打败", + "defeatedTrainerF": "被打败", + "defeatedTrainerDoubleF": "被组合打败", + "defeatedRivalF": "被劲敌打败", + "defeatedF":"被打败", + "luck":"幸运", + "score":"分数", + "mode":"模式", + "challengeRules":"规则", + "challengeMonoGen1":"一代", + "challengeMonoGen2":"二代", + "challengeMonoGen3":"三代", + "challengeMonoGen4":"四代", + "challengeMonoGen5":"五代", + "challengeMonoGen6":"六代", + "challengeMonoGen7":"七代", + "challengeMonoGen8":"八代", + "challengeMonoGen9":"九代", + "playerItems":"玩家道具", + "personalBest":"个人最佳!", + "SPDshortened":"速率", + "runInfo":"游戏记录", + "money":"金钱", + "runLength":"游戏时长", + "viewHeldItems":"持有道具", + "hallofFameTextM":"欢迎来到名人堂!", + "hallofFameTextF":"欢迎来到名人堂!", + "viewHallOfFame":"浏览名人堂!", + "viewEndingSplash":"浏览结算画面" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/locales/zh_TW/config.ts b/src/locales/zh_TW/config.ts index 269ea3003b9..5fc761d856c 100644 --- a/src/locales/zh_TW/config.ts +++ b/src/locales/zh_TW/config.ts @@ -54,6 +54,7 @@ import { settings } from "./settings.js"; import { common } from "./common.js"; import { modifierSelectUiHandler } from "./modifier-select-ui-handler"; import { moveTriggers } from "./move-trigger"; +import { runHistory } from "./run-history-ui-handler"; export const zhTwConfig = { ability: ability, @@ -114,5 +115,6 @@ export const zhTwConfig = { weather: weather, partyUiHandler: partyUiHandler, modifierSelectUiHandler: modifierSelectUiHandler, - moveTriggers: moveTriggers + moveTriggers: moveTriggers, + runHistory: runHistory, }; diff --git a/src/locales/zh_TW/menu-ui-handler.ts b/src/locales/zh_TW/menu-ui-handler.ts index ab70fd9af33..c7c6934d878 100644 --- a/src/locales/zh_TW/menu-ui-handler.ts +++ b/src/locales/zh_TW/menu-ui-handler.ts @@ -4,6 +4,7 @@ export const menuUiHandler: SimpleTranslationEntries = { "GAME_SETTINGS": "遊戲設置", "ACHIEVEMENTS": "成就", "STATS": "數據", + "RUN_HISTORY": "歷史記錄", "VOUCHERS": "兌換劵", "EGG_LIST": "蛋列表", "EGG_GACHA": "扭蛋機", @@ -16,6 +17,8 @@ export const menuUiHandler: SimpleTranslationEntries = { "importSlotSelect": "選擇要導入到的存檔位。", "exportSession": "導出存檔", "exportSlotSelect": "選擇要導出的存檔位。", + "importRunHistory":"導入歷史記錄", + "exportRunHistory":"導出歷史記錄", "importData": "導入數據", "exportData": "導出數據", "consentPreferences": "同意偏好", diff --git a/src/locales/zh_TW/run-history-ui-handler.ts b/src/locales/zh_TW/run-history-ui-handler.ts new file mode 100644 index 00000000000..15ca0cda875 --- /dev/null +++ b/src/locales/zh_TW/run-history-ui-handler.ts @@ -0,0 +1,42 @@ +import { SimpleTranslationEntries } from "#app/interfaces/locales"; + +export const runHistory: SimpleTranslationEntries = { + "victory": "勝利!", + "defeatedWildM": "被打敗", + "defeatedTrainerM": "被打敗", + "defeatedTrainerDoubleM": "被組合打敗", + "defeatedRivalM": "被勁敵打敗", + "defeatedM":"被打敗", + "defeatedWildF": "被打敗", + "defeatedTrainerF": "被打敗", + "defeatedTrainerDoubleF": "被組合打敗", + "defeatedRivalF": "被勁敵打敗", + "defeatedF":"被打敗", + "luck":"幸運", + "score":"分數", + "mode":"模式", + "challengeRules":"規則", + "challengeMonoGen1":"一代", + "challengeMonoGen2":"二代", + "challengeMonoGen3":"三代", + "challengeMonoGen4":"四代", + "challengeMonoGen5":"五代", + "challengeMonoGen6":"六代", + "challengeMonoGen7":"七代", + "challengeMonoGen8":"八代", + "challengeMonoGen9":"九代", + "playerItems":"玩家道具", + "personalBest":"個人最佳!", + "SPDshortened":"速率", + "runInfo":"遊戲記錄", + "money":"金錢", + "runLength":"遊戲時長", + "viewHeldItems":"持有道具", + "hallofFameTextM":"歡迎來到名人堂!", + "hallofFameTextF":"歡迎來到名人堂!", + "viewHallOfFame":"浏覽名人堂!", + "viewEndingSplash":"浏覽結算畫面" +} as const; + +// Mode Information found in game-mode.ts +// Wave / Lv found in save-slot-select-ui-handler.ts diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 4beed489f29..a42e8472952 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -98,6 +98,13 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } + this.scene.gameData.getSession(this.scene.sessionSlotId).then(sessionData => { + 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), ]; }