Documented /game

This commit is contained in:
thepaperpilot 2022-07-15 00:55:36 -05:00
parent 23545a9d33
commit 8d1234a916
7 changed files with 139 additions and 7 deletions

View file

@ -9,15 +9,45 @@ import type { App, Ref } from "vue";
import { watch } from "vue"; import { watch } from "vue";
import type { GenericLayer } from "./layers"; import type { GenericLayer } from "./layers";
/** All types of events able to be sent or emitted from the global event bus. */
export interface GlobalEvents { 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<string, unknown>) => void; addLayer: (layer: GenericLayer, saveData: Record<string, unknown>) => void;
/**
* Sent whenever a layer is removed.
* @param layer The layer being removed.
*/
removeLayer: (layer: GenericLayer) => void; 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; 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<Settings>) => void; loadSettings: (settings: Partial<Settings>) => void;
/**
* Sent when the game has ended.
*/
gameWon: VoidFunction; 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; setupVue: (vue: App) => void;
} }
/** A global event bus for hooking into {@link GlobalEvents}. */
export const globalBus = createNanoEvents<GlobalEvents>(); export const globalBus = createNanoEvents<GlobalEvents>();
let intervalID: NodeJS.Timer | null = null; 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() { export async function startGameLoop() {
hasWon = (await import("data/projEntry")).hasWon; hasWon = (await import("data/projEntry")).hasWon;
watch(hasWon, hasWon => { watch(hasWon, hasWon => {

View file

@ -53,11 +53,20 @@ export const BoundsInjectionKey: InjectionKey<Ref<DOMRect | undefined>> = Symbol
/** All types of events able to be sent or emitted from a layer's emitter. */ /** All types of events able to be sent or emitted from a layer's emitter. */
export interface LayerEvents { 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; 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; 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; postUpdate: (diff: number) => void;
} }

View file

@ -4,6 +4,12 @@ import "vue-toastification/dist/index.css";
globalBus.on("setupVue", vue => vue.use(Toast)); 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") { export function getNotifyStyle(color = "white", strength = "8px") {
return { return {
transform: "scale(1.05, 1.05)", 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() { export function getHighNotifyStyle() {
return getNotifyStyle("red", "20px"); return getNotifyStyle("red", "20px");
} }

View file

@ -8,15 +8,32 @@ import { ProxyState } from "util/proxies";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { isReactive, isRef, 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"); export const PersistentState = Symbol("PersistentState");
/**
* A symbol used in {@link Persistent} objects.
* @see {@link Persistent[DefaultValue]}
*/
export const DefaultValue = Symbol("DefaultValue"); export const DefaultValue = Symbol("DefaultValue");
/**
* A symbol used in {@link Persistent} objects.
* @see {@link Persistent[StackTrace]}
*/
export const StackTrace = Symbol("StackTrace"); export const StackTrace = Symbol("StackTrace");
/**
* A symbol used in {@link Persistent} objects.
* @see {@link Persistent[Deleted]}
*/
export const Deleted = Symbol("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 * 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. * - 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 * - DecimalSources are allowed because the string is a valid value for them
*/
export type State = export type State =
| string | string
| number | number
@ -25,10 +42,20 @@ export type State =
| { [key: string]: State } | { [key: string]: State }
| { [key: number]: 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<T extends State = State> = Ref<T> & { export type Persistent<T extends State = State> = Ref<T> & {
/** A flag that this is a persistent property. Typically a circular reference. */
[PersistentState]: Ref<T>; [PersistentState]: Ref<T>;
/** The value the ref should be set to in a fresh save, or when updating an old save to the current version. */
[DefaultValue]: T; [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; [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; [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<T extends State>(defaultValue: T | Ref<T>): Persistent<T> { 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)
@ -65,6 +97,12 @@ export function persistent<T extends State>(defaultValue: T | Ref<T>): Persisten
return persistent as Persistent<T>; return persistent as Persistent<T>;
} }
/**
* 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) { export function deletePersistent(persistent: Persistent) {
if (addingLayers.length === 0) { if (addingLayers.length === 0) {
console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent); console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent);

View file

@ -6,24 +6,40 @@ import { reactive, unref } from "vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import transientState from "./state"; import transientState from "./state";
/** The player save data object. */
export interface PlayerData { export interface PlayerData {
/** The ID of this save. */
id: string; id: string;
/** A multiplier for time passing. Set to 0 when the game is paused. */
devSpeed: number | null; devSpeed: number | null;
/** The display name of this save. */
name: string; name: string;
/** The open tabs. */
tabs: Array<string>; tabs: Array<string>;
/** The current time this save was last opened at, in ms since the unix epoch. */
time: number; time: number;
/** Whether or not to automatically save every couple of seconds and on tab close. */
autosave: boolean; autosave: boolean;
/** Whether or not to apply offline time when loading this save. */
offlineProd: boolean; offlineProd: boolean;
/** How much offline time has been accumulated and not yet processed. */
offlineTime: number | null; offlineTime: number | null;
/** How long, in ms, this game has been played. */
timePlayed: number; timePlayed: number;
/** Whether or not to continue playing after {@link data/projEntry.hasWon} is true. */
keepGoing: boolean; keepGoing: boolean;
/** The ID of this project, to make sure saves aren't imported into the wrong project. */
modID: string; modID: string;
/** The version of the project this save was created by. Used for upgrading saves for new versions. */
modVersion: string; modVersion: string;
/** A dictionary of layer save data. */
layers: Record<string, LayerData<unknown>>; layers: Record<string, LayerData<unknown>>;
} }
/** The proxied player that is used to track NaN values. */
export type Player = ProxiedWithState<PlayerData>; export type Player = ProxiedWithState<PlayerData>;
/** A layer's save data. Automatically unwraps refs. */
export type LayerData<T> = { export type LayerData<T> = {
[P in keyof T]?: T[P] extends (infer U)[] [P in keyof T]?: T[P] extends (infer U)[]
? LayerData<U>[] ? LayerData<U>[]
@ -52,6 +68,7 @@ const state = reactive<PlayerData>({
layers: {} layers: {}
}); });
/** Convert a player save data object into a JSON string. Unwraps refs. */
export function stringifySave(player: PlayerData): string { export function stringifySave(player: PlayerData): string {
return JSON.stringify(player, (key, value) => unref(value)); return JSON.stringify(player, (key, value) => unref(value));
} }
@ -133,6 +150,7 @@ declare global {
player: Player; player: Player;
} }
} }
/** The player save data object. */
export default window.player = new Proxy( export default window.player = new Proxy(
{ [ProxyState]: state, [ProxyPath]: ["player"] }, { [ProxyState]: state, [ProxyPath]: ["player"] },
playerHandler playerHandler

View file

@ -6,11 +6,17 @@ import LZString from "lz-string";
import { hardReset } from "util/save"; import { hardReset } from "util/save";
import { reactive, watch } from "vue"; import { reactive, watch } from "vue";
/** The player's settings object. */
export interface Settings { export interface Settings {
/** The ID of the active save. */
active: string; active: string;
/** The IDs of all created saves. */
saves: string[]; saves: string[];
/** Whether or not to show the current ticks per second in the lower left corner of the page. */
showTPS: boolean; showTPS: boolean;
/** The current theme to display the game in. */
theme: Themes; theme: Themes;
/** Whether or not to cap the project at 20 ticks per second. */
unthrottled: boolean; unthrottled: boolean;
} }
@ -40,7 +46,12 @@ declare global {
hardResetSettings: VoidFunction; 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; export default window.settings = state as Settings;
/** A function that erases all player settings, including all saves. */
export const hardResetSettings = (window.hardResetSettings = () => { export const hardResetSettings = (window.hardResetSettings = () => {
const settings = { const settings = {
active: "", active: "",
@ -53,6 +64,12 @@ export const hardResetSettings = (window.hardResetSettings = () => {
hardReset(); 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 { export function loadSettings(): void {
try { try {
let item: string | null = localStorage.getItem(projInfo.id); let item: string | null = localStorage.getItem(projInfo.id);
@ -80,17 +97,23 @@ export function loadSettings(): void {
} catch {} } catch {}
} }
/** A list of fields to append to the settings modal. */
export const settingFields: CoercableComponent[] = reactive([]); export const settingFields: CoercableComponent[] = reactive([]);
/** Register a field to be displayed in the settings modal. */
export function registerSettingField(component: CoercableComponent) { export function registerSettingField(component: CoercableComponent) {
settingFields.push(component); settingFields.push(component);
} }
/** A list of components to show in the info modal. */
export const infoComponents: CoercableComponent[] = reactive([]); export const infoComponents: CoercableComponent[] = reactive([]);
/** Register a component to be displayed in the info modal. */
export function registerInfoComponent(component: CoercableComponent) { export function registerInfoComponent(component: CoercableComponent) {
infoComponents.push(component); infoComponents.push(component);
} }
/** A list of components to add to the root of the page. */
export const gameComponents: CoercableComponent[] = reactive([]); export const gameComponents: CoercableComponent[] = reactive([]);
/** Register a component to be displayed at the root of the page. */
export function registerGameComponent(component: CoercableComponent) { export function registerGameComponent(component: CoercableComponent) {
gameComponents.push(component); gameComponents.push(component);
} }

View file

@ -1,9 +1,14 @@
import { shallowReactive } from "vue"; import { shallowReactive } from "vue";
/** An object of global data that is not persistent. */
export interface Transient { export interface Transient {
/** A list of the duration, in ms, of the last 10 game ticks. Used for calculating TPS. */
lastTenTicks: number[]; lastTenTicks: number[];
/** Whether or not a NaN value has been detected and undealt with. */
hasNaN: boolean; hasNaN: boolean;
/** The location within the player save data object of the NaN value. */
NaNPath?: string[]; NaNPath?: string[];
/** The parent object of the NaN value. */
NaNReceiver?: Record<string, unknown>; NaNReceiver?: Record<string, unknown>;
} }
@ -13,6 +18,7 @@ declare global {
state: Transient; state: Transient;
} }
} }
/** The global transient state object. */
export default window.state = shallowReactive<Transient>({ export default window.state = shallowReactive<Transient>({
lastTenTicks: [], lastTenTicks: [],
hasNaN: false, hasNaN: false,