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 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<string, unknown>) => 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<Settings>) => 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<GlobalEvents>();
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 => {

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. */
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;
}

View file

@ -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");
}

View file

@ -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<T extends State = State> = Ref<T> & {
/** A flag that this is a persistent property. Typically a circular reference. */
[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;
/** 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<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
const persistent = (
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>;
}
/**
* 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);

View file

@ -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<string>;
/** 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<string, LayerData<unknown>>;
}
/** The proxied player that is used to track NaN values. */
export type Player = ProxiedWithState<PlayerData>;
/** A layer's save data. Automatically unwraps refs. */
export type LayerData<T> = {
[P in keyof T]?: T[P] extends (infer U)[]
? LayerData<U>[]
@ -52,6 +68,7 @@ const state = reactive<PlayerData>({
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

View file

@ -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);
}

View file

@ -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<string, unknown>;
}
@ -13,6 +18,7 @@ declare global {
state: Transient;
}
}
/** The global transient state object. */
export default window.state = shallowReactive<Transient>({
lastTenTicks: [],
hasNaN: false,