mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 15:03:24 +02:00
Merge pull request #6431 from pagefaultgames/fix-starterprefs
Fix starterpref reset (#6430) to main
This commit is contained in:
commit
95ddcae543
@ -72,7 +72,7 @@ import {
|
|||||||
rgbHexToRgba,
|
rgbHexToRgba,
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
import type { StarterPreferences } from "#utils/data";
|
import type { StarterPreferences } from "#utils/data";
|
||||||
import { loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
import { deepCopy, loadStarterPreferences, saveStarterPreferences } from "#utils/data";
|
||||||
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
|
import { getPokemonSpeciesForm, getPokerusStarters } from "#utils/pokemon-utils";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import { argbFromRgba } from "@material/material-color-utilities";
|
import { argbFromRgba } from "@material/material-color-utilities";
|
||||||
@ -1148,7 +1148,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.starterSelectContainer.setVisible(true);
|
this.starterSelectContainer.setVisible(true);
|
||||||
|
|
||||||
this.starterPreferences = loadStarterPreferences();
|
this.starterPreferences = loadStarterPreferences();
|
||||||
this.originalStarterPreferences = loadStarterPreferences();
|
// Deep copy the JSON (avoid re-loading from disk)
|
||||||
|
this.originalStarterPreferences = deepCopy(this.starterPreferences);
|
||||||
|
|
||||||
this.allSpecies.forEach((species, s) => {
|
this.allSpecies.forEach((species, s) => {
|
||||||
const icon = this.starterContainers[s].icon;
|
const icon = this.starterContainers[s].icon;
|
||||||
@ -1212,6 +1213,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
preferences: StarterPreferences,
|
preferences: StarterPreferences,
|
||||||
ignoreChallenge = false,
|
ignoreChallenge = false,
|
||||||
): StarterAttributes {
|
): StarterAttributes {
|
||||||
|
// if preferences for the species is undefined, set it to an empty object
|
||||||
|
preferences[species.speciesId] ??= {};
|
||||||
const starterAttributes = preferences[species.speciesId];
|
const starterAttributes = preferences[species.speciesId];
|
||||||
const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge);
|
const { dexEntry, starterDataEntry: starterData } = this.getSpeciesData(species.speciesId, !ignoreChallenge);
|
||||||
|
|
||||||
@ -1828,9 +1831,15 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
// The persistent starter data to apply e.g. candy upgrades
|
// The persistent starter data to apply e.g. candy upgrades
|
||||||
const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId];
|
const persistentStarterData = globalScene.gameData.starterData[this.lastSpecies.speciesId];
|
||||||
// The sanitized starter preferences
|
// The sanitized starter preferences
|
||||||
let starterAttributes = this.starterPreferences[this.lastSpecies.speciesId];
|
if (this.starterPreferences[this.lastSpecies.speciesId] === undefined) {
|
||||||
// The original starter preferences
|
this.starterPreferences[this.lastSpecies.speciesId] = {};
|
||||||
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId];
|
}
|
||||||
|
if (this.originalStarterPreferences[this.lastSpecies.speciesId] === undefined) {
|
||||||
|
this.originalStarterPreferences[this.lastSpecies.speciesId] = {};
|
||||||
|
}
|
||||||
|
// Bangs are safe here due to the above check
|
||||||
|
const starterAttributes = this.starterPreferences[this.lastSpecies.speciesId]!;
|
||||||
|
const originalStarterAttributes = this.originalStarterPreferences[this.lastSpecies.speciesId]!;
|
||||||
|
|
||||||
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
|
// this gets the correct pokemon cursor depending on whether you're in the starter screen or the party icons
|
||||||
if (!this.starterIconsCursorObj.visible) {
|
if (!this.starterIconsCursorObj.visible) {
|
||||||
@ -2050,10 +2059,6 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
const option: OptionSelectItem = {
|
const option: OptionSelectItem = {
|
||||||
label: getNatureName(n, true, true, true, globalScene.uiTheme),
|
label: getNatureName(n, true, true, true, globalScene.uiTheme),
|
||||||
handler: () => {
|
handler: () => {
|
||||||
// update default nature in starter save data
|
|
||||||
if (!starterAttributes) {
|
|
||||||
starterAttributes = this.starterPreferences[this.lastSpecies.speciesId] = {};
|
|
||||||
}
|
|
||||||
starterAttributes.nature = n;
|
starterAttributes.nature = n;
|
||||||
originalStarterAttributes.nature = starterAttributes.nature;
|
originalStarterAttributes.nature = starterAttributes.nature;
|
||||||
this.clearText();
|
this.clearText();
|
||||||
@ -3408,8 +3413,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
if (species) {
|
if (species) {
|
||||||
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
|
const defaultDexAttr = this.getCurrentDexProps(species.speciesId);
|
||||||
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
const defaultProps = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||||
|
// Bang is correct due to the `?` before variant
|
||||||
const variant = this.starterPreferences[species.speciesId]?.variant
|
const variant = this.starterPreferences[species.speciesId]?.variant
|
||||||
? (this.starterPreferences[species.speciesId].variant as Variant)
|
? (this.starterPreferences[species.speciesId]!.variant as Variant)
|
||||||
: defaultProps.variant;
|
: defaultProps.variant;
|
||||||
const tint = getVariantTint(variant);
|
const tint = getVariantTint(variant);
|
||||||
this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint);
|
this.pokemonShinyIcon.setFrame(getVariantIcon(variant)).setTint(tint);
|
||||||
@ -3634,15 +3640,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
if (starterIndex > -1) {
|
if (starterIndex > -1) {
|
||||||
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
|
props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]);
|
||||||
this.setSpeciesDetails(species, {
|
this.setSpeciesDetails(
|
||||||
shiny: props.shiny,
|
species,
|
||||||
formIndex: props.formIndex,
|
{
|
||||||
female: props.female,
|
shiny: props.shiny,
|
||||||
variant: props.variant,
|
formIndex: props.formIndex,
|
||||||
abilityIndex: this.starterAbilityIndexes[starterIndex],
|
female: props.female,
|
||||||
natureIndex: this.starterNatures[starterIndex],
|
variant: props.variant,
|
||||||
teraType: this.starterTeras[starterIndex],
|
abilityIndex: this.starterAbilityIndexes[starterIndex],
|
||||||
});
|
natureIndex: this.starterNatures[starterIndex],
|
||||||
|
teraType: this.starterTeras[starterIndex],
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const defaultAbilityIndex =
|
const defaultAbilityIndex =
|
||||||
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
|
starterAttributes?.ability ?? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species);
|
||||||
@ -3659,15 +3669,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
props.formIndex = starterAttributes?.form ?? props.formIndex;
|
props.formIndex = starterAttributes?.form ?? props.formIndex;
|
||||||
props.female = starterAttributes?.female ?? props.female;
|
props.female = starterAttributes?.female ?? props.female;
|
||||||
|
|
||||||
this.setSpeciesDetails(species, {
|
this.setSpeciesDetails(
|
||||||
shiny: props.shiny,
|
species,
|
||||||
formIndex: props.formIndex,
|
{
|
||||||
female: props.female,
|
shiny: props.shiny,
|
||||||
variant: props.variant,
|
formIndex: props.formIndex,
|
||||||
abilityIndex: defaultAbilityIndex,
|
female: props.female,
|
||||||
natureIndex: defaultNature,
|
variant: props.variant,
|
||||||
teraType: starterAttributes?.tera,
|
abilityIndex: defaultAbilityIndex,
|
||||||
});
|
natureIndex: defaultNature,
|
||||||
|
teraType: starterAttributes?.tera,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNullOrUndefined(props.formIndex)) {
|
if (!isNullOrUndefined(props.formIndex)) {
|
||||||
@ -3704,15 +3718,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
|
const defaultNature = globalScene.gameData.getSpeciesDefaultNature(species);
|
||||||
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr);
|
||||||
|
|
||||||
this.setSpeciesDetails(species, {
|
this.setSpeciesDetails(
|
||||||
shiny: props.shiny,
|
species,
|
||||||
formIndex: props.formIndex,
|
{
|
||||||
female: props.female,
|
shiny: props.shiny,
|
||||||
variant: props.variant,
|
formIndex: props.formIndex,
|
||||||
abilityIndex: defaultAbilityIndex,
|
female: props.female,
|
||||||
natureIndex: defaultNature,
|
variant: props.variant,
|
||||||
forSeen: true,
|
abilityIndex: defaultAbilityIndex,
|
||||||
});
|
natureIndex: defaultNature,
|
||||||
|
forSeen: true,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
this.pokemonSprite.setTint(0x808080);
|
this.pokemonSprite.setTint(0x808080);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -3734,15 +3752,19 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
this.pokemonFormText.setVisible(false);
|
this.pokemonFormText.setVisible(false);
|
||||||
this.teraIcon.setVisible(false);
|
this.teraIcon.setVisible(false);
|
||||||
|
|
||||||
this.setSpeciesDetails(species!, {
|
this.setSpeciesDetails(
|
||||||
// TODO: is this bang correct?
|
species!,
|
||||||
shiny: false,
|
{
|
||||||
formIndex: 0,
|
// TODO: is this bang correct?
|
||||||
female: false,
|
shiny: false,
|
||||||
variant: 0,
|
formIndex: 0,
|
||||||
abilityIndex: 0,
|
female: false,
|
||||||
natureIndex: 0,
|
variant: 0,
|
||||||
});
|
abilityIndex: 0,
|
||||||
|
natureIndex: 0,
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
);
|
||||||
this.pokemonSprite.clearTint();
|
this.pokemonSprite.clearTint();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3764,7 +3786,7 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
|
return { dexEntry: { ...copiedDexEntry }, starterDataEntry: { ...copiedStarterDataEntry } };
|
||||||
}
|
}
|
||||||
|
|
||||||
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void {
|
setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}, save = true): void {
|
||||||
let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options;
|
let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options;
|
||||||
const forSeen: boolean = options.forSeen ?? false;
|
const forSeen: boolean = options.forSeen ?? false;
|
||||||
const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
|
const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null;
|
||||||
@ -4176,7 +4198,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
this.updateInstructions();
|
this.updateInstructions();
|
||||||
|
|
||||||
saveStarterPreferences(this.originalStarterPreferences);
|
if (save) {
|
||||||
|
saveStarterPreferences(this.originalStarterPreferences);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
|
setTypeIcons(type1: PokemonType | null, type2: PokemonType | null): void {
|
||||||
|
@ -8,7 +8,7 @@ import { AES, enc } from "crypto-js";
|
|||||||
* @param values - The object to be deep copied.
|
* @param values - The object to be deep copied.
|
||||||
* @returns A new object that is a deep copy of the input.
|
* @returns A new object that is a deep copy of the input.
|
||||||
*/
|
*/
|
||||||
export function deepCopy(values: object): object {
|
export function deepCopy<T extends object>(values: T): T {
|
||||||
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
|
// Convert the object to a JSON string and parse it back to an object to perform a deep copy
|
||||||
return JSON.parse(JSON.stringify(values));
|
return JSON.parse(JSON.stringify(values));
|
||||||
}
|
}
|
||||||
@ -58,13 +58,28 @@ export function decrypt(data: string, bypassLogin: boolean): string {
|
|||||||
return AES.decrypt(data, saveKey).toString(enc.Utf8);
|
return AES.decrypt(data, saveKey).toString(enc.Utf8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an object has no properties of its own (its shape is `{}`). An empty array is considered a bare object.
|
||||||
|
* @param obj - Object to check
|
||||||
|
* @returns - Whether the object is bare
|
||||||
|
*/
|
||||||
|
export function isBareObject(obj: any): boolean {
|
||||||
|
if (typeof obj !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (const _ in obj) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
// 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
|
// if they ever add private static variables, move this into StarterPrefs
|
||||||
const StarterPrefers_DEFAULT: string = "{}";
|
const StarterPrefers_DEFAULT: string = "{}";
|
||||||
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
|
let StarterPrefers_private_latest: string = StarterPrefers_DEFAULT;
|
||||||
|
|
||||||
export interface StarterPreferences {
|
export interface StarterPreferences {
|
||||||
[key: number]: StarterAttributes;
|
[key: number]: StarterAttributes | undefined;
|
||||||
}
|
}
|
||||||
// called on starter selection show once
|
// called on starter selection show once
|
||||||
|
|
||||||
@ -74,11 +89,17 @@ export function loadStarterPreferences(): StarterPreferences {
|
|||||||
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
|
localStorage.getItem(`starterPrefs_${loggedInUser?.username}`) || StarterPrefers_DEFAULT),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// called on starter selection clear, always
|
|
||||||
|
|
||||||
export function saveStarterPreferences(prefs: StarterPreferences): void {
|
export function saveStarterPreferences(prefs: StarterPreferences): void {
|
||||||
const pStr: string = JSON.stringify(prefs);
|
// Fastest way to check if an object has any properties (does no allocation)
|
||||||
|
if (isBareObject(prefs)) {
|
||||||
|
console.warn("Refusing to save empty starter preferences");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// no reason to store `{}` (for starters not customized)
|
||||||
|
const pStr: string = JSON.stringify(prefs, (_, value) => (isBareObject(value) ? undefined : value));
|
||||||
if (pStr !== StarterPrefers_private_latest) {
|
if (pStr !== StarterPrefers_private_latest) {
|
||||||
|
console.log("%cSaving starter preferences", "color: blue");
|
||||||
// something changed, store the update
|
// something changed, store the update
|
||||||
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
|
localStorage.setItem(`starterPrefs_${loggedInUser?.username}`, pStr);
|
||||||
// update the latest prefs
|
// update the latest prefs
|
||||||
|
39
test/utils/data.test.ts
Normal file
39
test/utils/data.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { deepCopy, isBareObject } from "#utils/data";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Utils - Data", () => {
|
||||||
|
describe("deepCopy", () => {
|
||||||
|
it("should create a deep copy of an object", () => {
|
||||||
|
const original = { a: 1, b: { c: 2 } };
|
||||||
|
const copy = deepCopy(original);
|
||||||
|
// ensure the references are different
|
||||||
|
expect(copy === original, "copied object should not compare equal").not;
|
||||||
|
expect(copy).toEqual(original);
|
||||||
|
// update copy's `a` to a different value and ensure original is unaffected
|
||||||
|
copy.a = 42;
|
||||||
|
expect(original.a, "adjusting property of copy should not affect original").toBe(1);
|
||||||
|
// update copy's nested `b.c` to a different value and ensure original is unaffected
|
||||||
|
copy.b.c = 99;
|
||||||
|
expect(original.b.c, "adjusting nested property of copy should not affect original").toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("isBareObject", () => {
|
||||||
|
it("should properly identify bare objects", () => {
|
||||||
|
expect(isBareObject({}), "{} should be considered bare");
|
||||||
|
expect(isBareObject(new Object()), "new Object() should be considered bare");
|
||||||
|
expect(isBareObject(Object.create(null)));
|
||||||
|
expect(isBareObject([]), "an empty array should be considered bare");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should properly reject non-objects", () => {
|
||||||
|
expect(isBareObject(new Date())).not;
|
||||||
|
expect(isBareObject(null)).not;
|
||||||
|
expect(isBareObject(42)).not;
|
||||||
|
expect(isBareObject("")).not;
|
||||||
|
expect(isBareObject(undefined)).not;
|
||||||
|
expect(isBareObject(() => {})).not;
|
||||||
|
expect(isBareObject(new (class A {})())).not;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user