Persistence rework

- Removed makePersistent
- Removed Persistent, renamed PersistentRef to Persistent
- createLazyProxy now takes optional base object
  - For use where previously makePersistent would be used
- Added warnings when creating refs outside a layer
- Added warnings when persistent refs aren't included in their layer object
- createLayer now takes id as a parameter rather than an option
This commit is contained in:
thepaperpilot 2022-04-05 22:16:40 -05:00
parent bd084e51c5
commit 5a59aaf4fc
13 changed files with 296 additions and 214 deletions

View file

@ -10,7 +10,7 @@ import {
Visibility Visibility
} from "features/feature"; } from "features/feature";
import "game/notifications"; import "game/notifications";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -69,9 +69,10 @@ export type GenericAchievement = Replace<
export function createAchievement<T extends AchievementOptions>( export function createAchievement<T extends AchievementOptions>(
optionsFunc: () => T & ThisType<Achievement<T>> optionsFunc: () => T & ThisType<Achievement<T>>
): Achievement<T> { ): Achievement<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const achievement: T & Partial<BaseAchievement> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
makePersistent<boolean>(achievement, false); const temp = Object.assign(persistent, optionsFunc());
const achievement: Partial<BaseAchievement> & typeof temp = temp;
achievement.id = getUniqueID("achievement-"); achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType; achievement.type = AchievementType;
achievement[Component] = AchievementComponent; achievement[Component] = AchievementComponent;
@ -122,5 +123,5 @@ export function createAchievement<T extends AchievementOptions>(
} }
return achievement as unknown as Achievement<T>; return achievement as unknown as Achievement<T>;
}); }, persistent<boolean>(false));
} }

View file

@ -10,7 +10,7 @@ import {
Visibility Visibility
} from "features/feature"; } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence"; import { State, Persistent, PersistentState, persistent } from "game/persistence";
import { isFunction } from "util/common"; import { isFunction } from "util/common";
import { import {
Computable, Computable,
@ -199,135 +199,142 @@ export type GenericBoard = Replace<
export function createBoard<T extends BoardOptions>( export function createBoard<T extends BoardOptions>(
optionsFunc: () => T & ThisType<Board<T>> optionsFunc: () => T & ThisType<Board<T>>
): Board<T> { ): Board<T> {
return createLazyProxy(() => { return createLazyProxy(
const board: T & Partial<BaseBoard> = optionsFunc(); persistent => {
makePersistent<BoardData>(board, { // Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const board: Partial<BaseBoard> & typeof temp = temp;
board.id = getUniqueID("board-");
board.type = BoardType;
board[Component] = BoardComponent;
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
processedBoard.nodes.value.find(
node => node.id === board[PersistentState].value.selectedNode
) || null
);
board.selectedAction = computed(() => {
const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null;
}
const type = processedBoard.types[selectedNode.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === processedBoard[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (processedBoard.selectedAction.value == null) {
return null;
}
if (
processedBoard.selectedAction.value.links &&
processedBoard.selectedNode.value
) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
board[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
board[PersistentState].value.selectedNode = node.id;
});
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
}
}
}
board[GatherProps] = function (this: GenericBoard) {
const {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
} = this;
return {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style: unref(style),
classes,
links,
selectedAction,
selectedNode
};
};
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
},
persistent<BoardData>({
nodes: [], nodes: [],
selectedNode: null, selectedNode: null,
selectedAction: null selectedAction: null
}); })
board.id = getUniqueID("board-"); );
board.type = BoardType;
board[Component] = BoardComponent;
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
processedBoard.nodes.value.find(
node => node.id === board[PersistentState].value.selectedNode
) || null
);
board.selectedAction = computed(() => {
const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null;
}
const type = processedBoard.types[selectedNode.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === processedBoard[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (processedBoard.selectedAction.value == null) {
return null;
}
if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
board[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
board[PersistentState].value.selectedNode = node.id;
});
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
}
}
}
board[GatherProps] = function (this: GenericBoard) {
const {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
} = this;
return {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style: unref(style),
classes,
links,
selectedAction,
selectedNode
};
};
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
});
} }
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T { export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {

View file

@ -1,6 +1,6 @@
import ClickableComponent from "features/clickables/Clickable.vue"; import ClickableComponent from "features/clickables/Clickable.vue";
import { Resource } from "features/resources/resource"; import { Resource } from "features/resources/resource";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import { import {
Computable, Computable,
@ -89,8 +89,10 @@ export type GenericBuyable = Replace<
export function createBuyable<T extends BuyableOptions>( export function createBuyable<T extends BuyableOptions>(
optionsFunc: () => T & ThisType<Buyable<T>> optionsFunc: () => T & ThisType<Buyable<T>>
): Buyable<T> { ): Buyable<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const buyable: T & Partial<BaseBuyable> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
const temp = Object.assign(persistent, optionsFunc());
const buyable: Partial<BaseBuyable> & typeof temp = temp;
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) { if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn( console.warn(
@ -100,7 +102,6 @@ export function createBuyable<T extends BuyableOptions>(
throw "Cannot create buyable without a canPurchase property or a resource and cost property"; throw "Cannot create buyable without a canPurchase property or a resource and cost property";
} }
makePersistent<DecimalSource>(buyable, 0);
buyable.id = getUniqueID("buyable-"); buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType; buyable.type = BuyableType;
buyable[Component] = ClickableComponent; buyable[Component] = ClickableComponent;
@ -239,5 +240,5 @@ export function createBuyable<T extends BuyableOptions>(
}; };
return buyable as unknown as Buyable<T>; return buyable as unknown as Buyable<T>;
}); }, persistent<DecimalSource>(0));
} }

