import type { WhilePlaying } from '../core/model/playSettings'; /** * How closely the editor follows a running game (#265). The game runs every frame whatever the * preset says; what is held back is only what the panels show of it. With hundreds of objects * appearing and vanishing, rebuilding the hierarchy on every one of them cost more than the game. */ /** Updates a second on a throttled part. One rate for all of them, so the setting stays one choice. */ export const PANEL_RATE = 4; /** When a part of the editor catches up: every frame, `PANEL_RATE` times a second, or only on pause. */ export type Follow = 'live' | 'throttled' | 'paused'; export interface PanelPolicy { /** The hierarchy and every panel that rebuilds when objects appear, vanish or move to a parent. */ structure: Follow; /** Inspector, variables and tile maps: values that change without an object appearing. */ values: Follow; /** Pulses and active states in the graph and state machine tabs. */ graphs: 'live' | 'throttled'; /** Whether the scene view keeps redrawing while the game tab hides it. */ hiddenSceneView: boolean; } export function panelPolicy(mode: WhilePlaying): PanelPolicy { switch (mode) { case 'live': return { structure: 'live', values: 'live', graphs: 'live', hiddenSceneView: true }; case 'balanced': // The graph tab draws only while it is on screen and cost nothing measurable; at 4 frames a // second its beads would stutter and bunch up, so it keeps up with the game. return { structure: 'throttled', values: 'throttled', graphs: 'live', hiddenSceneView: false }; case 'minimal': // A graph has no "on pause": a pulse shown only when the game stands still would never be seen. return { structure: 'paused', values: 'paused', graphs: 'throttled', hiddenSceneView: false }; } } /** What to bring up to date after a frame. */ export interface PanelUpdate { structure: boolean; values: boolean; graphs: boolean; } /** * Decides frame by frame what the panels get. A change of structure that falls between two updates * is kept, not dropped: the next update that is due carries it, and `flush` hands it out at once. */ export class PanelClock { private lastAt = Number.NEGATIVE_INFINITY; private pendingStructure = false; /** One frame of the game. `now` is wall time in milliseconds, `structure` whether objects changed. */ frame(policy: PanelPolicy, now: number, structure: boolean): PanelUpdate { this.pendingStructure ||= structure; const due = now - this.lastAt >= 1000 / PANEL_RATE; if (due) this.lastAt = now; const follows = (follow: Follow) => follow === 'live' || (follow === 'throttled' && due); const update = { structure: this.pendingStructure && follows(policy.structure), values: follows(policy.values), graphs: policy.graphs === 'live' || due, }; if (update.structure) this.pendingStructure = false; return update; } /** The game stands still or something must be exact now: everything held back comes out at once. */ flush(): PanelUpdate { const structure = this.pendingStructure; this.pendingStructure = false; return { structure, values: true, graphs: true }; } /** Whether a change of structure is waiting for its update. */ get holdsStructure(): boolean { return this.pendingStructure; } }