diff --git a/src/locales/de/starter-select-ui-handler.ts b/src/locales/de/starter-select-ui-handler.ts index a448dcedad8..c31f4725806 100644 --- a/src/locales/de/starter-select-ui-handler.ts +++ b/src/locales/de/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "Zum Team hinzufügen", "toggleIVs": "DVs anzeigen/verbergen", "manageMoves": "Attacken ändern", + "manageNature": "Wesen ändern", "useCandies": "Bonbons verwenden", + "selectNature": "Wähle das neue Wesen.", "selectMoveSwapOut": "Wähle die zu ersetzende Attacke.", "selectMoveSwapWith": "Wähle die gewünschte Attacke.", "unlockPassive": "Passiv-Skill freischalten", diff --git a/src/locales/en/starter-select-ui-handler.ts b/src/locales/en/starter-select-ui-handler.ts index fd2eb6c40df..d17f681b53c 100644 --- a/src/locales/en/starter-select-ui-handler.ts +++ b/src/locales/en/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "Add to Party", "toggleIVs": "Toggle IVs", "manageMoves": "Manage Moves", + "manageNature": "Manage Nature", "useCandies": "Use Candies", + "selectNature": "Select nature.", "selectMoveSwapOut": "Select a move to swap out.", "selectMoveSwapWith": "Select a move to swap with", "unlockPassive": "Unlock Passive", diff --git a/src/locales/es/starter-select-ui-handler.ts b/src/locales/es/starter-select-ui-handler.ts index 4d025820260..9b6e03b26c0 100644 --- a/src/locales/es/starter-select-ui-handler.ts +++ b/src/locales/es/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "Añadir a Equipo", "toggleIVs": "Mostrar IVs", "manageMoves": "Gestionar Movs.", + "manageNature": "Gestionar Natur", "useCandies": "Usar Caramelos", + "selectNature": "Elige Natur.", "selectMoveSwapOut": "Elige el movimiento que sustituir.", "selectMoveSwapWith": "Elige el movimiento que sustituirá a", "unlockPassive": "Añadir Pasiva", diff --git a/src/locales/fr/starter-select-ui-handler.ts b/src/locales/fr/starter-select-ui-handler.ts index 9f504cab11e..02e5c2742ac 100644 --- a/src/locales/fr/starter-select-ui-handler.ts +++ b/src/locales/fr/starter-select-ui-handler.ts @@ -23,9 +23,11 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "eggMoves": "Capacités Œuf", "start": "Lancer", "addToParty": "Ajouter à l’équipe", - "toggleIVs": "Voir IVs", - "manageMoves": "Gérer Capacités", - "useCandies": "Utiliser Bonbons", + "toggleIVs": "Voir les IV", + "manageMoves": "Modifier les Capacités", + "manageNature": "Modifier la Nature", + "useCandies": "Utiliser des Bonbons", + "selectNature": "Sélectionnez une nature.", "selectMoveSwapOut": "Sélectionnez la capacité à échanger.", "selectMoveSwapWith": "Sélectionnez laquelle échanger avec", "unlockPassive": "Débloquer Passif", diff --git a/src/locales/it/starter-select-ui-handler.ts b/src/locales/it/starter-select-ui-handler.ts index 48a0badaefc..fea0d2d8352 100644 --- a/src/locales/it/starter-select-ui-handler.ts +++ b/src/locales/it/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "Aggiungi al gruppo", "toggleIVs": "Vedi/Nascondi IV", "manageMoves": "Gestisci mosse", + "manageNature": "Gestisci natura", "useCandies": "Usa caramelle", + "selectNature": "Seleziona natura.", "selectMoveSwapOut": "Seleziona una mossa da scambiare.", "selectMoveSwapWith": "Seleziona una mossa da scambiare con", "unlockPassive": "Sblocca passiva", diff --git a/src/locales/pt_BR/starter-select-ui-handler.ts b/src/locales/pt_BR/starter-select-ui-handler.ts index fc98e72c614..7ee8789f860 100644 --- a/src/locales/pt_BR/starter-select-ui-handler.ts +++ b/src/locales/pt_BR/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "Adicionar à equipe", "toggleIVs": "Mostrar IVs", "manageMoves": "Mudar Movimentos", + "manageNature": "Mudar Natureza", "useCandies": "Usar Doces", + "selectNature": "Escolha Natureza.", "selectMoveSwapOut": "Escolha um movimento para substituir.", "selectMoveSwapWith": "Escolha o movimento que substituirá", "unlockPassive": "Aprender Passiva", diff --git a/src/locales/zh_CN/starter-select-ui-handler.ts b/src/locales/zh_CN/starter-select-ui-handler.ts index 803e64e0439..742e3c815f0 100644 --- a/src/locales/zh_CN/starter-select-ui-handler.ts +++ b/src/locales/zh_CN/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "加入队伍", "toggleIVs": "切换个体值", "manageMoves": "管理招式", + "manageNature": "管理性格", "useCandies": "使用糖果", + "selectNature": "选择性格", "selectMoveSwapOut": "选择要替换的招式。", "selectMoveSwapWith": "选择要替换成的招式", "unlockPassive": "解锁被动", diff --git a/src/locales/zh_TW/starter-select-ui-handler.ts b/src/locales/zh_TW/starter-select-ui-handler.ts index c28cb39b94b..748cb05af40 100644 --- a/src/locales/zh_TW/starter-select-ui-handler.ts +++ b/src/locales/zh_TW/starter-select-ui-handler.ts @@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = { "addToParty": "加入隊伍", "toggleIVs": "查看個體值", "manageMoves": "管理技能", + "manageNature": "管理性格", "useCandies": "使用糖果", + "selectNature": "選擇性格", "selectMoveSwapOut": "選擇想要替換走的招式", "selectMoveSwapWith": "選擇想要替換成的招式", "unlockPassive": "解鎖被動", diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 0e26eab436d..f36bf1af229 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -136,7 +136,7 @@ interface VoucherUnlocks { } export interface VoucherCounts { - [type: string]: integer; + [type: string]: integer; } export interface DexData { @@ -187,6 +187,46 @@ export interface StarterMoveData { [key: integer]: StarterMoveset | StarterFormMoveData } +export interface StarterAttributes { + nature?: integer; + ability?: integer; + variant?: integer; + form?: integer; + female?: boolean; +} + +export interface StarterPreferences { + [key: integer]: StarterAttributes; +} + +// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present. +// if they ever add private static variables, move this into StarterPrefs +const StarterPrefers_DEFAULT : string = "{}"; +let StarterPrefers_private_latest : string = StarterPrefers_DEFAULT; + +// This is its own class as StarterPreferences... +// - don't need to be loaded on startup +// - isn't stored with other data +// - don't require to be encrypted +// - shouldn't require calls outside of the starter selection +export class StarterPrefs { + // called on starter selection show once + static load(): StarterPreferences { + return JSON.parse( + StarterPrefers_private_latest = (localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT) + ); + } + + // called on starter selection clear, always + static save(prefs: StarterPreferences): void { + const pStr : string = JSON.stringify(prefs); + if (pStr !== StarterPrefers_private_latest) { + // something changed, store the update + localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr); + } + } +} + export interface StarterDataEntry { moveset: StarterMoveset | StarterFormMoveData; eggMoves: integer; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 0aa71de7e90..b9fd5c5c27e 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -17,7 +17,7 @@ import PokemonSpecies, { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, g import { Type } from "../data/type"; import { GameModes } from "../game-mode"; import { SelectChallengePhase, TitlePhase } from "../phases"; -import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterFormMoveData, StarterMoveset } from "../system/game-data"; +import { AbilityAttr, DexAttr, DexAttrProps, DexEntry, StarterFormMoveData, StarterMoveset, StarterAttributes, StarterPreferences, StarterPrefs } from "../system/game-data"; import { Tutorial, handleTutorial } from "../tutorial"; import * as Utils from "../utils"; import { OptionSelectItem } from "./abstact-option-select-ui-handler"; @@ -270,6 +270,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private starterSelectCallback: StarterSelectCallback; + private starterPreferences: StarterPreferences; + protected blockInput: boolean = false; constructor(scene: BattleScene) { @@ -780,6 +782,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } show(args: any[]): boolean { + if (!this.starterPreferences) { + // starterPreferences haven't been loaded yet + this.starterPreferences = StarterPrefs.load(); + } this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers if (args.length >= 1 && args[0] instanceof Function) { super.show(args); @@ -1223,6 +1229,54 @@ export default class StarterSelectUiHandler extends MessageUiHandler { }); } const starterData = this.scene.gameData.starterData[this.lastSpecies.speciesId]; + let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]; + if (this.canCycleNature) { + // if we could cycle natures, enable the improved nature menu + const showNatureOptions = () => { + ui.setMode(Mode.STARTER_SELECT).then(() => { + ui.showText(i18next.t("starterSelectUiHandler:selectNature"), null, () => { + const natures = this.scene.gameData.getNaturesForAttr(this.speciesStarterDexEntry.natureAttr); + ui.setModeWithoutClear(Mode.OPTION_SELECT, { + options: natures.map((n: Nature, i: number) => { + const option: OptionSelectItem = { + label: getNatureName(n, true, true, true, this.scene.uiTheme), + handler: () => { + // update default nature in starter save data + if (!starterAttributes) { + starterAttributes= + this.starterPreferences[this.lastSpecies.speciesId] = {}; + } + starterAttributes.nature = n as unknown as integer; + this.clearText(); + ui.setMode(Mode.STARTER_SELECT); + // set nature for starter + this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, n, undefined); + return true; + } + }; + return option; + }).concat({ + label: i18next.t("menu:cancel"), + handler: () => { + this.clearText(); + ui.setMode(Mode.STARTER_SELECT); + return true; + } + }), + maxOptions: 8, + yOffset: 19 + }); + }); + }); + }; + options.push({ + label: i18next.t("starterSelectUiHandler:manageNature"), + handler: () => { + showNatureOptions(); + return true; + } + }); + } const candyCount = starterData.candyCount; const passiveAttr = starterData.passiveAttr; if (passiveAttr & PassiveAttr.UNLOCKED) { @@ -1362,9 +1416,16 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const rows = Math.ceil(genStarters / 9); const row = Math.floor(this.cursor / 9); const props = this.scene.gameData.getSpeciesDexAttrProps(this.lastSpecies, this.dexAttrCursor); + // prepare persistent starter data to store changes + let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]; + if (!starterAttributes) { + starterAttributes = + this.starterPreferences[this.lastSpecies.speciesId] = {}; + } switch (button) { case Button.CYCLE_SHINY: if (this.canCycleShiny) { + starterAttributes.variant = !props.shiny ? props.variant : -1; // update shiny setting this.setSpeciesDetails(this.lastSpecies, !props.shiny, undefined, undefined, props.shiny ? 0 : undefined, undefined, undefined); if (this.dexAttrCursor & DexAttr.SHINY) { this.scene.playSound("sparkle"); @@ -1383,12 +1444,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { break; } } while (newFormIndex !== props.formIndex); + starterAttributes.form = newFormIndex; // store the selected form this.setSpeciesDetails(this.lastSpecies, undefined, newFormIndex, undefined, undefined, undefined, undefined); success = true; } break; case Button.CYCLE_GENDER: if (this.canCycleGender) { + starterAttributes.female = !props.female; this.setSpeciesDetails(this.lastSpecies, undefined, undefined, !props.female, undefined, undefined, undefined); success = true; } @@ -1414,6 +1477,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } } while (newAbilityIndex !== this.abilityCursor); + starterAttributes.ability = newAbilityIndex; // store the selected ability this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, newAbilityIndex, undefined); success = true; } @@ -1423,6 +1487,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const natures = this.scene.gameData.getNaturesForAttr(this.speciesStarterDexEntry.natureAttr); const natureIndex = natures.indexOf(this.natureCursor); const newNature = natures[natureIndex < natures.length - 1 ? natureIndex + 1 : 0]; + // store cycled nature as default + starterAttributes.nature = newNature as unknown as integer; this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, undefined, undefined, newNature, undefined); success = true; } @@ -1446,6 +1512,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } } while (newVariant !== props.variant); + starterAttributes.variant = newVariant; // store the selected variant this.setSpeciesDetails(this.lastSpecies, undefined, undefined, undefined, newVariant, undefined, undefined); // Cycle tint based on current sprite tint @@ -1782,6 +1849,60 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.abilityCursor = species ? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0; this.natureCursor = species ? this.scene.gameData.getSpeciesDefaultNature(species) : 0; + const starterAttributes : StarterAttributes = species ? {...this.starterPreferences[species.speciesId]} : null; + // validate starterAttributes + if (starterAttributes) { + // this may cause changes so we created a copy of the attributes before + if (!isNaN(starterAttributes.variant)) { + if (![ + this.speciesStarterDexEntry.caughtAttr & DexAttr.NON_SHINY, + this.speciesStarterDexEntry.caughtAttr & DexAttr.DEFAULT_VARIANT, + this.speciesStarterDexEntry.caughtAttr & DexAttr.VARIANT_2, + this.speciesStarterDexEntry.caughtAttr & DexAttr.VARIANT_3 + ][starterAttributes.variant+1]) { // add 1 as -1 = non-shiny + // requested variant wasn't unlocked, purging setting + delete starterAttributes.variant; + } + } + + if (typeof starterAttributes.female !== "boolean" || !(starterAttributes.female ? + this.speciesStarterDexEntry.caughtAttr & DexAttr.FEMALE : + this.speciesStarterDexEntry.caughtAttr & DexAttr.MALE + )) { + // requested gender wasn't unlocked, purging setting + delete starterAttributes.female; + } + + const abilityAttr = this.scene.gameData.starterData[species.speciesId].abilityAttr; + if (![ + abilityAttr & AbilityAttr.ABILITY_1, + species.ability2 ? (abilityAttr & AbilityAttr.ABILITY_2) : abilityAttr & AbilityAttr.ABILITY_HIDDEN, + species.ability2 && abilityAttr & AbilityAttr.ABILITY_HIDDEN + ][starterAttributes.ability]) { + // requested ability wasn't unlocked, purging setting + delete starterAttributes.ability; + } + + if (!(species.forms[starterAttributes.form]?.isStarterSelectable && this.speciesStarterDexEntry.caughtAttr & this.scene.gameData.getFormAttr(starterAttributes.form))) { + // requested form wasn't unlocked/isn't a starter form, purging setting + delete starterAttributes.form; + } + + if (this.scene.gameData.getNaturesForAttr(this.speciesStarterDexEntry.natureAttr).indexOf(starterAttributes.nature as unknown as Nature) < 0) { + // requested nature wasn't unlocked, purging setting + delete starterAttributes.nature; + } + } + + if (starterAttributes?.nature) { + // load default nature from stater save data, if set + this.natureCursor = starterAttributes.nature; + } + if (!isNaN(starterAttributes?.ability)) { + // load default nature from stater save data, if set + this.abilityCursor = starterAttributes.ability; + } + if (this.statsMode) { if (this.speciesStarterDexEntry?.caughtAttr) { this.statsContainer.setVisible(true); @@ -1920,9 +2041,17 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, this.starterAbilityIndexes[starterIndex], this.starterNatures[starterIndex]); } else { const defaultDexAttr = this.scene.gameData.getSpeciesDefaultDexAttr(species, false, true); - const defaultAbilityIndex = this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); - const defaultNature = this.scene.gameData.getSpeciesDefaultNature(species); + const defaultAbilityIndex = starterAttributes?.ability ?? this.scene.gameData.getStarterSpeciesDefaultAbilityIndex(species); + // load default nature from stater save data, if set + const defaultNature = starterAttributes?.nature || this.scene.gameData.getSpeciesDefaultNature(species); props = this.scene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); + if (!isNaN(starterAttributes?.variant)) { + if (props.shiny = (starterAttributes.variant >= 0)) { + props.variant = starterAttributes.variant as Variant; + } + } + props.formIndex = starterAttributes?.form ?? props.formIndex; + props.female = starterAttributes?.female ?? props.female; this.setSpeciesDetails(species, props.shiny, props.formIndex, props.female, props.variant, defaultAbilityIndex, defaultNature); } @@ -2417,6 +2546,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { clear(): void { super.clear(); + + StarterPrefs.save(this.starterPreferences); this.cursor = -1; this.hideInstructions(); this.starterSelectContainer.setVisible(false);