PURPOSE
Deterministic simulation clock. Advances ONLY when the fixed-step sim ticks, so any gameplay code that reads Clock.now() produces byte-identical results given the same input seed. Replaces every performance.now() / Date.now() call in gameplay code. Wall-clock time is reserved for scheduler concerns only (setTimeout delays, telemetry log timestamps, render-perf diagnostics).
Units are milliseconds, matching performance.now() return semantics so call sites can be swapped 1:1 without arithmetic adjustments.
OWNS
- Module-private
_simTimeMs— milliseconds elapsed in sim time since last reset. - Module-private
_subCounter— sub-millisecond tie-breaker reset on every sim step. - Public
Clocksingleton object exposingnow,uniqueNow,advanceSimStep,reset.
READS FROM
Nothing. The clock has no inputs other than the call to advanceSimStep(). It does not read wall-clock time, frame deltas, or any external state.
PUSHES TO
Nothing directly. Clock.now() and Clock.uniqueNow() are pull-based — consumers read on demand. Sim time is consumed by weapons, spawners, visual pulses, artifact timers, crate ID generation, debris, HUD, draw, custom effect handlers, and the warp-puddle renderer.
DOES NOT
- Does not read
performance.now()orDate.now(). - Does not auto-advance on a timer or frame callback.
- Does not advance during render-only frames — only during fixed sim steps.
- Does not pause or scale time; speed/pause behavior lives elsewhere.
- Does not emit signals or notify subscribers when time advances.
- Does not persist across runs — state is module-local globals reset to zero on
reset().
Signals
None. The clock is a pure state container with no event emission.
Entry points
Clock.now(): number— returns current sim time in milliseconds. Deterministic given the same advancement history.Clock.uniqueNow(): number— returns sim time plus an increment of the sub-counter scaled by 0.001, providing a monotonically-unique timestamp within a single sim step. Intended for IDs (e.g. crate IDs) that must differ between call sites in the same tick.Clock.advanceSimStep(): void— increments sim time by1000 / 60ms (≈16.666 ms) and resets the sub-counter to zero. Called exclusively fromloop.ts::stepSimulation.Clock.reset(): void— sets sim time and sub-counter to zero. Called fromloop.tsat new-game start and frombridge.tssandbox/test reset so every test run begins with an identical clock.
Pattern notes
- Fixed-step advancement: the increment is hard-coded to 60 Hz (
1000 / 60). Sim cadence is owned byloop.ts; the clock just trusts that each call corresponds to one fixed step. - Single-writer invariant:
advanceSimStepis intended to be invoked only fromloop.ts::stepSimulation. Any other writer would break determinism guarantees. - Sub-counter pattern for unique IDs:
uniqueNow()returns_simTimeMs + (++_subCounter) * 0.001, giving each call within the same sim step a distinct, monotonically increasing value while keeping the integer-ms part stable for time comparisons. - Module-scope state:
_simTimeMsand_subCounterare file-privateletbindings, exposed only through theClockconst. There is no class instance or DI container. - Deterministic-replay contract: because the clock never reads wall-clock, runs are byte-identical given the same input seed — this is the foundation that lets weapons, spawners, artifact timers, and crate IDs all participate in deterministic replay.
- 1:1 swap with
performance.now(): by returning milliseconds, the API matches the wall-clock function it replaces, so migration of call sites is a name change with no math.