[QoL] Improved Nature selection + Persistent selection of various values (#1401)

* Added nature selection menu

Added a nature selection menu to the starter selection, as you can way too easily skip over the nature you want when cycling.
All nature selections are furthermore persistent and stored in the starterData.

Those changes are compatible with the current save format, but will increase the size, so, in case the size of the save file can't be increased, further changes will be needed.

* Sorted nature selection into sub-menus

The nature-selection menu is now, instead of one large list, multiple smaller menus.
Those menus will appear once at least one nature of the appropriate kind has been collected.

Translations partially required.

* Update French starter-select-ui-handler.ts

* Added support for updated save structure

Adds compatibility with updated save-data structure which supports more persistent starter attributes

* more persistent start data

Abilities, Variants and Forms are now also saved.

* added gender to stored information

* fixed typedoc issues

* Starter Preferences now stored locally

* removed deprecated import (due last merge)

* Sub menus removed

---------

Co-authored-by: Lugiad <adrien.grivel@hotmail.fr>
This commit is contained in:
prime 2024-06-17 06:49:29 +02:00 committed by GitHub
parent 9e1da3d548
commit 111f4362fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 194 additions and 7 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"addToParty": "加入队伍",
"toggleIVs": "切换个体值",
"manageMoves": "管理招式",
"manageNature": "管理性格",
"useCandies": "使用糖果",
"selectNature": "选择性格",
"selectMoveSwapOut": "选择要替换的招式。",
"selectMoveSwapWith": "选择要替换成的招式",
"unlockPassive": "解锁被动",

View File

@ -25,7 +25,9 @@ export const starterSelectUiHandler: SimpleTranslationEntries = {
"addToParty": "加入隊伍",
"toggleIVs": "查看個體值",
"manageMoves": "管理技能",
"manageNature": "管理性格",
"useCandies": "使用糖果",
"selectNature": "選擇性格",
"selectMoveSwapOut": "選擇想要替換走的招式",
"selectMoveSwapWith": "選擇想要替換成的招式",
"unlockPassive": "解鎖被動",

View File

@ -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;

View File

@ -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);