diff --git a/src/battle-phases.ts b/src/battle-phases.ts index 122867f68..646fddd7b 100644 --- a/src/battle-phases.ts +++ b/src/battle-phases.ts @@ -39,7 +39,6 @@ import { vouchers } from "./system/voucher"; import { loggedInUser, updateUserInfo } from "./account"; import { GameDataType } from "./system/game-data"; import { addPokeballCaptureStars, addPokeballOpenParticles } from "./anims"; -import { getPokemonSpecies } from "./data/pokemon-species"; import { SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangeMoveUsedTrigger } from "./data/pokemon-forms"; export class LoginPhase extends BattlePhase { @@ -244,6 +243,10 @@ export class SelectStarterPhase extends BattlePhase { this.scene.ui.setMode(Mode.MESSAGE).then(() => { SoundFade.fadeOut(this.scene, this.scene.sound.get('menu'), 500, true); this.scene.time.delayedCall(500, () => this.scene.playBgm()); + if (this.scene.gameMode === GameMode.CLASSIC) + this.scene.gameData.gameStats.classicSessionsPlayed++; + else + this.scene.gameData.gameStats.endlessSessionsPlayed++; this.scene.newBattle(); this.end(); }); @@ -1550,6 +1553,12 @@ export class BattleEndPhase extends BattlePhase { start() { super.start(); + this.scene.gameData.gameStats.battles++; + if (this.scene.currentBattle.trainer) + this.scene.gameData.gameStats.trainersDefeated++; + if (this.scene.gameMode === GameMode.ENDLESS && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) + this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1; + for (let pokemon of this.scene.getField()) { if (pokemon) pokemon.resetBattleSummonData(); @@ -2418,6 +2427,8 @@ export class VictoryPhase extends PokemonPhase { start() { super.start(); + this.scene.gameData.gameStats.pokemonDefeated++; + const participantIds = this.scene.currentBattle.playerParticipantIds; const party = this.scene.getParty(); const expShareModifier = this.scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; @@ -2627,8 +2638,11 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.clearSession().then(() => { this.scene.time.delayedCall(1000, () => { - if (this.victory) + if (this.victory) { this.scene.validateAchv(achvs.CLASSIC_VICTORY); + this.scene.gameData.gameStats.sessionsWon++; + } + this.scene.gameData.saveSystem(); const fadeDuration = this.victory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); this.scene.ui.fadeOut(fadeDuration).then(() => { @@ -2801,6 +2815,9 @@ export class LevelUpPhase extends PlayerPartyMemberPokemonPhase { start() { super.start(); + if (this.level > this.scene.gameData.gameStats.highestLevel) + this.scene.gameData.gameStats.highestLevel = this.level; + this.scene.validateAchvs(LevelAchv, new Utils.IntegerHolder(this.level)); const pokemon = this.getPokemon(); diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 7f8bca88c..5e1d65621 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -402,6 +402,15 @@ export default class BattleScene extends Phaser.Scene { this.gameData = new GameData(this); + this.time.addEvent({ + delay: Utils.fixedInt(1000), + repeat: -1, + callback: () => { + if (this.gameData) + this.gameData.gameStats.playTime++; + } + }) + this.setupControls(); this.load.setBaseURL(); diff --git a/src/pokemon.ts b/src/pokemon.ts index cfa0efdb4..d7293a069 100644 --- a/src/pokemon.ts +++ b/src/pokemon.ts @@ -1929,6 +1929,7 @@ export class PlayerPokemon extends Pokemon { this.generateCompatibleTms(); this.scene.gameData.setPokemonSeen(this, false); this.scene.gameData.setPokemonCaught(this, false); + this.scene.gameData.gameStats.pokemonFused++; this.loadAssets().then(() => { this.calculateStats(); this.scene.updateModifiers(true, true); diff --git a/src/system/game-data.ts b/src/system/game-data.ts index d778be04f..f12c1d57e 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -21,6 +21,7 @@ import { AES, enc } from "crypto-js"; import { Mode } from "../ui/ui"; import { loggedInUser, updateUserInfo } from "../account"; import { Nature } from "../data/nature"; +import { GameStats } from "./game-stats"; const saveKey = 'x0i2O7WRiANTqPmZ'; // Temporary; secure encryption is not yet necessary @@ -45,6 +46,7 @@ interface SystemSaveData { trainerId: integer; secretId: integer; dexData: DexData; + gameStats: GameStats; unlocks: Unlocks; achvUnlocks: AchvUnlocks; voucherUnlocks: VoucherUnlocks; @@ -138,6 +140,8 @@ export class GameData { public dexData: DexData; private defaultDexData: DexData; + public gameStats: GameStats; + public unlocks: Unlocks; public achvUnlocks: AchvUnlocks; @@ -151,6 +155,7 @@ export class GameData { this.loadSettings(); this.trainerId = Utils.randSeedInt(65536); this.secretId = Utils.randSeedInt(65536); + this.gameStats = new GameStats(); this.unlocks = { [Unlockables.ENDLESS_MODE]: false, [Unlockables.MINI_BLACK_HOLE]: false, @@ -174,13 +179,14 @@ export class GameData { if (this.scene.quickStart) return resolve(true); - updateUserInfo().then(success => { + updateUserInfo().then((success: boolean) => { if (!success) return resolve(false); const data: SystemSaveData = { trainerId: this.trainerId, secretId: this.secretId, dexData: this.dexData, + gameStats: this.gameStats, unlocks: this.unlocks, achvUnlocks: this.achvUnlocks, voucherUnlocks: this.voucherUnlocks, @@ -233,6 +239,9 @@ export class GameData { this.trainerId = systemData.trainerId; this.secretId = systemData.secretId; + if (systemData.gameStats) + this.gameStats = systemData.gameStats; + if (systemData.unlocks) { for (let key of Object.keys(systemData.unlocks)) { if (this.unlocks.hasOwnProperty(key)) @@ -290,7 +299,9 @@ export class GameData { private parseSystemData(dataStr: string): SystemSaveData { return JSON.parse(dataStr, (k: string, v: any) => { - if (k === 'eggs') { + if (k === 'gameStats') + return new GameStats(v); + else if (k === 'eggs') { const ret: EggData[] = []; if (v === null) v = []; @@ -418,6 +429,9 @@ export class GameData { scene.money = sessionData.money || 0; scene.updateMoneyText(); + if (scene.money > this.gameStats.highestMoney) + this.gameStats.highestMoney = scene.money; + const battleType = sessionData.battleType || 0; const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfigs[sessionData.trainer.trainerType].isDouble : sessionData.enemyParty.length > 1); @@ -706,8 +720,12 @@ export class GameData { setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true): void { const dexEntry = this.dexData[pokemon.species.speciesId]; dexEntry.seenAttr |= pokemon.getDexAttr(); - if (incrementCount) + if (incrementCount) { dexEntry.seenCount++; + this.gameStats.pokemonSeen++; + if (pokemon.isShiny()) + this.gameStats.shinyPokemonSeen++; + } } setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false): Promise { @@ -721,10 +739,25 @@ export class GameData { dexEntry.caughtAttr |= pokemon.getDexAttr(); dexEntry.natureAttr |= Math.pow(2, pokemon.nature + 1); if (incrementCount) { - if (!fromEgg) + if (!fromEgg) { dexEntry.caughtCount++; - else + this.gameStats.pokemonCaught++; + if (pokemon.species.pseudoLegendary || pokemon.species.legendary) + this.gameStats.legendaryPokemonCaught++; + else if (pokemon.species.mythical) + this.gameStats.mythicalPokemonCaught++; + if (pokemon.isShiny()) + this.gameStats.shinyPokemonCaught++; + } else { dexEntry.hatchedCount++; + this.gameStats.pokemonHatched++; + if (pokemon.species.pseudoLegendary || pokemon.species.legendary) + this.gameStats.legendaryPokemonHatched++; + else if (pokemon.species.mythical) + this.gameStats.mythicalPokemonHatched++; + if (pokemon.isShiny()) + this.gameStats.shinyPokemonHatched++; + } } const hasPrevolution = pokemonPrevolutions.hasOwnProperty(species.speciesId); diff --git a/src/system/game-stats.ts b/src/system/game-stats.ts new file mode 100644 index 000000000..8f5ea646d --- /dev/null +++ b/src/system/game-stats.ts @@ -0,0 +1,64 @@ +// public (.*?): integer; +// this.$1 = source?.$1 || 0; + +export class GameStats { + public playTime: integer; + public battles: integer; + public classicSessionsPlayed: integer; + public sessionsWon: integer; + public endlessSessionsPlayed: integer; + public highestEndlessWave: integer; + public highestLevel: integer; + public highestMoney: integer; + public pokemonSeen: integer; + public pokemonDefeated: integer; + public pokemonCaught: integer; + public pokemonHatched: integer; + public legendaryPokemonSeen: integer; + public legendaryPokemonCaught: integer; + public legendaryPokemonHatched: integer; + public mythicalPokemonSeen: integer; + public mythicalPokemonCaught: integer; + public mythicalPokemonHatched: integer; + public shinyPokemonSeen: integer; + public shinyPokemonCaught: integer; + public shinyPokemonHatched: integer; + public pokemonFused: integer; + public trainersDefeated: integer; + public eggsPulled: integer; + public rareEggsPulled: integer; + public epicEggsPulled: integer; + public legendaryEggsPulled: integer; + public manaphyEggsPulled: integer; + + constructor(source?: any) { + this.playTime = source?.playTime || 0; + this.battles = source?.battles || 0; + this.classicSessionsPlayed = source?.classicSessionsPlayed || 0; + this.sessionsWon = source?.sessionsWon || 0; + this.endlessSessionsPlayed = source?.endlessSessionsPlayed || 0; + this.highestEndlessWave = source?.highestEndlessWave || 0; + this.highestLevel = source?.highestLevel || 0; + this.highestMoney = source?.highestMoney || 0; + this.pokemonSeen = source?.pokemonSeen || 0; + this.pokemonDefeated = source?.pokemonDefeated || 0; + this.pokemonCaught = source?.pokemonCaught || 0; + this.pokemonHatched = source?.pokemonHatched || 0; + this.legendaryPokemonSeen = source?.legendaryPokemonSeen || 0; + this.legendaryPokemonCaught = source?.legendaryPokemonCaught || 0; + this.legendaryPokemonHatched = source?.legendaryPokemonHatched || 0; + this.mythicalPokemonSeen = source?.mythicalPokemonSeen || 0; + this.mythicalPokemonCaught = source?.mythicalPokemonCaught || 0; + this.mythicalPokemonHatched = source?.mythicalPokemonCaught || 0; + this.shinyPokemonSeen = source?.shinyPokemonSeen || 0; + this.shinyPokemonCaught = source?.shinyPokemonCaught || 0; + this.shinyPokemonHatched = source?.shinyPokemonHatched || 0; + this.pokemonFused = source?.pokemonFused || 0; + this.trainersDefeated = source?.trainersDefeated || 0; + this.eggsPulled = source?.eggsPulled || 0; + this.rareEggsPulled = source?.rareEggsPulled || 0; + this.epicEggsPulled = source?.epicEggsPulled || 0; + this.legendaryEggsPulled = source?.legendaryEggsPulled || 0; + this.manaphyEggsPulled = source?.manaphyEggsPulled || 0; + } +} diff --git a/src/ui/abstact-option-select-ui-handler.ts b/src/ui/abstact-option-select-ui-handler.ts index 071dd16b0..31d925769 100644 --- a/src/ui/abstact-option-select-ui-handler.ts +++ b/src/ui/abstact-option-select-ui-handler.ts @@ -4,13 +4,25 @@ import { Mode } from "./ui"; import UiHandler from "./ui-handler"; import { addWindow } from "./window"; -export default abstract class AbstractOptionSelectUiHandler extends UiHandler { - protected handlers: Function[]; +export interface OptionSelectConfig { + xOffset?: number; + options: OptionSelectItem[]; +} +export interface OptionSelectItem { + label: string; + handler: Function; + keepOpen?: boolean; + overrideSound?: boolean; +} + +export default abstract class AbstractOptionSelectUiHandler extends UiHandler { protected optionSelectContainer: Phaser.GameObjects.Container; protected optionSelectBg: Phaser.GameObjects.NineSlice; protected optionSelectText: Phaser.GameObjects.Text; + protected config: OptionSelectConfig; + private cursorObj: Phaser.GameObjects.Image; constructor(scene: BattleScene, mode?: Mode) { @@ -19,9 +31,9 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { abstract getWindowWidth(): integer; - abstract getWindowHeight(): integer; - - abstract getOptions(): string[]; + getWindowHeight(): integer { + return ((this.config?.options || []).length + 1) * 16; + } setup() { const ui = this.getUi(); @@ -34,19 +46,19 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { this.optionSelectBg.setOrigin(1, 1); this.optionSelectContainer.add(this.optionSelectBg); - this.setupOptions(); this.setCursor(0); } protected setupOptions() { - const options = this.getOptions(); + const options = this.config?.options || []; if (this.optionSelectText) this.optionSelectText.destroy(); - this.optionSelectText = addTextObject(this.scene, 0, 0, options.join('\n'), TextStyle.WINDOW, { maxLines: options.length }); + this.optionSelectText = addTextObject(this.scene, 0, 0, options.map(o => o.label).join('\n'), TextStyle.WINDOW, { maxLines: options.length }); this.optionSelectText.setLineSpacing(12); this.optionSelectContainer.add(this.optionSelectText); + this.optionSelectContainer.x = (this.scene.game.canvas.width / 6) - 1 - (this.config?.xOffset || 0); this.optionSelectBg.width = Math.max(this.optionSelectText.displayWidth + 24, this.getWindowWidth()); this.optionSelectBg.height = this.getWindowHeight(); @@ -55,20 +67,20 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { } show(args: any[]): boolean { - const options = this.getOptions(); + if (!args.length || !args[0].hasOwnProperty('options') || !args[0].options.length) + return false; - if (args.length >= options.length && args.slice(0, options.length).filter(a => a instanceof Function).length === options.length) { - super.show(args); - - this.handlers = args.slice(0, options.length) as Function[]; + super.show(args); - this.optionSelectContainer.setVisible(true); - this.setCursor(0); + this.config = args[0] as OptionSelectConfig; + this.setupOptions(); - return true; - } + this.scene.ui.bringToTop(this.optionSelectContainer); - return false; + this.optionSelectContainer.setVisible(true); + this.setCursor(0); + + return true; } processInput(button: Button): boolean { @@ -76,15 +88,19 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { let success = false; - const options = this.getOptions(); + const options = this.config?.options || []; + + let playSound = true; if (button === Button.ACTION || button === Button.CANCEL) { success = true; if (button === Button.CANCEL) this.setCursor(options.length - 1); - const handler = this.handlers[this.cursor]; - handler(); - this.clear(); + const option = options[this.cursor]; + option.handler(); + if (!option.keepOpen) + this.clear(); + playSound = !option.overrideSound; } else { switch (button) { case Button.UP: @@ -98,7 +114,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { } } - if (success) + if (success && playSound) ui.playSelect(); return success; @@ -119,8 +135,7 @@ export default abstract class AbstractOptionSelectUiHandler extends UiHandler { clear() { super.clear(); - for (let h = 0; h < this.handlers.length; h++) - this.handlers[h] = null; + this.config = null; this.optionSelectContainer.setVisible(false); this.eraseCursor(); } diff --git a/src/ui/confirm-ui-handler.ts b/src/ui/confirm-ui-handler.ts index cde4fed7f..f25d142b2 100644 --- a/src/ui/confirm-ui-handler.ts +++ b/src/ui/confirm-ui-handler.ts @@ -1,5 +1,5 @@ import BattleScene from "../battle-scene"; -import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler"; +import AbstractOptionSelectUiHandler, { OptionSelectConfig } from "./abstact-option-select-ui-handler"; import { Mode } from "./ui"; export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { @@ -14,17 +14,22 @@ export default class ConfirmUiHandler extends AbstractOptionSelectUiHandler { return 48; } - getWindowHeight(): integer { - return 48; - } - - getOptions(): string[] { - return [ 'Yes', 'No' ]; - } - show(args: any[]): boolean { if (args.length >= 2 && args[0] instanceof Function && args[1] instanceof Function) { - super.show(args); + const config: OptionSelectConfig = { + options: [ + { + label: 'Yes', + handler: args[0] + }, + { + label: 'No', + handler: args[1] + } + ] + }; + + super.show([ config ]); this.switchCheck = args.length >= 3 && args[2] as boolean; diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index acecc947a..722111427 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -346,10 +346,25 @@ export default class EggGachaUiHandler extends MessageUiHandler { for (let tier of tiers) { const egg = new Egg(Utils.randInt(EGG_SEED, EGG_SEED * tier), this.gachaCursor, getEggTierDefaultHatchWaves(tier), timestamp); - if (egg.isManaphyEgg()) + if (egg.isManaphyEgg()) { + this.scene.gameData.gameStats.manaphyEggsPulled++; egg.hatchWaves = getEggTierDefaultHatchWaves(ModifierTier.ULTRA); + } else { + switch (tier) { + case ModifierTier.GREAT: + this.scene.gameData.gameStats.rareEggsPulled++; + break; + case ModifierTier.ULTRA: + this.scene.gameData.gameStats.epicEggsPulled++; + break; + case ModifierTier.MASTER: + this.scene.gameData.gameStats.legendaryEggsPulled++; + break; + } + } eggs.push(egg); this.scene.gameData.eggs.push(egg); + this.scene.gameData.gameStats.eggsPulled++; } this.scene.gameData.saveSystem().then(success => { diff --git a/src/ui/game-mode-select-ui-handler.ts b/src/ui/game-mode-select-ui-handler.ts index 07921da40..70ea0f68d 100644 --- a/src/ui/game-mode-select-ui-handler.ts +++ b/src/ui/game-mode-select-ui-handler.ts @@ -1,7 +1,7 @@ -import BattleScene, { Button } from "../battle-scene"; +import BattleScene from "../battle-scene"; import { GameMode, gameModeNames } from "../game-mode"; import { Unlockables } from "../system/unlockables"; -import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler"; +import AbstractOptionSelectUiHandler, { OptionSelectConfig, OptionSelectItem } from "./abstact-option-select-ui-handler"; import { Mode } from "./ui"; export default class GameModeSelectUiHandler extends AbstractOptionSelectUiHandler { @@ -14,28 +14,38 @@ export default class GameModeSelectUiHandler extends AbstractOptionSelectUiHandl return 104; } - getWindowHeight(): number { - return (this.getOptions().length + 1) * 16; - } - - getOptions(): string[] { - const ret = [ gameModeNames[GameMode.CLASSIC] ]; - if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { - ret.push(gameModeNames[GameMode.ENDLESS]); - if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) - ret.push(gameModeNames[GameMode.SPLICED_ENDLESS]); - } - ret.push('Cancel'); - return ret; - } - show(args: any[]): boolean { if (args.length === 2 && args[0] instanceof Function && args[1] instanceof Function) { - this.setupOptions(); + const options: OptionSelectItem[] = [ + { + label: gameModeNames[GameMode.CLASSIC], + handler: () => args[0](GameMode.CLASSIC) + } + ]; + + if (this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) { + options.push({ + label: gameModeNames[GameMode.ENDLESS], + handler: () => args[0](GameMode.ENDLESS) + }); + if (this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) { + options.push({ + label: gameModeNames[GameMode.SPLICED_ENDLESS], + handler: () => args[0](GameMode.SPLICED_ENDLESS) + }); + } + } + + options.push({ + label: 'Cancel', + handler: args[1] + }) + + const config: OptionSelectConfig = { + options: options + }; - super.show(args); - - this.handlers = args as Function[]; + super.show([ config ]); this.optionSelectContainer.setVisible(true); this.setCursor(0); @@ -45,25 +55,4 @@ export default class GameModeSelectUiHandler extends AbstractOptionSelectUiHandl return false; } - - processInput(button: Button): boolean { - const ui = this.getUi(); - - const options = this.getOptions(); - - if (button === Button.ACTION || button === Button.CANCEL) { - if (button === Button.CANCEL) - this.setCursor(options.length - 1); - if (this.cursor < options.length - 1) { - const gameMode = Object.values(gameModeNames).indexOf(options[this.cursor]) as GameMode; - this.handlers[0](gameMode); - } else - this.handlers[1](); - this.clear(); - ui.playSelect(); - } else - return super.processInput(button); - - return true; - } } \ No newline at end of file diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts new file mode 100644 index 000000000..8ba1e2ae4 --- /dev/null +++ b/src/ui/game-stats-ui-handler.ts @@ -0,0 +1,241 @@ +import BattleScene, { Button } from "../battle-scene"; +import { GameStats } from "../system/game-stats"; +import { TextStyle, addTextObject, getTextColor } from "./text"; +import { Mode } from "./ui"; +import UiHandler from "./ui-handler"; +import { addWindow } from "./window"; +import * as Utils from "../utils"; +import { GameData } from "../system/game-data"; +import { speciesStarters } from "../data/pokemon-species"; + +interface DisplayStat { + label?: string; + sourceFunc?: (gameData: GameData) => string; + hidden?: boolean; +} + +interface DisplayStats { + [key: string]: DisplayStat | string +} + +const secondsInHour = 3600; + +const displayStats: DisplayStats = { + playTime: { + sourceFunc: gameData => { + const totalSeconds = gameData.gameStats.playTime; + + const days = `${Math.floor(totalSeconds / (secondsInHour * 24))}`; + const hours = `${Math.floor(totalSeconds % (secondsInHour * 24) / secondsInHour)}`; + const minutes = `${Math.floor(totalSeconds % secondsInHour / 60)}`; + const seconds = `${Math.floor(totalSeconds % 60)}`; + + return `${days.padStart(2, '0')}:${hours.padStart(2, '0')}:${minutes.padStart(2, '0')}:${seconds.padStart(2, '0')}`; + } + }, + battles: 'Total Battles', + startersUnlocked: { + label: 'Starters', + sourceFunc: gameData => { + const starterKeys = Object.keys(speciesStarters); + let starterCount = 0; + for (let s of starterKeys) { + if (gameData.dexData[s].caughtAttr) + starterCount++; + } + return `${starterCount} (${Math.floor((starterCount / starterKeys.length) * 1000) / 10}%)`; + } + }, + classicSessionsPlayed: 'Runs (Classic)', + sessionsWon: 'Runs (Classic)', + endlessSessionsPlayed: 'Runs (Endless)?', + highestEndlessWave: 'Highest Wave (Endless)?', + highestMoney: 'Highest Money', + pokemonSeen: 'Pokémon Encountered', + pokemonDefeated: 'Pokémon Defeated', + pokemonCaught: 'Pokémon Caught', + pokemonHatched: 'Eggs Hatched', + legendaryPokemonSeen: 'Legendary Encounters?', + legendaryPokemonCaught: 'Legendaries Caught?', + legendaryPokemonHatched: 'Legendaries Hatched?', + mythicalPokemonSeen: 'Mythical Encounters?', + mythicalPokemonCaught: 'Mythicals Caught?', + mythicalPokemonHatched: 'Mythicals Hatched?', + shinyPokemonSeen: 'Shiny Encounters?', + shinyPokemonCaught: 'Shinies Caught?', + shinyPokemonHatched: 'Shinies Hatched?', + pokemonFused: 'Pokémon Fused?', + trainersDefeated: 'Trainers Defeated', + eggsPulled: 'Eggs Pulled', + rareEggsPulled: 'Rare Eggs Pulled?', + epicEggsPulled: 'Epic Eggs Pulled?', + legendaryEggsPulled: 'Legendary Eggs Pulled?', + manaphyEggsPulled: 'Manaphy Eggs Pulled?' +}; + +export default class GameStatsUiHandler extends UiHandler { + private gameStatsContainer: Phaser.GameObjects.Container; + private statsContainer: Phaser.GameObjects.Container; + + private statLabels: Phaser.GameObjects.Text[]; + private statValues: Phaser.GameObjects.Text[]; + + constructor(scene: BattleScene, mode?: Mode) { + super(scene, mode); + + this.statLabels = []; + this.statValues = []; + } + + setup() { + const ui = this.getUi(); + + this.gameStatsContainer = this.scene.add.container(1, -(this.scene.game.canvas.height / 6) + 1); + + this.gameStatsContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, this.scene.game.canvas.width / 6, this.scene.game.canvas.height / 6), Phaser.Geom.Rectangle.Contains); + + const headerBg = addWindow(this.scene, 0, 0, (this.scene.game.canvas.width / 6) - 2, 24); + headerBg.setOrigin(0, 0); + + const headerText = addTextObject(this.scene, 0, 0, 'Stats', TextStyle.SETTINGS_LABEL); + headerText.setOrigin(0, 0); + headerText.setPositionRelative(headerBg, 8, 4); + + const statsBgWidth = ((this.scene.game.canvas.width / 6) - 2) / 2; + const [ statsBgLeft, statsBgRight ] = new Array(2).fill(null).map((_, i) => { + let width = statsBgWidth; + if (!i) + width += 5; + const statsBg = addWindow(this.scene, statsBgWidth * i, headerBg.height, width, (this.scene.game.canvas.height / 6) - headerBg.height - 2, false, !!i, 2); + statsBg.setOrigin(0, 0); + return statsBg; + }); + + this.statsContainer = this.scene.add.container(0, 0); + + new Array(18).fill(null).map((_, s) => { + const statLabel = addTextObject(this.scene, 8 + (s % 2 === 1 ? statsBgWidth : 0), 28 + Math.floor(s / 2) * 16, '', TextStyle.SETTINGS_LABEL); + statLabel.setOrigin(0, 0); + this.statsContainer.add(statLabel); + this.statLabels.push(statLabel); + + const statValue = addTextObject(this.scene, (statsBgWidth * ((s % 2) + 1)) - 8, statLabel.y, '', TextStyle.WINDOW); + statValue.setOrigin(1, 0); + this.statsContainer.add(statValue); + this.statValues.push(statValue); + }); + + this.gameStatsContainer.add(headerBg); + this.gameStatsContainer.add(headerText); + this.gameStatsContainer.add(statsBgLeft); + this.gameStatsContainer.add(statsBgRight); + this.gameStatsContainer.add(this.statsContainer); + + ui.add(this.gameStatsContainer); + + this.setCursor(0); + + this.gameStatsContainer.setVisible(false); + } + + show(args: any[]): boolean { + super.show(args); + + this.setCursor(0); + + this.updateStats(); + + this.gameStatsContainer.setVisible(true); + + this.getUi().moveTo(this.gameStatsContainer, this.getUi().length - 1); + + this.getUi().hideTooltip(); + + return true; + } + + updateStats(): void { + const statKeys = Object.keys(displayStats).slice(this.cursor * 2, this.cursor * 2 + 18); + statKeys.forEach((key, s) => { + const stat = displayStats[key] as DisplayStat; + const value = stat.sourceFunc(this.scene.gameData); + this.statLabels[s].setText(!stat.hidden || isNaN(parseInt(value)) || parseInt(value) ? stat.label : '???'); + this.statValues[s].setText(value); + }); + if (statKeys.length < 18) { + for (let s = statKeys.length; s < 18; s++) { + this.statLabels[s].setText(''); + this.statValues[s].setText(''); + } + } + } + + processInput(button: Button): boolean { + const ui = this.getUi(); + + let success = false; + + if (button === Button.CANCEL) { + success = true; + this.scene.ui.revertMode(); + } else { + switch (button) { + case Button.UP: + if (this.cursor) + success = this.setCursor(this.cursor - 1); + break; + case Button.DOWN: + if (this.cursor < Math.ceil((Object.keys(displayStats).length - 18) / 2)) + success = this.setCursor(this.cursor + 1); + break; + } + } + + if (success) + ui.playSelect(); + + return success; + } + + setCursor(cursor: integer): boolean { + const ret = super.setCursor(cursor); + + if (ret) + this.updateStats(); + + return ret; + } + + clear() { + super.clear(); + this.gameStatsContainer.setVisible(false); + } +} + +(function () { + const statKeys = Object.keys(displayStats); + + for (let key of statKeys) { + if (typeof displayStats[key] === 'string') { + let label = displayStats[key] as string; + let hidden = false; + if (label.endsWith('?')) { + label = label.slice(0, -1); + hidden = true; + } + displayStats[key] = { + label: label, + sourceFunc: gameData => gameData.gameStats[key].toString(), + hidden: hidden + }; + } else if (displayStats[key] === null) { + displayStats[key] = { + sourceFunc: gameData => gameData.gameStats[key].toString() + }; + } + if (!(displayStats[key] as DisplayStat).label) { + const splittableKey = key.replace(/([a-z]{2,})([A-Z]{1}(?:[^A-Z]|$))/g, '$1_$2'); + (displayStats[key] as DisplayStat).label = Utils.toReadableString(`${splittableKey[0].toUpperCase()}${splittableKey.slice(1)}`); + } + } +})(); \ No newline at end of file diff --git a/src/ui/menu-ui-handler.ts b/src/ui/menu-ui-handler.ts index 96cb606f4..1d145d49c 100644 --- a/src/ui/menu-ui-handler.ts +++ b/src/ui/menu-ui-handler.ts @@ -5,17 +5,16 @@ import * as Utils from "../utils"; import { addWindow } from "./window"; import MessageUiHandler from "./message-ui-handler"; import { GameDataType } from "../system/game-data"; +import { OptionSelectConfig } from "./abstact-option-select-ui-handler"; export enum MenuOptions { GAME_SETTINGS, ACHIEVEMENTS, + STATS, VOUCHERS, EGG_LIST, EGG_GACHA, - IMPORT_SESSION, - EXPORT_SESSION, - IMPORT_DATA, - EXPORT_DATA, + MANAGE_DATA, LOG_OUT } @@ -31,12 +30,14 @@ export default class MenuUiHandler extends MessageUiHandler { protected ignoredMenuOptions: MenuOptions[]; protected menuOptions: MenuOptions[]; + protected manageDataConfig: OptionSelectConfig; + constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); - this.ignoredMenuOptions = /*!bypassLogin */ false - ? [ MenuOptions.IMPORT_SESSION, MenuOptions.IMPORT_DATA ] - : []; + this.ignoredMenuOptions = !bypassLogin + ? [ ] + : [ MenuOptions.LOG_OUT ]; this.menuOptions = Utils.getEnumKeys(MenuOptions).map(m => parseInt(MenuOptions[m]) as MenuOptions).filter(m => this.ignoredMenuOptions.indexOf(m) === -1); } @@ -76,6 +77,44 @@ export default class MenuUiHandler extends MessageUiHandler { this.menuContainer.add(this.menuMessageBoxContainer); + const manageDataOptions = []; + + if (bypassLogin) { + manageDataOptions.push({ + label: 'Import Session', + handler: () => this.scene.gameData.importData(GameDataType.SESSION), + keepOpen: true + }); + } + manageDataOptions.push({ + label: 'Export Session', + handler: () => this.scene.gameData.exportData(GameDataType.SESSION), + keepOpen: true + }); + if (bypassLogin) { + manageDataOptions.push({ + label: 'Import Data', + handler: () => this.scene.gameData.importData(GameDataType.SYSTEM), + keepOpen: true + }); + } + manageDataOptions.push( + { + label: 'Export Data', + handler: () => this.scene.gameData.exportData(GameDataType.SYSTEM), + keepOpen: true + }, + { + label: 'Cancel', + handler: () => this.scene.ui.revertMode() + } + ); + + this.manageDataConfig = { + xOffset: 98, + options: manageDataOptions + }; + this.setCursor(0); this.menuContainer.setVisible(false); @@ -112,38 +151,36 @@ export default class MenuUiHandler extends MessageUiHandler { } switch (adjustedCursor) { case MenuOptions.GAME_SETTINGS: - this.scene.ui.setOverlayMode(Mode.SETTINGS); + ui.setOverlayMode(Mode.SETTINGS); success = true; break; case MenuOptions.ACHIEVEMENTS: - this.scene.ui.setOverlayMode(Mode.ACHIEVEMENTS); + ui.setOverlayMode(Mode.ACHIEVEMENTS); + success = true; + break; + case MenuOptions.STATS: + ui.setOverlayMode(Mode.GAME_STATS); success = true; break; case MenuOptions.VOUCHERS: - this.scene.ui.setOverlayMode(Mode.VOUCHERS); + ui.setOverlayMode(Mode.VOUCHERS); success = true; break; case MenuOptions.EGG_LIST: if (this.scene.gameData.eggs.length) { - this.scene.ui.revertMode(); - this.scene.ui.setOverlayMode(Mode.EGG_LIST); + ui.revertMode(); + ui.setOverlayMode(Mode.EGG_LIST); success = true; } else error = true; break; case MenuOptions.EGG_GACHA: - this.scene.ui.revertMode(); - this.scene.ui.setOverlayMode(Mode.EGG_GACHA); + ui.revertMode(); + ui.setOverlayMode(Mode.EGG_GACHA); success = true; break; - case MenuOptions.IMPORT_SESSION: - case MenuOptions.IMPORT_DATA: - this.scene.gameData.importData(this.cursor === MenuOptions.IMPORT_DATA ? GameDataType.SYSTEM : GameDataType.SESSION); - success = true; - break; - case MenuOptions.EXPORT_SESSION: - case MenuOptions.EXPORT_DATA: - this.scene.gameData.exportData(this.cursor === MenuOptions.EXPORT_DATA ? GameDataType.SYSTEM : GameDataType.SESSION); + case MenuOptions.MANAGE_DATA: + ui.setOverlayMode(Mode.OPTION_SELECT, this.manageDataConfig); success = true; break; case MenuOptions.LOG_OUT: @@ -157,10 +194,10 @@ export default class MenuUiHandler extends MessageUiHandler { }); }; if (this.scene.currentBattle) { - this.scene.ui.showText('You will lose any progress since the beginning of the battle. Proceed?', null, () => { - this.scene.ui.setOverlayMode(Mode.CONFIRM, doLogout, () => { - this.scene.ui.revertMode(); - this.scene.ui.showText(null, 0); + ui.showText('You will lose any progress since the beginning of the battle. Proceed?', null, () => { + ui.setOverlayMode(Mode.CONFIRM, doLogout, () => { + ui.revertMode(); + ui.showText(null, 0); }, false, 98); }); } else @@ -169,7 +206,7 @@ export default class MenuUiHandler extends MessageUiHandler { } } else if (button === Button.CANCEL) { success = true; - if (!this.scene.ui.revertMode()) + if (!ui.revertMode()) ui.setMode(Mode.MESSAGE); } else { switch (button) { diff --git a/src/ui/option-select-ui-handler.ts b/src/ui/option-select-ui-handler.ts index 9d14ec586..fea7784f9 100644 --- a/src/ui/option-select-ui-handler.ts +++ b/src/ui/option-select-ui-handler.ts @@ -3,8 +3,6 @@ import AbstractOptionSelectUiHandler from "./abstact-option-select-ui-handler"; import { Mode } from "./ui"; export default class OptionSelectUiHandler extends AbstractOptionSelectUiHandler { - private options: string[] = []; - constructor(scene: BattleScene) { super(scene, Mode.OPTION_SELECT); } @@ -12,28 +10,4 @@ export default class OptionSelectUiHandler extends AbstractOptionSelectUiHandler getWindowWidth(): integer { return 64; } - - getWindowHeight(): integer { - return (this.getOptions().length + 1) * 16; - } - - getOptions(): string[] { - return this.options; - } - - show(args: any[]): boolean { - if (args.length < 2 || args.length % 2 === 1) - return false; - - const optionNames: string[] = []; - const optionFuncs: Function[] = []; - - args.map((arg, i) => (i % 2 ? optionFuncs : optionNames).push(arg)); - - this.options = optionNames; - - this.setupOptions(); - - return super.show(optionFuncs); - } } \ No newline at end of file diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index edb1fd09a..1d0d61bba 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -419,38 +419,50 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (!this.speciesStarterDexEntry?.caughtAttr) error = true; else if (this.starterCursors.length < 6) { - ui.setModeWithoutClear(Mode.OPTION_SELECT, 'Add to Party', () => { - ui.setMode(Mode.STARTER_SELECT); - let isDupe = false; - for (let s = 0; s < this.starterCursors.length; s++) { - if (this.starterGens[s] === this.getGenCursorWithScroll() && this.starterCursors[s] === this.cursor) { - isDupe = true; - break; + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: [ + { + label: 'Add to Party', + handler: () => { + ui.setMode(Mode.STARTER_SELECT); + let isDupe = false; + for (let s = 0; s < this.starterCursors.length; s++) { + if (this.starterGens[s] === this.getGenCursorWithScroll() && this.starterCursors[s] === this.cursor) { + isDupe = true; + break; + } + } + const species = this.genSpecies[this.getGenCursorWithScroll()][this.cursor]; + if (!isDupe && this.tryUpdateValue(speciesStarterValues[species.speciesId])) { + const cursorObj = this.starterCursorObjs[this.starterCursors.length]; + cursorObj.setVisible(true); + cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); + const props = this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + this.starterIcons[this.starterCursors.length].setTexture(species.getIconAtlasKey(props.formIndex)); + this.starterIcons[this.starterCursors.length].setFrame(species.getIconId(props.female, props.formIndex, props.shiny)); + this.starterGens.push(this.getGenCursorWithScroll()); + this.starterCursors.push(this.cursor); + this.starterAttr.push(this.dexAttrCursor); + this.starterNatures.push(this.natureCursor as unknown as Nature); + if (this.speciesLoaded.get(species.speciesId)) + species.cry(this.scene); + if (this.starterCursors.length === 6 || this.value === 10) + this.tryStart(); + this.updateInstructions(); + ui.playSelect(); + } else + ui.playError(); + }, + overrideSound: true + }, + { + label: 'Toggle IVs', + handler: () => { + this.toggleStatsMode(); + ui.setMode(Mode.STARTER_SELECT); + } } - } - const species = this.genSpecies[this.getGenCursorWithScroll()][this.cursor]; - if (!isDupe && this.tryUpdateValue(speciesStarterValues[species.speciesId])) { - const cursorObj = this.starterCursorObjs[this.starterCursors.length]; - cursorObj.setVisible(true); - cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); - const props = this.scene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); - this.starterIcons[this.starterCursors.length].setTexture(species.getIconAtlasKey(props.formIndex)); - this.starterIcons[this.starterCursors.length].setFrame(species.getIconId(props.female, props.formIndex, props.shiny)); - this.starterGens.push(this.getGenCursorWithScroll()); - this.starterCursors.push(this.cursor); - this.starterAttr.push(this.dexAttrCursor); - this.starterNatures.push(this.natureCursor as unknown as Nature); - if (this.speciesLoaded.get(species.speciesId)) - species.cry(this.scene); - if (this.starterCursors.length === 6 || this.value === 10) - this.tryStart(); - this.updateInstructions(); - ui.playSelect(); - } else - ui.playError(); - }, 'Toggle IVs', () => { - this.toggleStatsMode(); - ui.setMode(Mode.STARTER_SELECT); + ] }); success = true; } diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 9a6e66eb9..f1434d135 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -29,6 +29,7 @@ import LoginFormUiHandler from './login-form-ui-handler'; import RegistrationFormUiHandler from './registration-form-ui-handler'; import LoadingModalUiHandler from './loading-modal-ui-handler'; import * as Utils from "../utils"; +import GameStatsUiHandler from './game-stats-ui-handler'; export enum Mode { MESSAGE, @@ -49,6 +50,7 @@ export enum Mode { MENU, SETTINGS, ACHIEVEMENTS, + GAME_STATS, VOUCHERS, EGG_LIST, EGG_GACHA, @@ -74,6 +76,7 @@ const noTransitionModes = [ Mode.MENU, Mode.SETTINGS, Mode.ACHIEVEMENTS, + Mode.GAME_STATS, Mode.VOUCHERS, Mode.LOGIN_FORM, Mode.REGISTRATION_FORM, @@ -118,6 +121,7 @@ export default class UI extends Phaser.GameObjects.Container { new MenuUiHandler(scene), new SettingsUiHandler(scene), new AchvsUiHandler(scene), + new GameStatsUiHandler(scene), new VouchersUiHandler(scene), new EggListUiHandler(scene), new EggGachaUiHandler(scene),