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 Clock singleton object exposing now, 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() or Date.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 by 1000 / 60 ms (≈16.666 ms) and resets the sub-counter to zero. Called exclusively from loop.ts::stepSimulation.
  • Clock.reset(): void — sets sim time and sub-counter to zero. Called from loop.ts at new-game start and from bridge.ts sandbox/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 by loop.ts; the clock just trusts that each call corresponds to one fixed step.
  • Single-writer invariant: advanceSimStep is intended to be invoked only from loop.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: _simTimeMs and _subCounter are file-private let bindings, exposed only through the Clock const. 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.