diff --git a/src/data/layers/aca/c.tsx b/src/data/layers/aca/c.tsx index ebb2357..fee6975 100644 --- a/src/data/layers/aca/c.tsx +++ b/src/data/layers/aca/c.tsx @@ -19,7 +19,7 @@ import { createCumulativeConversion, createExponentialScaling } from "@/features/conversion"; -import { jsx, persistent, showIf, Visibility } from "@/features/feature"; +import { jsx, showIf, Visibility } from "@/features/feature"; import { createHotkey } from "@/features/hotkey"; import { createInfobox } from "@/features/infoboxes/infobox"; import { createMilestone } from "@/features/milestones/milestone"; @@ -32,6 +32,7 @@ import { createTabButton, createTabFamily } from "@/features/tabs/tabFamily"; import { createTree, createTreeNode, GenericTreeNode, TreeBranch } from "@/features/trees/tree"; import { createUpgrade } from "@/features/upgrades/upgrade"; import { createLayer } from "@/game/layers"; +import { persistent } from "@/game/persistence"; import settings from "@/game/settings"; import { DecimalSource } from "@/lib/break_eternity"; import Decimal, { format, formatWhole } from "@/util/bignum"; diff --git a/src/data/layers/aca/f.tsx b/src/data/layers/aca/f.tsx index 416dfec..0aa7a90 100644 --- a/src/data/layers/aca/f.tsx +++ b/src/data/layers/aca/f.tsx @@ -2,12 +2,13 @@ import { createLayerTreeNode, createResetButton } from "@/data/common"; import { main } from "@/data/mod"; import { createClickable } from "@/features/clickables/clickable"; import { createExponentialScaling, createIndependentConversion } from "@/features/conversion"; -import { jsx, persistent } from "@/features/feature"; +import { jsx } from "@/features/feature"; import { createInfobox } from "@/features/infoboxes/infobox"; import { createReset } from "@/features/reset"; import MainDisplay from "@/features/resources/MainDisplay.vue"; import { createResource, displayResource } from "@/features/resources/resource"; import { createLayer } from "@/game/layers"; +import { persistent } from "@/game/persistence"; import Decimal, { DecimalSource, formatWhole } from "@/util/bignum"; import { render } from "@/util/vue"; import c from "./c"; diff --git a/src/features/achievements/achievement.tsx b/src/features/achievements/achievement.tsx index 27279e3..cd9e403 100644 --- a/src/features/achievements/achievement.tsx +++ b/src/features/achievements/achievement.tsx @@ -5,9 +5,6 @@ import { findFeatures, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, @@ -15,6 +12,7 @@ import { } from "@/features/feature"; import { globalBus } from "@/game/events"; import "@/game/notifications"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; import { Computable, GetComputableType, diff --git a/src/features/boards/board.ts b/src/features/boards/board.ts index 2833721..1681011 100644 --- a/src/features/boards/board.ts +++ b/src/features/boards/board.ts @@ -4,16 +4,13 @@ import { findFeatures, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, - State, StyleValue, Visibility } from "@/features/feature"; import { globalBus } from "@/game/events"; +import { State, Persistent, makePersistent, PersistentState } from "@/game/persistence"; import Decimal, { DecimalSource } from "@/lib/break_eternity"; import { isFunction } from "@/util/common"; import { diff --git a/src/features/buyable.tsx b/src/features/buyable.tsx index 3df26e6..33ab2f9 100644 --- a/src/features/buyable.tsx +++ b/src/features/buyable.tsx @@ -1,5 +1,6 @@ import ClickableComponent from "@/features/clickables/Clickable.vue"; import { Resource } from "@/features/resources/resource"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum"; import { Computable, @@ -17,9 +18,6 @@ import { GatherProps, getUniqueID, jsx, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, diff --git a/src/features/challenges/challenge.ts b/src/features/challenges/challenge.ts index c8e9740..6406958 100644 --- a/src/features/challenges/challenge.ts +++ b/src/features/challenges/challenge.ts @@ -4,8 +4,6 @@ import { Component, GatherProps, getUniqueID, - persistent, - PersistentRef, Replace, setDefault, StyleValue, @@ -14,6 +12,7 @@ import { import { GenericReset } from "@/features/reset"; import { Resource } from "@/features/resources/resource"; import { globalBus } from "@/game/events"; +import { PersistentRef, persistent } from "@/game/persistence"; import settings from "@/game/settings"; import Decimal, { DecimalSource } from "@/util/bignum"; import { diff --git a/src/features/feature.ts b/src/features/feature.ts index 553e60e..2310dbe 100644 --- a/src/features/feature.ts +++ b/src/features/feature.ts @@ -1,37 +1,15 @@ -import { globalBus } from "@/game/events"; -import { GenericLayer } from "@/game/layers"; -import Decimal, { DecimalSource } from "@/util/bignum"; +import { DefaultValue } from "@/game/persistence"; +import Decimal from "@/util/bignum"; import { DoNotCache, ProcessedComputable } from "@/util/computed"; -import { ProxyState } from "@/util/proxies"; -import { isArray } from "@vue/shared"; -import { CSSProperties, DefineComponent, isRef, ref, Ref } from "vue"; +import { CSSProperties, DefineComponent, isRef } from "vue"; -export const PersistentState = Symbol("PersistentState"); -export const DefaultValue = Symbol("DefaultValue"); export const Component = Symbol("Component"); export const GatherProps = Symbol("GatherProps"); -// Note: This is a union of things that should be safely stringifiable without needing -// special processes for knowing what to load them in as -// - Decimals aren't allowed because we'd need to know to parse them back. -// - DecimalSources are allowed because the string is a valid value for them -export type State = - | string - | number - | boolean - | DecimalSource - | { [key: string]: State } - | { [key: number]: State }; export type JSXFunction = (() => JSX.Element) & { [DoNotCache]: true }; export type CoercableComponent = string | DefineComponent | JSXFunction; export type StyleValue = string | CSSProperties | Array; -export type Persistent = { - [PersistentState]: Ref; - [DefaultValue]: T; -}; -export type PersistentRef = Ref & Persistent; - // TODO if importing .vue components in .tsx can become type safe, // this type can probably be safely removed // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -70,27 +48,6 @@ export function showIf(condition: boolean, otherwise = Visibility.None): Visibil return condition ? Visibility.Visible : otherwise; } -export function persistent(defaultValue: T | Ref): PersistentRef { - const persistent = ( - isRef(defaultValue) ? defaultValue : (ref(defaultValue) as unknown) - ) as PersistentRef; - - persistent[PersistentState] = persistent; - persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; - return persistent as PersistentRef; -} - -export function makePersistent( - obj: unknown, - defaultValue: T -): asserts obj is Persistent { - const persistent = obj as Partial>; - const state = ref(defaultValue) as Ref; - - persistent[PersistentState] = state; - persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue; -} - export function setDefault( object: T, key: K, @@ -118,62 +75,3 @@ export function findFeatures(obj: Record, type: symbol): unknow handleObject(obj); return objects; } - -globalBus.on("addLayer", (layer: GenericLayer, saveData: Record) => { - const handleObject = (obj: Record, path: string[] = []): boolean => { - let foundPersistent = false; - Object.keys(obj).forEach(key => { - const value = obj[key]; - if (value && typeof value === "object") { - if (PersistentState in value) { - foundPersistent = true; - - // Construct save path if it doesn't exist - const persistentState = path.reduce>((acc, curr) => { - if (!(curr in acc)) { - acc[curr] = {}; - } - return acc[curr] as Record; - }, saveData); - - // Cache currently saved value - const savedValue = persistentState[key]; - // Add ref to save data - persistentState[key] = (value as Persistent)[PersistentState]; - // Load previously saved value - if (savedValue != null) { - (persistentState[key] as Ref).value = savedValue; - } else { - (persistentState[key] as Ref).value = (value as Persistent)[ - DefaultValue - ]; - } - } else if (!(value instanceof Decimal) && !isRef(value)) { - // Continue traversing - const foundPersistentInChild = handleObject(value as Record, [ - ...path, - key - ]); - - // Show warning for persistent values inside arrays - // TODO handle arrays better - if (foundPersistentInChild) { - if (isArray(value) && !isArray(obj)) { - console.warn( - "Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.", - ProxyState in obj - ? (obj as Record)[ProxyState] - : obj, - key - ); - } else { - foundPersistent = true; - } - } - } - } - }); - return foundPersistent; - }; - handleObject(layer); -}); diff --git a/src/features/grids/grid.ts b/src/features/grids/grid.ts index 8081642..3c6ab97 100644 --- a/src/features/grids/grid.ts +++ b/src/features/grids/grid.ts @@ -4,12 +4,8 @@ import { Component, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, - State, StyleValue, Visibility } from "@/features/feature"; @@ -23,6 +19,7 @@ import { } from "@/util/computed"; import { createLazyProxy } from "@/util/proxies"; import { computed, Ref, unref } from "vue"; +import { State, Persistent, makePersistent, PersistentState } from "@/game/persistence"; export const GridType = Symbol("Grid"); diff --git a/src/features/infoboxes/infobox.ts b/src/features/infoboxes/infobox.ts index d86a22e..226b40e 100644 --- a/src/features/infoboxes/infobox.ts +++ b/src/features/infoboxes/infobox.ts @@ -4,9 +4,6 @@ import { Component, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, @@ -21,6 +18,7 @@ import { } from "@/util/computed"; import { createLazyProxy } from "@/util/proxies"; import { Ref } from "vue"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; export const InfoboxType = Symbol("Infobox"); diff --git a/src/features/milestones/milestone.tsx b/src/features/milestones/milestone.tsx index ea9a3c8..31819fa 100644 --- a/src/features/milestones/milestone.tsx +++ b/src/features/milestones/milestone.tsx @@ -5,9 +5,6 @@ import { findFeatures, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, @@ -28,6 +25,7 @@ import { coerceComponent, isCoercableComponent } from "@/util/vue"; import { Unsubscribe } from "nanoevents"; import { computed, Ref, unref } from "vue"; import { useToast } from "vue-toastification"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; export const MilestoneType = Symbol("Milestone"); diff --git a/src/features/resources/resource.ts b/src/features/resources/resource.ts index 42ba77e..c1f956a 100644 --- a/src/features/resources/resource.ts +++ b/src/features/resources/resource.ts @@ -1,7 +1,7 @@ -import { persistent, State } from "@/features/feature"; import Decimal, { DecimalSource, format, formatWhole } from "@/util/bignum"; import { computed, ComputedRef, ref, Ref, watch } from "vue"; import { globalBus } from "@/game/events"; +import { State, persistent } from "@/game/persistence"; export interface Resource extends Ref { displayName: string; diff --git a/src/features/tabs/tabFamily.ts b/src/features/tabs/tabFamily.ts index 871f7bd..c92d428 100644 --- a/src/features/tabs/tabFamily.ts +++ b/src/features/tabs/tabFamily.ts @@ -3,9 +3,6 @@ import { Component, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, @@ -13,6 +10,7 @@ import { } from "@/features/feature"; import TabButtonComponent from "@/features/tabs/TabButton.vue"; import TabFamilyComponent from "@/features/tabs/TabFamily.vue"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; import { Computable, GetComputableType, diff --git a/src/features/trees/tree.ts b/src/features/trees/tree.ts index 8b634e9..e9be03b 100644 --- a/src/features/trees/tree.ts +++ b/src/features/trees/tree.ts @@ -3,7 +3,6 @@ import { Component, GatherProps, getUniqueID, - persistent, Replace, setDefault, StyleValue, @@ -14,6 +13,7 @@ import { GenericReset } from "@/features/reset"; import { displayResource, Resource } from "@/features/resources/resource"; import { Tooltip } from "@/features/tooltip"; import TreeComponent from "@/features/trees/Tree.vue"; +import { persistent } from "@/game/persistence"; import { DecimalSource, format } from "@/util/bignum"; import Decimal, { formatWhole } from "@/util/break_eternity"; import { diff --git a/src/features/upgrades/upgrade.ts b/src/features/upgrades/upgrade.ts index 1036c9a..a9f96c2 100644 --- a/src/features/upgrades/upgrade.ts +++ b/src/features/upgrades/upgrade.ts @@ -5,9 +5,6 @@ import { findFeatures, GatherProps, getUniqueID, - makePersistent, - Persistent, - PersistentState, Replace, setDefault, StyleValue, @@ -26,6 +23,7 @@ import { } from "@/util/computed"; import { createLazyProxy } from "@/util/proxies"; import { computed, Ref, unref } from "vue"; +import { Persistent, makePersistent, PersistentState } from "@/game/persistence"; export const UpgradeType = Symbol("Upgrade"); @@ -89,9 +87,9 @@ export function createUpgrade( upgrade.type = UpgradeType; upgrade[Component] = UpgradeComponent; - if (upgrade.canPurchase == null && (upgrade.resource == null || upgrade.cost == null)) { + if (upgrade.canAfford == null && (upgrade.resource == null || upgrade.cost == null)) { console.warn( - "Error: can't create upgrade without a canPurchase property or a resource and cost property", + "Error: can't create upgrade without a canAfford property or a resource and cost property", upgrade ); } diff --git a/src/game/layers.ts b/src/game/layers.ts index 0d634b9..90e2bf5 100644 --- a/src/game/layers.ts +++ b/src/game/layers.ts @@ -1,11 +1,4 @@ -import { - CoercableComponent, - persistent, - PersistentRef, - Replace, - setDefault, - StyleValue -} from "@/features/feature"; +import { CoercableComponent, Replace, setDefault, StyleValue } from "@/features/feature"; import { Link } from "@/features/links"; import Decimal from "@/util/bignum"; import { @@ -18,6 +11,7 @@ import { import { createLazyProxy } from "@/util/proxies"; import { createNanoEvents, Emitter } from "nanoevents"; import { globalBus } from "./events"; +import { persistent, PersistentRef } from "./persistence"; import player from "./player"; export interface LayerEvents { diff --git a/src/game/persistence.ts b/src/game/persistence.ts new file mode 100644 index 0000000..f8d2e80 --- /dev/null +++ b/src/game/persistence.ts @@ -0,0 +1,107 @@ +import { globalBus } from "@/game/events"; +import Decimal, { DecimalSource } from "@/util/bignum"; +import { ProxyState } from "@/util/proxies"; +import { isArray } from "@vue/shared"; +import { isRef, Ref, ref } from "vue"; +import { GenericLayer } from "./layers"; + +export const PersistentState = Symbol("PersistentState"); +export const DefaultValue = Symbol("DefaultValue"); + +// Note: This is a union of things that should be safely stringifiable without needing +// special processes for knowing what to load them in as +// - Decimals aren't allowed because we'd need to know to parse them back. +// - DecimalSources are allowed because the string is a valid value for them +export type State = + | string + | number + | boolean + | DecimalSource + | { [key: string]: State } + | { [key: number]: State }; + +export type Persistent = { + [PersistentState]: Ref; + [DefaultValue]: T; +}; +export type PersistentRef = Ref & Persistent; + +export function persistent(defaultValue: T | Ref): PersistentRef { + const persistent = ( + isRef(defaultValue) ? defaultValue : (ref(defaultValue) as unknown) + ) as PersistentRef; + + persistent[PersistentState] = persistent; + persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; + return persistent as PersistentRef; +} + +export function makePersistent( + obj: unknown, + defaultValue: T +): asserts obj is Persistent { + const persistent = obj as Partial>; + const state = ref(defaultValue) as Ref; + + persistent[PersistentState] = state; + persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue; +} + +globalBus.on("addLayer", (layer: GenericLayer, saveData: Record) => { + const handleObject = (obj: Record, path: string[] = []): boolean => { + let foundPersistent = false; + Object.keys(obj).forEach(key => { + const value = obj[key]; + if (value && typeof value === "object") { + if (PersistentState in value) { + foundPersistent = true; + + // Construct save path if it doesn't exist + const persistentState = path.reduce>((acc, curr) => { + if (!(curr in acc)) { + acc[curr] = {}; + } + return acc[curr] as Record; + }, saveData); + + // Cache currently saved value + const savedValue = persistentState[key]; + // Add ref to save data + persistentState[key] = (value as Persistent)[PersistentState]; + // Load previously saved value + if (savedValue != null) { + (persistentState[key] as Ref).value = savedValue; + } else { + (persistentState[key] as Ref).value = (value as Persistent)[ + DefaultValue + ]; + } + } else if (!(value instanceof Decimal) && !isRef(value)) { + // Continue traversing + const foundPersistentInChild = handleObject(value as Record, [ + ...path, + key + ]); + + // Show warning for persistent values inside arrays + // TODO handle arrays better + if (foundPersistentInChild) { + if (isArray(value) && !isArray(obj)) { + console.warn( + "Found array that contains persistent values when adding layer. Keep in mind changing the order of elements in the array will mess with existing player saves.", + ProxyState in obj + ? (obj as Record)[ProxyState] + : obj, + key + ); + } else { + foundPersistent = true; + } + } + } + } + }); + return foundPersistent; + }; + handleObject(layer); +});