View file

@ -15,7 +15,7 @@ import {
import { GenericReset } from "features/reset"; import { GenericReset } from "features/reset";
import { Resource } from "features/resources/resource"; import { Resource } from "features/resources/resource";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { persistent, PersistentRef } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { import {
@ -58,10 +58,10 @@ export interface ChallengeOptions {
export interface BaseChallenge { export interface BaseChallenge {
id: string; id: string;
completions: PersistentRef<DecimalSource>; completions: Persistent<DecimalSource>;
completed: Ref<boolean>; completed: Ref<boolean>;
maxed: Ref<boolean>; maxed: Ref<boolean>;
active: PersistentRef<boolean>; active: Persistent<boolean>;
toggle: VoidFunction; toggle: VoidFunction;
complete: (remainInChallenge?: boolean) => void; complete: (remainInChallenge?: boolean) => void;
type: typeof ChallengeType; type: typeof ChallengeType;
@ -98,6 +98,8 @@ export type GenericChallenge = Replace<
export function createChallenge<T extends ChallengeOptions>( export function createChallenge<T extends ChallengeOptions>(
optionsFunc: () => T & ThisType<Challenge<T>> optionsFunc: () => T & ThisType<Challenge<T>>
): Challenge<T> { ): Challenge<T> {
const completions = persistent(0);
const active = persistent(false);
return createLazyProxy(() => { return createLazyProxy(() => {
const challenge: T & Partial<BaseChallenge> = optionsFunc(); const challenge: T & Partial<BaseChallenge> = optionsFunc();
@ -116,8 +118,8 @@ export function createChallenge<T extends ChallengeOptions>(
challenge.type = ChallengeType; challenge.type = ChallengeType;
challenge[Component] = ChallengeComponent; challenge[Component] = ChallengeComponent;
challenge.completions = persistent(0); challenge.completions = completions;
challenge.active = persistent(false); challenge.active = active;
challenge.completed = computed(() => challenge.completed = computed(() =>
Decimal.gt((challenge as GenericChallenge).completions.value, 0) Decimal.gt((challenge as GenericChallenge).completions.value, 0)
); );

View file

@ -19,7 +19,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence"; import { State, Persistent, PersistentState, persistent } from "game/persistence";
export const GridType = Symbol("Grid"); export const GridType = Symbol("Grid");
@ -243,9 +243,10 @@ export type GenericGrid = Replace<
export function createGrid<T extends GridOptions>( export function createGrid<T extends GridOptions>(
optionsFunc: () => T & ThisType<Grid<T>> optionsFunc: () => T & ThisType<Grid<T>>
): Grid<T> { ): Grid<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const grid: T & Partial<BaseGrid> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
makePersistent(grid, {}); const temp = Object.assign(persistent, optionsFunc());
const grid: Partial<BaseGrid> & typeof temp = temp;
grid.id = getUniqueID("grid-"); grid.id = getUniqueID("grid-");
grid[Component] = GridComponent; grid[Component] = GridComponent;
@ -301,5 +302,5 @@ export function createGrid<T extends GridOptions>(
}; };
return grid as unknown as Grid<T>; return grid as unknown as Grid<T>;
}); }, persistent({}));
} }

View file

@ -18,7 +18,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Ref, unref } from "vue"; import { Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
export const InfoboxType = Symbol("Infobox"); export const InfoboxType = Symbol("Infobox");
@ -65,9 +65,10 @@ export type GenericInfobox = Replace<
export function createInfobox<T extends InfoboxOptions>( export function createInfobox<T extends InfoboxOptions>(
optionsFunc: () => T & ThisType<Infobox<T>> optionsFunc: () => T & ThisType<Infobox<T>>
): Infobox<T> { ): Infobox<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const infobox: T & Partial<BaseInfobox> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
makePersistent<boolean>(infobox, false); const temp = Object.assign(persistent, optionsFunc());
const infobox: Partial<BaseInfobox> & typeof temp = temp;
infobox.id = getUniqueID("infobox-"); infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType; infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent; infobox[Component] = InfoboxComponent;
@ -112,5 +113,5 @@ export function createInfobox<T extends InfoboxOptions>(
}; };
return infobox as unknown as Infobox<T>; return infobox as unknown as Infobox<T>;
}); }, persistent<boolean>(false));
} }

View file

@ -13,7 +13,7 @@ import {
import MilestoneComponent from "features/milestones/Milestone.vue"; import MilestoneComponent from "features/milestones/Milestone.vue";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import "game/notifications"; import "game/notifications";
import { makePersistent, Persistent, PersistentState } from "game/persistence"; import { persistent, Persistent, PersistentState } from "game/persistence";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common"; import { camelToTitle } from "util/common";
import { import {
@ -85,9 +85,10 @@ export type GenericMilestone = Replace<
export function createMilestone<T extends MilestoneOptions>( export function createMilestone<T extends MilestoneOptions>(
optionsFunc: () => T & ThisType<Milestone<T>> optionsFunc: () => T & ThisType<Milestone<T>>
): Milestone<T> { ): Milestone<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const milestone: T & Partial<BaseMilestone> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
makePersistent<boolean>(milestone, false); const temp = Object.assign(persistent, optionsFunc());
const milestone: Partial<BaseMilestone> & typeof temp = temp;
milestone.id = getUniqueID("milestone-"); milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType; milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent; milestone[Component] = MilestoneComponent;
@ -168,7 +169,7 @@ export function createMilestone<T extends MilestoneOptions>(
} }
return milestone as unknown as Milestone<T>; return milestone as unknown as Milestone<T>;
}); }, persistent<boolean>(false));
} }
declare module "game/settings" { declare module "game/settings" {

View file

@ -10,7 +10,7 @@ import {
} from "features/feature"; } from "features/feature";
import TabButtonComponent from "features/tabs/TabButton.vue"; import TabButtonComponent from "features/tabs/TabButton.vue";
import TabFamilyComponent from "features/tabs/TabFamily.vue"; import TabFamilyComponent from "features/tabs/TabFamily.vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -60,13 +60,13 @@ export type GenericTabButton = Replace<
export interface TabFamilyOptions { export interface TabFamilyOptions {
visibility?: Computable<Visibility>; visibility?: Computable<Visibility>;
tabs: Record<string, TabButtonOptions>;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
style?: Computable<StyleValue>; style?: Computable<StyleValue>;
} }
export interface BaseTabFamily extends Persistent<string> { export interface BaseTabFamily extends Persistent<string> {
id: string; id: string;
tabs: Record<string, TabButtonOptions>;
activeTab: Ref<GenericTab | CoercableComponent | null>; activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>; selected: Ref<string>;
type: typeof TabFamilyType; type: typeof TabFamilyType;
@ -90,21 +90,41 @@ export type GenericTabFamily = Replace<
>; >;
export function createTabFamily<T extends TabFamilyOptions>( export function createTabFamily<T extends TabFamilyOptions>(
tabs: Record<string, () => TabButtonOptions>,
optionsFunc: () => T & ThisType<TabFamily<T>> optionsFunc: () => T & ThisType<TabFamily<T>>
): TabFamily<T> { ): TabFamily<T> {
return createLazyProxy(() => { if (Object.keys(tabs).length === 0) {
const tabFamily: T & Partial<BaseTabFamily> = optionsFunc(); console.warn("Cannot create tab family with 0 tabs");
throw "Cannot create tab family with 0 tabs";
}
if (Object.keys(tabFamily.tabs).length === 0) { return createLazyProxy(persistent => {
console.warn("Cannot create tab family with 0 tabs", tabFamily); // Create temp literally just to avoid explicitly assigning types
throw "Cannot create tab family with 0 tabs"; const temp = Object.assign(persistent, optionsFunc());
} const tabFamily: Partial<BaseTabFamily> & typeof temp = temp;
tabFamily.id = getUniqueID("tabFamily-"); tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType; tabFamily.type = TabFamilyType;
tabFamily[Component] = TabFamilyComponent; tabFamily[Component] = TabFamilyComponent;
makePersistent<string>(tabFamily, Object.keys(tabFamily.tabs)[0]); tabFamily.tabs = Object.keys(tabs).reduce<Record<string, GenericTabButton>>(
(parsedTabs, tab) => {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabs[tab]();
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
parsedTabs[tab] = tabButton as GenericTabButton;
return parsedTabs;
},
{}
);
tabFamily.selected = tabFamily[PersistentState]; tabFamily.selected = tabFamily[PersistentState];
tabFamily.activeTab = computed(() => { tabFamily.activeTab = computed(() => {
const tabs = unref(processedTabFamily.tabs); const tabs = unref(processedTabFamily.tabs);
@ -129,20 +149,6 @@ export function createTabFamily<T extends TabFamilyOptions>(
processComputable(tabFamily as T, "classes"); processComputable(tabFamily as T, "classes");
processComputable(tabFamily as T, "style"); processComputable(tabFamily as T, "style");
for (const tab in tabFamily.tabs) {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabFamily.tabs[tab];
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
}
tabFamily[GatherProps] = function (this: GenericTabFamily) { tabFamily[GatherProps] = function (this: GenericTabFamily) {
const { visibility, activeTab, selected, tabs, style, classes } = this; const { visibility, activeTab, selected, tabs, style, classes } = this;
return { visibility, activeTab, selected, tabs, style: unref(style), classes }; return { visibility, activeTab, selected, tabs, style: unref(style), classes };
@ -151,5 +157,5 @@ export function createTabFamily<T extends TabFamilyOptions>(
// This is necessary because board.types is different from T and TabFamily // This is necessary because board.types is different from T and TabFamily
const processedTabFamily = tabFamily as unknown as TabFamily<T>; const processedTabFamily = tabFamily as unknown as TabFamily<T>;
return processedTabFamily; return processedTabFamily;
}); }, persistent(Object.keys(tabs)[0]));
} }

View file

@ -13,7 +13,7 @@ import { GenericReset } from "features/reset";
import { displayResource, Resource } from "features/resources/resource"; import { displayResource, Resource } from "features/resources/resource";
import { Tooltip } from "features/tooltip"; import { Tooltip } from "features/tooltip";
import TreeComponent from "features/trees/Tree.vue"; import TreeComponent from "features/trees/Tree.vue";
import { persistent } from "game/persistence"; import { deletePersistent, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import { import {
Computable, Computable,
@ -76,16 +76,18 @@ export type GenericTreeNode = Replace<
export function createTreeNode<T extends TreeNodeOptions>( export function createTreeNode<T extends TreeNodeOptions>(
optionsFunc: () => T & ThisType<TreeNode<T>> optionsFunc: () => T & ThisType<TreeNode<T>>
): TreeNode<T> { ): TreeNode<T> {
const forceTooltip = persistent(false);
return createLazyProxy(() => { return createLazyProxy(() => {
const treeNode: T & Partial<BaseTreeNode> = optionsFunc(); const treeNode: T & Partial<BaseTreeNode> = optionsFunc();
treeNode.id = getUniqueID("treeNode-"); treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType; treeNode.type = TreeNodeType;
if (treeNode.tooltip) { if (treeNode.tooltip) {
treeNode.forceTooltip = persistent(false); treeNode.forceTooltip = forceTooltip;
} else { } else {
// If we don't have a tooltip, no point in making this persistent // If we don't have a tooltip, no point in making this persistent
treeNode.forceTooltip = ref(false); treeNode.forceTooltip = ref(false);
deletePersistent(forceTooltip);
} }
processComputable(treeNode as T, "visibility"); processComputable(treeNode as T, "visibility");

View file

@ -23,7 +23,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { persistent, Persistent, PersistentState } from "game/persistence";
export const UpgradeType = Symbol("Upgrade"); export const UpgradeType = Symbol("Upgrade");
@ -80,9 +80,10 @@ export type GenericUpgrade = Replace<
export function createUpgrade<T extends UpgradeOptions>( export function createUpgrade<T extends UpgradeOptions>(
optionsFunc: () => T & ThisType<Upgrade<T>> optionsFunc: () => T & ThisType<Upgrade<T>>
): Upgrade<T> { ): Upgrade<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const upgrade: T & Partial<BaseUpgrade> = optionsFunc(); // Create temp literally just to avoid explicitly assigning types
makePersistent<boolean>(upgrade, false); const temp = Object.assign(persistent, optionsFunc());
const upgrade: Partial<BaseUpgrade> & typeof temp = temp;
upgrade.id = getUniqueID("upgrade-"); upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType; upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent; upgrade[Component] = UpgradeComponent;
@ -167,7 +168,7 @@ export function createUpgrade<T extends UpgradeOptions>(
}; };
return upgrade as unknown as Upgrade<T>; return upgrade as unknown as Upgrade<T>;
}); }, persistent<boolean>(false));
} }
export function setupAutoPurchase( export function setupAutoPurchase(

View file

@ -18,7 +18,7 @@ import { createLazyProxy } from "util/proxies";
import { createNanoEvents, Emitter } from "nanoevents"; import { createNanoEvents, Emitter } from "nanoevents";
import { InjectionKey, Ref, ref, unref } from "vue"; import { InjectionKey, Ref, ref, unref } from "vue";
import { globalBus } from "./events"; import { globalBus } from "./events";
import { persistent, PersistentRef } from "./persistence"; import { Persistent, persistent } from "./persistence";
import player from "./player"; import player from "./player";
export interface FeatureNode { export interface FeatureNode {
@ -58,7 +58,6 @@ export interface Position {
} }
export interface LayerOptions { export interface LayerOptions {
id: string;
color?: Computable<string>; color?: Computable<string>;
display: Computable<CoercableComponent>; display: Computable<CoercableComponent>;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
@ -70,7 +69,8 @@ export interface LayerOptions {
} }
export interface BaseLayer { export interface BaseLayer {
minimized: PersistentRef<boolean>; id: string;
minimized: Persistent<boolean>;
emitter: Emitter<LayerEvents>; emitter: Emitter<LayerEvents>;
on: OmitThisParameter<Emitter<LayerEvents>["on"]>; on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void; emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void;
@ -84,7 +84,7 @@ export type Layer<T extends LayerOptions> = Replace<
display: GetComputableType<T["display"]>; display: GetComputableType<T["display"]>;
classes: GetComputableType<T["classes"]>; classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>; style: GetComputableType<T["style"]>;
name: GetComputableTypeWithDefault<T["name"], T["id"]>; name: GetComputableTypeWithDefault<T["name"], string>;
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>; minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>; minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
forceHideGoBack: GetComputableType<T["forceHideGoBack"]>; forceHideGoBack: GetComputableType<T["forceHideGoBack"]>;
@ -100,7 +100,10 @@ export type GenericLayer = Replace<
} }
>; >;
export const persistentRefs: Record<string, Set<Persistent>> = {};
export const addingLayers: string[] = [];
export function createLayer<T extends LayerOptions>( export function createLayer<T extends LayerOptions>(
id: string,
optionsFunc: (() => T) & ThisType<BaseLayer> optionsFunc: (() => T) & ThisType<BaseLayer>
): Layer<T> { ): Layer<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
@ -109,10 +112,19 @@ export function createLayer<T extends LayerOptions>(
layer.on = emitter.on.bind(emitter); layer.on = emitter.on.bind(emitter);
layer.emit = emitter.emit.bind(emitter); layer.emit = emitter.emit.bind(emitter);
layer.nodes = ref({}); layer.nodes = ref({});
layer.id = id;
addingLayers.push(id);
persistentRefs[id] = new Set();
layer.minimized = persistent(false); layer.minimized = persistent(false);
Object.assign(layer, optionsFunc.call(layer)); Object.assign(layer, optionsFunc.call(layer));
if (
addingLayers[addingLayers.length - 1] == null ||
addingLayers[addingLayers.length - 1] !== id
) {
throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`;
}
addingLayers.pop();
processComputable(layer as T, "color"); processComputable(layer as T, "color");
processComputable(layer as T, "display"); processComputable(layer as T, "display");

View file

@ -3,10 +3,12 @@ import Decimal, { DecimalSource } from "util/bignum";
import { ProxyState } from "util/proxies"; import { ProxyState } from "util/proxies";
import { isArray } from "@vue/shared"; import { isArray } from "@vue/shared";
import { isReactive, isRef, Ref, ref } from "vue"; import { isReactive, isRef, Ref, ref } from "vue";
import { GenericLayer } from "./layers"; import { addingLayers, GenericLayer, persistentRefs } from "./layers";
export const PersistentState = Symbol("PersistentState"); export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue"); export const DefaultValue = Symbol("DefaultValue");
export const StackTrace = Symbol("StackTrace");
export const Deleted = Symbol("Deleted");
// Note: This is a union of things that should be safely stringifiable without needing // Note: This is a union of things that should be safely stringifiable without needing
// special processes for knowing what to load them in as // special processes for knowing what to load them in as
@ -20,31 +22,52 @@ export type State =
| { [key: string]: State } | { [key: string]: State }
| { [key: number]: State }; | { [key: number]: State };
export type Persistent<T extends State = State> = { export type Persistent<T extends State = State> = Ref<T> & {
[PersistentState]: Ref<T>; [PersistentState]: Ref<T>;
[DefaultValue]: T; [DefaultValue]: T;
[StackTrace]: string;
[Deleted]: boolean;
}; };
export type PersistentRef<T extends State = State> = Ref<T> & Persistent<T>;
export function persistent<T extends State>(defaultValue: T | Ref<T>): PersistentRef<T> { function getStackTrace() {
return (
new Error().stack
?.split("\n")
.slice(3, 5)
.map(line => line.trim())
.join("\n") || ""
);
}
export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
const persistent = ( const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown) isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as PersistentRef<T>; ) as Persistent<T>;
persistent[PersistentState] = persistent; persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
return persistent as PersistentRef<T>; persistent[StackTrace] = getStackTrace();
persistent[Deleted] = false;
if (addingLayers.length === 0) {
console.warn(
"Creating a persistent ref outside of a layer. This is not officially supported",
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
} else {
persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent);
}
return persistent as Persistent<T>;
} }
export function makePersistent<T extends State>( export function deletePersistent(persistent: Persistent) {
obj: unknown, if (addingLayers.length === 0) {
defaultValue: T console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent);
): asserts obj is Persistent<T> { }
const persistent = obj as Partial<Persistent<T>>; persistentRefs[addingLayers[addingLayers.length - 1]].delete(persistent);
const state = ref(defaultValue) as Ref<T>; persistent[Deleted] = true;
persistent[PersistentState] = state;
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
} }
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => { globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
@ -56,6 +79,19 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
if (value && typeof value === "object") { if (value && typeof value === "object") {
if (PersistentState in value) { if (PersistentState in value) {
foundPersistent = true; foundPersistent = true;
if ((value as Persistent)[Deleted]) {
console.warn(
"Deleted persistent ref present in returned object. Ignoring...",
value,
"\nCreated at:\n" + (value as Persistent)[StackTrace]
);
return;
}
persistentRefs[layer.id].delete(
ProxyState in value
? ((value as any)[ProxyState] as Persistent)
: (value as Persistent)
);
// Construct save path if it doesn't exist // Construct save path if it doesn't exist
const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => { const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => {
@ -122,4 +158,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
return foundPersistent; return foundPersistent;
}; };
handleObject(layer); handleObject(layer);
persistentRefs[layer.id].forEach(persistent => {
console.error(
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
});
persistentRefs[layer.id].clear();
}); });

View file

@ -17,15 +17,18 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
// Takes a function that returns an object and pretends to be that object // Takes a function that returns an object and pretends to be that object
// Note that the object is lazily calculated // Note that the object is lazily calculated
export function createLazyProxy<T extends object>(objectFunc: () => T): T { export function createLazyProxy<T extends object, S>(
const obj: T | Record<string, never> = {}; objectFunc: (baseObject: S) => T & S,
baseObject: S = {} as S
): T {
const obj: S & Partial<T> = baseObject;
let calculated = false; let calculated = false;
function calculateObj(): T { function calculateObj(): T {
if (!calculated) { if (!calculated) {
Object.assign(obj, objectFunc()); Object.assign(obj, objectFunc(obj));
calculated = true; calculated = true;
} }
return obj as T; return obj as S & T;
} }
return new Proxy(obj, { return new Proxy(obj, {
@ -51,10 +54,10 @@ export function createLazyProxy<T extends object>(objectFunc: () => T): T {
}, },
getOwnPropertyDescriptor(target, key) { getOwnPropertyDescriptor(target, key) {
if (!calculated) { if (!calculated) {
Object.assign(obj, objectFunc()); Object.assign(obj, objectFunc(obj));
calculated = true; calculated = true;
} }
return Object.getOwnPropertyDescriptor(target, key); return Object.getOwnPropertyDescriptor(target, key);
} }
}) as T; }) as S & T;
} }