forked from profectus/Profectus
Fix save data object redundancy
Note: Requires the use of noPersist quite a bit, but the console will tell you when you missed a spot Also required breaking up events.ts due to cyclical dependencies
This commit is contained in:
parent
ae0d19c267
commit
4207677944
6 changed files with 218 additions and 128 deletions
|
@ -1,5 +1,5 @@
|
|||
import { globalBus } from "game/events";
|
||||
import type { State } from "game/persistence";
|
||||
import { NonPersistent, Persistent, State } from "game/persistence";
|
||||
import { persistent } from "game/persistence";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { format, formatWhole } from "util/bignum";
|
||||
|
@ -13,18 +13,38 @@ export interface Resource<T = DecimalSource> extends Ref<T> {
|
|||
small?: boolean;
|
||||
}
|
||||
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: T,
|
||||
displayName?: string,
|
||||
precision?: number,
|
||||
small?: boolean | undefined
|
||||
): Resource<T> & Persistent<T> & { [NonPersistent]: Resource<T> };
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: Ref<T>,
|
||||
displayName?: string,
|
||||
precision?: number,
|
||||
small?: boolean | undefined
|
||||
): Resource<T>;
|
||||
export function createResource<T extends State>(
|
||||
defaultValue: T | Ref<T>,
|
||||
displayName = "points",
|
||||
precision = 0,
|
||||
small = undefined
|
||||
): Resource<T> {
|
||||
small: boolean | undefined = undefined
|
||||
) {
|
||||
const resource: Partial<Resource<T>> = isRef(defaultValue)
|
||||
? defaultValue
|
||||
: persistent(defaultValue);
|
||||
resource.displayName = displayName;
|
||||
resource.precision = precision;
|
||||
resource.small = small;
|
||||
if (!isRef(defaultValue)) {
|
||||
const nonPersistentResource = (resource as Persistent<T>)[
|
||||
NonPersistent
|
||||
] as unknown as Resource<T>;
|
||||
nonPersistentResource.displayName = displayName;
|
||||
nonPersistentResource.precision = precision;
|
||||
nonPersistentResource.small = small;
|
||||
}
|
||||
return resource as Resource<T>;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,6 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import player from "game/player";
|
||||
import type { Settings } from "game/settings";
|
||||
import settings from "game/settings";
|
||||
import state from "game/state";
|
||||
import { createNanoEvents } from "nanoevents";
|
||||
import Decimal from "util/bignum";
|
||||
import type { App, Ref } from "vue";
|
||||
import { watch } from "vue";
|
||||
import type { App } from "vue";
|
||||
import type { GenericLayer } from "./layers";
|
||||
|
||||
/** All types of events able to be sent or emitted from the global event bus. */
|
||||
|
@ -60,102 +54,4 @@ export interface GlobalEvents {
|
|||
/** A global event bus for hooking into {@link GlobalEvents}. */
|
||||
export const globalBus = createNanoEvents<GlobalEvents>();
|
||||
|
||||
let intervalID: NodeJS.Timer | null = null;
|
||||
|
||||
// Not imported immediately due to dependency cycles
|
||||
// This gets set during startGameLoop(), and will only be used in the update function
|
||||
let hasWon: null | Ref<boolean> = null;
|
||||
|
||||
function update() {
|
||||
const now = Date.now();
|
||||
let diff = (now - player.time) / 1e3;
|
||||
player.time = now;
|
||||
const trueDiff = diff;
|
||||
|
||||
state.lastTenTicks.push(trueDiff);
|
||||
if (state.lastTenTicks.length > 10) {
|
||||
state.lastTenTicks = state.lastTenTicks.slice(1);
|
||||
}
|
||||
|
||||
// Stop here if the game is paused on the win screen
|
||||
if (hasWon?.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
// Stop here if the player had a NaN value
|
||||
if (state.hasNaN) {
|
||||
return;
|
||||
}
|
||||
|
||||
diff = Math.max(diff, 0);
|
||||
|
||||
if (player.devSpeed === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
if (Decimal.gt(player.offlineTime, 0) && player.devSpeed !== 0) {
|
||||
const offlineDiff = Math.max(player.offlineTime / 10, diff);
|
||||
player.offlineTime = player.offlineTime - offlineDiff;
|
||||
diff += offlineDiff;
|
||||
} else if (player.devSpeed === 0) {
|
||||
player.offlineTime += diff;
|
||||
}
|
||||
if (!player.offlineProd || Decimal.lt(player.offlineTime, 0)) {
|
||||
player.offlineTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at max tick length
|
||||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(diff)) {
|
||||
diff = 1e308;
|
||||
}
|
||||
|
||||
// Update
|
||||
if (Decimal.eq(diff, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.timePlayed += diff;
|
||||
if (!Number.isFinite(player.timePlayed)) {
|
||||
player.timePlayed = 1e308;
|
||||
}
|
||||
globalBus.emit("update", diff, trueDiff);
|
||||
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
if (intervalID != null) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
} else if (intervalID == null) {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the game loop for the project, which updates the game in ticks. */
|
||||
export async function startGameLoop() {
|
||||
hasWon = (await import("data/projEntry")).hasWon;
|
||||
watch(hasWon, hasWon => {
|
||||
if (hasWon) {
|
||||
globalBus.emit("gameWon");
|
||||
}
|
||||
});
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
document.fonts.onloadingdone = () => globalBus.emit("fontsLoaded");
|
||||
|
|
106
src/game/gameLoop.ts
Normal file
106
src/game/gameLoop.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import { globalBus } from "game/events";
|
||||
import settings from "game/settings";
|
||||
import Decimal from "util/bignum";
|
||||
import type { Ref } from "vue";
|
||||
import { watch } from "vue";
|
||||
import player from "./player";
|
||||
import state from "./state";
|
||||
|
||||
let intervalID: NodeJS.Timer | null = null;
|
||||
|
||||
// Not imported immediately due to dependency cycles
|
||||
// This gets set during startGameLoop(), and will only be used in the update function
|
||||
let hasWon: null | Ref<boolean> = null;
|
||||
|
||||
function update() {
|
||||
const now = Date.now();
|
||||
let diff = (now - player.time) / 1e3;
|
||||
player.time = now;
|
||||
const trueDiff = diff;
|
||||
|
||||
state.lastTenTicks.push(trueDiff);
|
||||
if (state.lastTenTicks.length > 10) {
|
||||
state.lastTenTicks = state.lastTenTicks.slice(1);
|
||||
}
|
||||
|
||||
// Stop here if the game is paused on the win screen
|
||||
if (hasWon?.value && !player.keepGoing) {
|
||||
return;
|
||||
}
|
||||
// Stop here if the player had a NaN value
|
||||
if (state.hasNaN) {
|
||||
return;
|
||||
}
|
||||
|
||||
diff = Math.max(diff, 0);
|
||||
|
||||
if (player.devSpeed === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add offline time if any
|
||||
if (player.offlineTime != undefined) {
|
||||
if (Decimal.gt(player.offlineTime, projInfo.offlineLimit * 3600)) {
|
||||
player.offlineTime = projInfo.offlineLimit * 3600;
|
||||
}
|
||||
if (Decimal.gt(player.offlineTime, 0) && player.devSpeed !== 0) {
|
||||
const offlineDiff = Math.max(player.offlineTime / 10, diff);
|
||||
player.offlineTime = player.offlineTime - offlineDiff;
|
||||
diff += offlineDiff;
|
||||
} else if (player.devSpeed === 0) {
|
||||
player.offlineTime += diff;
|
||||
}
|
||||
if (!player.offlineProd || Decimal.lt(player.offlineTime, 0)) {
|
||||
player.offlineTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Cap at max tick length
|
||||
diff = Math.min(diff, projInfo.maxTickLength);
|
||||
|
||||
// Apply dev speed
|
||||
if (player.devSpeed != undefined) {
|
||||
diff *= player.devSpeed;
|
||||
}
|
||||
|
||||
if (!Number.isFinite(diff)) {
|
||||
diff = 1e308;
|
||||
}
|
||||
|
||||
// Update
|
||||
if (Decimal.eq(diff, 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
player.timePlayed += diff;
|
||||
if (!Number.isFinite(player.timePlayed)) {
|
||||
player.timePlayed = 1e308;
|
||||
}
|
||||
globalBus.emit("update", diff, trueDiff);
|
||||
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
if (intervalID != null) {
|
||||
clearInterval(intervalID);
|
||||
intervalID = null;
|
||||
}
|
||||
} else if (intervalID == null) {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the game loop for the project, which updates the game in ticks. */
|
||||
export async function startGameLoop() {
|
||||
hasWon = (await import("data/projEntry")).hasWon;
|
||||
watch(hasWon, hasWon => {
|
||||
if (hasWon) {
|
||||
globalBus.emit("gameWon");
|
||||
}
|
||||
});
|
||||
if (settings.unthrottled) {
|
||||
requestAnimationFrame(update);
|
||||
} else {
|
||||
intervalID = setInterval(update, 50);
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { identifier } from "@babel/types";
|
||||
import { isArray } from "@vue/shared";
|
||||
import { globalBus } from "game/events";
|
||||
import type { GenericLayer } from "game/layers";
|
||||
|
@ -5,8 +6,8 @@ import { addingLayers, persistentRefs } from "game/layers";
|
|||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal from "util/bignum";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import type { Ref } from "vue";
|
||||
import { isReactive, isRef, ref } from "vue";
|
||||
import type { Ref, WritableComputedRef } from "vue";
|
||||
import { computed, isReactive, isRef, ref } from "vue";
|
||||
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
|
@ -28,6 +29,16 @@ export const StackTrace = Symbol("StackTrace");
|
|||
* @see {@link Persistent[Deleted]}
|
||||
*/
|
||||
export const Deleted = Symbol("Deleted");
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
* @see {@link Persistent[NonPersistent]}
|
||||
*/
|
||||
export const NonPersistent = Symbol("NonPersistent");
|
||||
/**
|
||||
* A symbol used in {@link Persistent} objects.
|
||||
* @see {@link Persistent[SaveDataPath]}
|
||||
*/
|
||||
export const SaveDataPath = Symbol("SaveDataPath");
|
||||
|
||||
/**
|
||||
* This is a union of things that should be safely stringifiable without needing special processes or knowing what to load them in as.
|
||||
|
@ -57,6 +68,14 @@ export type Persistent<T extends State = State> = Ref<T> & {
|
|||
* @see {@link deletePersistent} for marking a persistent ref as deleted.
|
||||
*/
|
||||
[Deleted]: boolean;
|
||||
/**
|
||||
* A non-persistent ref that just reads and writes ot the persistent ref. Used for passing to other features without duplicating the persistent ref in the constructed save data object.
|
||||
*/
|
||||
[NonPersistent]: WritableComputedRef<T>;
|
||||
/**
|
||||
* The path this persistent appears in within the save data object. Predominantly used to ensure it's only placed in there one time.
|
||||
*/
|
||||
[SaveDataPath]: string[] | undefined;
|
||||
};
|
||||
|
||||
function getStackTrace() {
|
||||
|
@ -83,6 +102,15 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten
|
|||
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
|
||||
persistent[StackTrace] = getStackTrace();
|
||||
persistent[Deleted] = false;
|
||||
persistent[NonPersistent] = computed({
|
||||
get() {
|
||||
return persistent.value;
|
||||
},
|
||||
set(value) {
|
||||
persistent.value = value;
|
||||
}
|
||||
});
|
||||
persistent[SaveDataPath] = undefined;
|
||||
|
||||
if (addingLayers.length === 0) {
|
||||
console.warn(
|
||||
|
@ -97,6 +125,25 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten
|
|||
return persistent as Persistent<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for whether an arbitrary value is a persistent ref
|
||||
* @param value The value that may or may not be a persistent ref
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function isPersistent(value: any): value is Persistent {
|
||||
return value && typeof value === "object" && PersistentState in value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwraps the non-persistent ref inside of persistent refs, to be passed to other features without duplicating values in the save data object.
|
||||
* @param persistent The persistent ref to unwrap
|
||||
*/
|
||||
export function noPersist<T extends Persistent<S>, S extends State>(
|
||||
persistent: T
|
||||
): T[typeof NonPersistent] {
|
||||
return persistent[NonPersistent];
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a {@link Persistent} as deleted, so it won't be saved and loaded.
|
||||
* Since persistent refs must be created during a layer's options func, features can not create persistent refs after evaluating their own options funcs.
|
||||
|
@ -117,24 +164,40 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
const handleObject = (obj: Record<string, unknown>, path: string[] = []): boolean => {
|
||||
let foundPersistent = false;
|
||||
Object.keys(obj).forEach(key => {
|
||||
const value = obj[key];
|
||||
let value = obj[key];
|
||||
if (value && typeof value === "object") {
|
||||
if (PersistentState in value) {
|
||||
if (ProxyState in value) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
value = (value as any)[ProxyState] as object;
|
||||
}
|
||||
if (isPersistent(value)) {
|
||||
foundPersistent = true;
|
||||
if ((value as Persistent)[Deleted]) {
|
||||
if (value[Deleted]) {
|
||||
console.warn(
|
||||
"Deleted persistent ref present in returned object. Ignoring...",
|
||||
value,
|
||||
"\nCreated at:\n" + (value as Persistent)[StackTrace]
|
||||
"\nCreated at:\n" + value[StackTrace]
|
||||
);
|
||||
return;
|
||||
}
|
||||
persistentRefs[layer.id].delete(
|
||||
ProxyState in value
|
||||
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
((value as any)[ProxyState] as Persistent)
|
||||
: (value as Persistent)
|
||||
);
|
||||
persistentRefs[layer.id].delete(value);
|
||||
|
||||
// Handle SaveDataPath
|
||||
const newPath = [layer.id, ...path, key];
|
||||
if (
|
||||
value[SaveDataPath] != undefined &&
|
||||
JSON.stringify(newPath) !== JSON.stringify(value[SaveDataPath])
|
||||
) {
|
||||
console.error(
|
||||
`Persistent ref is being saved to \`${newPath.join(
|
||||
"."
|
||||
)}\` when it's already present at \`${value[SaveDataPath].join(
|
||||
"."
|
||||
)}\`. This can cause unexpected behavior when loading saves between updates.`,
|
||||
value
|
||||
);
|
||||
}
|
||||
value[SaveDataPath] = newPath;
|
||||
|
||||
// Construct save path if it doesn't exist
|
||||
const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => {
|
||||
|
@ -147,21 +210,19 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
// Cache currently saved value
|
||||
const savedValue = persistentState[key];
|
||||
// Add ref to save data
|
||||
persistentState[key] = (value as Persistent)[PersistentState];
|
||||
persistentState[key] = value[PersistentState];
|
||||
// Load previously saved value
|
||||
if (isReactive(persistentState)) {
|
||||
if (savedValue != null) {
|
||||
persistentState[key] = savedValue;
|
||||
} else {
|
||||
persistentState[key] = (value as Persistent)[DefaultValue];
|
||||
persistentState[key] = value[DefaultValue];
|
||||
}
|
||||
} else {
|
||||
if (savedValue != null) {
|
||||
(persistentState[key] as Ref<unknown>).value = savedValue;
|
||||
} else {
|
||||
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
|
||||
DefaultValue
|
||||
];
|
||||
(persistentState[key] as Ref<unknown>).value = value[DefaultValue];
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
|
@ -200,7 +261,8 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
});
|
||||
return foundPersistent;
|
||||
};
|
||||
handleObject(layer);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handleObject((layer as any)[ProxyState]);
|
||||
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`,
|
||||
|
|
|
@ -36,7 +36,8 @@ requestAnimationFrame(async () => {
|
|||
"padding: 4px;"
|
||||
);
|
||||
await load();
|
||||
const { globalBus, startGameLoop } = await import("./game/events");
|
||||
const { globalBus } = await import("./game/events");
|
||||
const { startGameLoop } = await import("./game/gameLoop");
|
||||
|
||||
// Create Vue
|
||||
const vue = (window.vue = createApp(App));
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { NonPersistent } from "game/persistence";
|
||||
import Decimal from "util/bignum";
|
||||
|
||||
export const ProxyState = Symbol("ProxyState");
|
||||
|
@ -37,7 +38,11 @@ export function createLazyProxy<T extends object, S extends T>(
|
|||
return calculateObj();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return (calculateObj() as any)[key];
|
||||
const val = (calculateObj() as any)[key];
|
||||
if (val && typeof val === "object" && NonPersistent in val) {
|
||||
return val[NonPersistent];
|
||||
}
|
||||
return val;
|
||||
},
|
||||
set(target, key, value) {
|
||||
// TODO give warning about this? It should only be done with caution
|
||||
|
|
Loading…
Reference in a new issue