diff --git a/src/game/events.ts b/src/game/events.ts index 1c53f17..78219fd 100644 --- a/src/game/events.ts +++ b/src/game/events.ts @@ -9,15 +9,45 @@ import type { App, Ref } from "vue"; import { watch } from "vue"; import type { GenericLayer } from "./layers"; +/** All types of events able to be sent or emitted from the global event bus. */ export interface GlobalEvents { + /** + * Sent whenever a layer is added. + * @param layer The layer being added. + * @param saveData The layer's save data object within player. + */ addLayer: (layer: GenericLayer, saveData: Record) => void; + /** + * Sent whenever a layer is removed. + * @param layer The layer being removed. + */ removeLayer: (layer: GenericLayer) => void; + /** + * Sent every game tick. Runs the life cycle of the project. + * @param diff The delta time since last tick, in ms. + * @param trueDiff The delta time since last tick, in ms. Unaffected by time modifiers like {@link game/player.Player.devSpeed} or {@link game/player.Player.offlineTime}. Intended for things like updating animations. + */ update: (diff: number, trueDiff: number) => void; + /** + * Sent when constructing the {@link Settings} object. + * Use it to add default values for custom properties to the object. + * @param settings The settings object being constructed. + * @see {@link features/features.setDefault} for setting default values. + */ loadSettings: (settings: Partial) => void; + /** + * Sent when the game has ended. + */ gameWon: VoidFunction; + /** + * Sent when setting up the Vue Application instance. + * Use it to register global components or otherwise set up things that should affect Vue globally. + * @param vue The Vue App being constructed. + */ setupVue: (vue: App) => void; } +/** A global event bus for hooking into {@link GlobalEvents}. */ export const globalBus = createNanoEvents(); let intervalID: NodeJS.Timer | null = null; @@ -103,6 +133,7 @@ function update() { } } +/** 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 => { diff --git a/src/game/layers.tsx b/src/game/layers.tsx index 2ecae92..404d72c 100644 --- a/src/game/layers.tsx +++ b/src/game/layers.tsx @@ -53,11 +53,20 @@ export const BoundsInjectionKey: InjectionKey> = Symbol /** All types of events able to be sent or emitted from a layer's emitter. */ export interface LayerEvents { - /** Sent every game tick, before the update event. Intended for "generation" type actions. */ + /** + * Sent every game tick, before the update event. Intended for "generation" type actions. + * @param diff The delta time since last tick, in ms. + */ preUpdate: (diff: number) => void; - /** Sent every game tick. Intended for "automation" type actions. */ + /** + * Sent every game tick. Intended for "automation" type actions. + * @param diff The delta time since last tick, in ms. + */ update: (diff: number) => void; - /** Sent every game tick, after the update event. Intended for checking state. */ + /** + * Sent every game tick, after the update event. Intended for checking state. + * @param diff The delta time since last tick, in ms. + */ postUpdate: (diff: number) => void; } diff --git a/src/game/notifications.ts b/src/game/notifications.ts index 9dd3831..4d87daf 100644 --- a/src/game/notifications.ts +++ b/src/game/notifications.ts @@ -4,6 +4,12 @@ import "vue-toastification/dist/index.css"; globalBus.on("setupVue", vue => vue.use(Toast)); +/** + * Gives a {@link CSSProperties} object that makes an object glow, to bring focus to it. + * Default values are for a "soft" white notif effect. + * @param color The color of the glow effect. + * @param strength The strength of the glow effect - affects its spread. + */ export function getNotifyStyle(color = "white", strength = "8px") { return { transform: "scale(1.05, 1.05)", @@ -13,6 +19,7 @@ export function getNotifyStyle(color = "white", strength = "8px") { }; } +/** Utility function to call {@link getNotifyStyle} with "high importance" parameters */ export function getHighNotifyStyle() { return getNotifyStyle("red", "20px"); } diff --git a/src/game/persistence.ts b/src/game/persistence.ts index 9f7303d..b871fbf 100644 --- a/src/game/persistence.ts +++ b/src/game/persistence.ts @@ -8,15 +8,32 @@ import { ProxyState } from "util/proxies"; import type { Ref } from "vue"; import { isReactive, isRef, ref } from "vue"; +/** + * A symbol used in {@link Persistent} objects. + * @see {@link Persistent[PersistentState]} + */ export const PersistentState = Symbol("PersistentState"); +/** + * A symbol used in {@link Persistent} objects. + * @see {@link Persistent[DefaultValue]} + */ export const DefaultValue = Symbol("DefaultValue"); +/** + * A symbol used in {@link Persistent} objects. + * @see {@link Persistent[StackTrace]} + */ export const StackTrace = Symbol("StackTrace"); +/** + * A symbol used in {@link Persistent} objects. + * @see {@link Persistent[Deleted]} + */ export const Deleted = Symbol("Deleted"); -// 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 +/** + * This is a union of things that should be safely stringifiable without needing special processes or 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 @@ -25,10 +42,20 @@ export type State = | { [key: string]: State } | { [key: number]: State }; +/** + * A {@link Ref} that has been augmented with properties to allow it to be saved and loaded within the player save data object. + */ export type Persistent = Ref & { + /** A flag that this is a persistent property. Typically a circular reference. */ [PersistentState]: Ref; + /** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */ [DefaultValue]: T; + /** The stack trace of where the persistent ref was created. This is used for debugging purposes when a persistent ref is created but not placed in its layer object. */ [StackTrace]: string; + /** + * This is a flag that can be set once the option func is evaluated, to mark that a persistent ref should _not_ be saved to the player save data object. + * @see {@link deletePersistent} for marking a persistent ref as deleted. + */ [Deleted]: boolean; }; @@ -42,6 +69,11 @@ function getStackTrace() { ); } +/** + * Create a persistent ref, which can be saved and loaded. + * All (non-deleted) persistent refs must be included somewhere within the layer object returned by that layer's options func. + * @param defaultValue The value the persistent ref should start at on fresh saves or when reset. + */ export function persistent(defaultValue: T | Ref): Persistent { const persistent = ( isRef(defaultValue) ? defaultValue : (ref(defaultValue) as unknown) @@ -65,6 +97,12 @@ export function persistent(defaultValue: T | Ref): Persisten return persistent as Persistent; } +/** + * 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. + * As a result, it must create any persistent refs it _might_ need. + * This function can then be called after the options func is evaluated to mark the persistent ref to not be saved or loaded. + */ export function deletePersistent(persistent: Persistent) { if (addingLayers.length === 0) { console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent); diff --git a/src/game/player.ts b/src/game/player.ts index fae969c..aa4e789 100644 --- a/src/game/player.ts +++ b/src/game/player.ts @@ -6,24 +6,40 @@ import { reactive, unref } from "vue"; import type { Ref } from "vue"; import transientState from "./state"; +/** The player save data object. */ export interface PlayerData { + /** The ID of this save. */ id: string; + /** A multiplier for time passing. Set to 0 when the game is paused. */ devSpeed: number | null; + /** The display name of this save. */ name: string; + /** The open tabs. */ tabs: Array; + /** The current time this save was last opened at, in ms since the unix epoch. */ time: number; + /** Whether or not to automatically save every couple of seconds and on tab close. */ autosave: boolean; + /** Whether or not to apply offline time when loading this save. */ offlineProd: boolean; + /** How much offline time has been accumulated and not yet processed. */ offlineTime: number | null; + /** How long, in ms, this game has been played. */ timePlayed: number; + /** Whether or not to continue playing after {@link data/projEntry.hasWon} is true. */ keepGoing: boolean; + /** The ID of this project, to make sure saves aren't imported into the wrong project. */ modID: string; + /** The version of the project this save was created by. Used for upgrading saves for new versions. */ modVersion: string; + /** A dictionary of layer save data. */ layers: Record>; } +/** The proxied player that is used to track NaN values. */ export type Player = ProxiedWithState; +/** A layer's save data. Automatically unwraps refs. */ export type LayerData = { [P in keyof T]?: T[P] extends (infer U)[] ? LayerData[] @@ -52,6 +68,7 @@ const state = reactive({ layers: {} }); +/** Convert a player save data object into a JSON string. Unwraps refs. */ export function stringifySave(player: PlayerData): string { return JSON.stringify(player, (key, value) => unref(value)); } @@ -133,6 +150,7 @@ declare global { player: Player; } } +/** The player save data object. */ export default window.player = new Proxy( { [ProxyState]: state, [ProxyPath]: ["player"] }, playerHandler diff --git a/src/game/settings.ts b/src/game/settings.ts index af295c4..cad4438 100644 --- a/src/game/settings.ts +++ b/src/game/settings.ts @@ -6,11 +6,17 @@ import LZString from "lz-string"; import { hardReset } from "util/save"; import { reactive, watch } from "vue"; +/** The player's settings object. */ export interface Settings { + /** The ID of the active save. */ active: string; + /** The IDs of all created saves. */ saves: string[]; + /** Whether or not to show the current ticks per second in the lower left corner of the page. */ showTPS: boolean; + /** The current theme to display the game in. */ theme: Themes; + /** Whether or not to cap the project at 20 ticks per second. */ unthrottled: boolean; } @@ -40,7 +46,12 @@ declare global { hardResetSettings: VoidFunction; } } +/** + * The player settings object. Stores data that persists across all saves. + * Automatically saved to localStorage whenever changed. + */ export default window.settings = state as Settings; +/** A function that erases all player settings, including all saves. */ export const hardResetSettings = (window.hardResetSettings = () => { const settings = { active: "", @@ -53,6 +64,12 @@ export const hardResetSettings = (window.hardResetSettings = () => { hardReset(); }); +/** + * Loads the player settings from localStorage. + * Calls the {@link GlobalEvents.loadSettings} event for custom properties to be included. + * Custom properties should be added by the file they relate to, so they won't be included if the file is tree shaken away. + * Custom properties should also register the field to modify said setting using {@link registerSettingField}. + */ export function loadSettings(): void { try { let item: string | null = localStorage.getItem(projInfo.id); @@ -80,17 +97,23 @@ export function loadSettings(): void { } catch {} } +/** A list of fields to append to the settings modal. */ export const settingFields: CoercableComponent[] = reactive([]); +/** Register a field to be displayed in the settings modal. */ export function registerSettingField(component: CoercableComponent) { settingFields.push(component); } +/** A list of components to show in the info modal. */ export const infoComponents: CoercableComponent[] = reactive([]); +/** Register a component to be displayed in the info modal. */ export function registerInfoComponent(component: CoercableComponent) { infoComponents.push(component); } +/** A list of components to add to the root of the page. */ export const gameComponents: CoercableComponent[] = reactive([]); +/** Register a component to be displayed at the root of the page. */ export function registerGameComponent(component: CoercableComponent) { gameComponents.push(component); } diff --git a/src/game/state.ts b/src/game/state.ts index 00934c4..bda0b6a 100644 --- a/src/game/state.ts +++ b/src/game/state.ts @@ -1,9 +1,14 @@ import { shallowReactive } from "vue"; +/** An object of global data that is not persistent. */ export interface Transient { + /** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */ lastTenTicks: number[]; + /** Whether or not a NaN value has been detected and undealt with. */ hasNaN: boolean; + /** The location within the player save data object of the NaN value. */ NaNPath?: string[]; + /** The parent object of the NaN value. */ NaNReceiver?: Record; } @@ -13,6 +18,7 @@ declare global { state: Transient; } } +/** The global transient state object. */ export default window.state = shallowReactive({ lastTenTicks: [], hasNaN: false,