import Modal from "components/Modal.vue"; import type { CoercableComponent, JSXFunction, OptionsFunc, Replace, StyleValue } from "features/feature"; import { jsx, setDefault } from "features/feature"; import { globalBus } from "game/events"; import type { Persistent } from "game/persistence"; import { persistent } from "game/persistence"; import player from "game/player"; import type { Emitter } from "nanoevents"; import { createNanoEvents } from "nanoevents"; import type { Computable, GetComputableType, GetComputableTypeWithDefault, ProcessedComputable } from "util/computed"; import { processComputable } from "util/computed"; import { createLazyProxy } from "util/proxies"; import type { InjectionKey, Ref } from "vue"; import { ref, shallowReactive, unref } from "vue"; /** A feature's node in the DOM that has its size tracked. */ export interface FeatureNode { rect: DOMRect; observer: MutationObserver; element: HTMLElement; } /** * An injection key that a {@link ContextComponent} will use to provide a function that registers a {@link FeatureNode} with the given id and HTML element. */ export const RegisterNodeInjectionKey: InjectionKey<(id: string, element: HTMLElement) => void> = Symbol("RegisterNode"); /** * An injection key that a {@link ContextComponent} will use to provide a function that unregisters a {@link FeatureNode} with the given id. */ export const UnregisterNodeInjectionKey: InjectionKey<(id: string) => void> = Symbol("UnregisterNode"); /** * An injection key that a {@link ContextComponent} will use to provide a ref to a map of all currently registered {@link FeatureNode}s. */ export const NodesInjectionKey: InjectionKey>> = Symbol("Nodes"); /** * An injection key that a {@link ContextComponent} will use to provide a ref to a bounding rect of the Context. */ export const BoundsInjectionKey: InjectionKey> = Symbol("Bounds"); /** 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. * @param diff The delta time since last tick, in ms. */ preUpdate: (diff: number) => void; /** * 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. * @param diff The delta time since last tick, in ms. */ postUpdate: (diff: number) => void; } /** * A reference to all the current layers. * It is shallow reactive so it will update when layers are added or removed, but not interfere with the existing refs within each layer. */ export const layers: Record | undefined> = shallowReactive({}); declare global { /** Augment the window object so the layers can be accessed from the console. */ interface Window { layers: Record | undefined>; } } window.layers = layers; declare module "@vue/runtime-dom" { /** Augment CSS Properties to allow for setting the layer color CSS variable. */ interface CSSProperties { "--layer-color"?: string; } } /** An object representing the position of some entity. */ export interface Position { /** The X component of the entity's position. */ x: number; /** The Y component of the entity's position. */ y: number; } /** * An object that configures a {@link Layer}. * Even moreso than features, the developer is expected to include extra properties in this object. * All {@link game/persistence.Persistent} refs must be included somewhere within the layer object. */ export interface LayerOptions { /** The color of the layer, used to theme the entire layer's display. */ color?: Computable; /** * The layout of this layer's features. * When the layer is open in {@link game/player.PlayerData.tabs}, this is the content that is display. */ display: Computable; /** An object of classes that should be applied to the display. */ classes?: Computable>; /** Styles that should be applied to the display. */ style?: Computable; /** * The name of the layer, used on minimized tabs. * Defaults to {@link BaseLayer.id}. */ name?: Computable; /** * Whether or not the layer can be minimized. * Defaults to true. */ minimizable?: Computable; /** * Whether or not to force the go back button to be hidden. * If true, go back will be hidden regardless of {@link data/projInfo.allowGoBack}. */ forceHideGoBack?: Computable; /** * A CSS min-width value that is applied to the layer. * Can be a number, in which case the unit is assumed to be px. * Defaults to 600px. */ minWidth?: Computable; } /** The properties that are added onto a processed {@link LayerOptions} to create a {@link Layer} */ export interface BaseLayer { /** * The ID of the layer. * Populated from the {@link createLayer} parameters. * Used for saving and tracking open tabs. */ id: string; /** A persistent ref tracking if the tab is minimized or not. */ minimized: Persistent; /** An emitter for sending {@link LayerEvents} events for this layer. */ emitter: Emitter; /** A function to register an event listener on {@link emitter}. */ on: OmitThisParameter["on"]>; /** A function to emit a {@link LayerEvents} event to this layer. */ emit: (...args: [K, ...Parameters]) => void; /** A map of {@link FeatureNode}s present in this layer's {@link ContextComponent} component. */ nodes: Ref>; } /** An unit of game content. Displayed to the user as a tab or modal. */ export type Layer = Replace< T & BaseLayer, { color: GetComputableType; display: GetComputableType; classes: GetComputableType; style: GetComputableType; name: GetComputableTypeWithDefault; minWidth: GetComputableTypeWithDefault; minimizable: GetComputableTypeWithDefault; forceHideGoBack: GetComputableType; } >; /** A type that matches any valid {@link Layer} object. */ export type GenericLayer = Replace< Layer, { name: ProcessedComputable; minWidth: ProcessedComputable; minimizable: ProcessedComputable; } >; /** * When creating layers, this object a map of layer ID to a set of any created persistent refs in order to check they're all included in the final layer object. */ export const persistentRefs: Record> = {}; /** * When creating layers, this array stores the layers currently being created, as a stack. */ export const addingLayers: string[] = []; /** * Lazily creates a layer with the given options. * @param id The ID this layer will have. See {@link BaseLayer.id}. * @param optionsFunc Layer options. */ export function createLayer( id: string, optionsFunc: OptionsFunc ): Layer { return createLazyProxy(() => { const layer = {} as T & Partial; const emitter = (layer.emitter = createNanoEvents()); layer.on = emitter.on.bind(emitter); layer.emit = emitter.emit.bind(emitter) as ( ...args: [K, ...Parameters] ) => void; layer.nodes = ref({}); layer.id = id; addingLayers.push(id); persistentRefs[id] = new Set(); layer.minimized = persistent(false); Object.assign(layer, optionsFunc.call(layer as BaseLayer)); 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, "display"); processComputable(layer as T, "name"); setDefault(layer, "name", layer.id); processComputable(layer as T, "minWidth"); setDefault(layer, "minWidth", 600); processComputable(layer as T, "minimizable"); setDefault(layer, "minimizable", true); return layer as unknown as Layer; }); } /** * Enables a layer object, so it will be updated every tick. * Note that accessing a layer/its properties does NOT require it to be enabled. * For dynamic layers you can call this function and {@link removeLayer} as necessary. Just make sure {@link data/projEntry.getInitialLayers} will provide an accurate list of layers based on the player data object. * For static layers just make {@link data/projEntry.getInitialLayers} return all the layers. * @param layer The layer to add. * @param player The player data object, which will have a data object for this layer. */ export function addLayer( layer: GenericLayer, player: { layers?: Record> } ): void { console.info("Adding layer", layer.id); if (layers[layer.id]) { console.error( "Attempted to add layer with same ID as existing layer", layer.id, layers[layer.id] ); return; } setDefault(player, "layers", {}); if (player.layers[layer.id] == null) { player.layers[layer.id] = {}; } layers[layer.id] = layer; globalBus.emit("addLayer", layer, player.layers[layer.id]); } /** * Convenience method for getting a layer by its ID with correct typing. * @param layerID The ID of the layer to get. */ export function getLayer(layerID: string): T { return layers[layerID] as T; } /** * Disables a layer, so it will no longer be updated every tick. * Note that accessing a layer/its properties does NOT require it to be enabled. * @param layer The layer to remove. */ export function removeLayer(layer: GenericLayer): void { console.info("Removing layer", layer.id); globalBus.emit("removeLayer", layer); layers[layer.id] = undefined; } /** * Convenience method for removing and immediately re-adding a layer. * This is useful for layers with dynamic content, to ensure persistent refs are correctly configured. * @param layer Layer to remove and then re-add */ export function reloadLayer(layer: GenericLayer): void { removeLayer(layer); // Re-create layer addLayer(layer, player); } /** * Utility function for creating a modal that display's a {@link LayerOptions.display}. * Returns the modal itself, which can be rendered anywhere you need, as well as a function to open the modal. * @param layer The layer to display in the modal. */ export function setupLayerModal(layer: GenericLayer): { openModal: VoidFunction; modal: JSXFunction; } { const showModal = ref(false); return { openModal: () => (showModal.value = true), modal: jsx(() => ( (showModal.value = value)} v-slots={{ header: () =>

{unref(layer.name)}

, body: unref(layer.display) }} /> )) }; } globalBus.on("update", function updateLayers(diff) { Object.values(layers).forEach(layer => { layer?.emit("preUpdate", diff); }); Object.values(layers).forEach(layer => { layer?.emit("update", diff); }); Object.values(layers).forEach(layer => { layer?.emit("postUpdate", diff); }); });