/** * The one clock everything time-based reads: the world's delta, particle age and shader time. It * accumulates scaled frames instead of reading the wall clock, which is what makes a global time * scale a single multiplication – a paused game is scale 0, and nothing underneath has to be * rebased because the clock itself stands still. */ /** A frame longer than this (a breakpoint, a hidden tab) is treated as this long. */ export const MAX_FRAME = 0.1; /** * The scale that actually applies: a pause is a scale of 0, and so is anything that is not a * positive number – time never runs backwards. */ export function effectiveScale(scale: number, paused: boolean): number { if (paused || !Number.isFinite(scale) || scale <= 0) return 0; return scale; } /** How far the clock moves in one frame. */ export function timeStep(dt: number, scale: number, paused: boolean): number { return Math.min(Math.max(dt, 0), MAX_FRAME) * effectiveScale(scale, paused); } export class GameClock { private seconds = 0; /** The global time scale: 1 is real time, 2 twice as fast, 0 frozen. */ scale = 1; /** Seconds the clock has run, in its own scaled time. */ get now(): number { return this.seconds; } /** Moves the clock on and returns the step, which is the delta every ticked world gets. */ advance(dt: number, paused = false): number { const step = timeStep(dt, this.scale, paused); this.seconds += step; return step; } /** * Back to second zero and real time – a run starts there and leaves nothing behind (#167). * Anything holding an absolute second of this clock has to start over. */ reset(): void { this.seconds = 0; this.scale = 1; } } export const gameClock = new GameClock();