Files
Flight-Ops/lib/useSimClock.ts
T
kamilkrzywda a0560ec767
CI / build + unit + e2e (pull_request) Failing after 12m10s
Fixed world clock: no player time controls (ISS-117)
The world always runs at WORLD_TIME_SCALE = 24 (lib/worldClock.ts). A server
background tick (instrumentation.ts -> startBackgroundTick, 1 s interval)
advances the sim with no open browser; idle-pause freezes the clock exactly
while no plane is flying or in repair. Fresh worlds start year 2000
(STARTING_CLOCK_MS in seed.ts).

- Removed: POST /api/sim + OpenAPI entry, applyTimeControls/clampTimeScale,
  lib/timeScale.ts, LeftPane pause button + scale slider machinery,
  ISS-057 slow-on-landing/resume in flyPlane.
- Snapshot simState is now { clockMs } only; client extrapolates at the fixed
  rate between polls and freezes while idle (useSimClock).
- New test-only hook POST /api/test/set-timescale behind FLIGHT_TEST_HOOKS=1
  (sibling of set-health); all e2e setScale helpers call it, afterEach restores
  the world rate. E2E pause tests replaced by idle-pause assertions; sim.spec.ts
  activates the world via set-health + /repair for advance/expiry tests.
2026-09-22 10:50:21 +00:00

63 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client';
import { useEffect, useRef, useState } from 'react';
import type { StateSnapshot } from './game/state';
import { WORLD_TIME_SCALE } from './worldClock';
// ISS-007 (v0.16.1): the sim clock ticks smoothly on the CLIENT between polls instead of
// jumping once per snapshot. The server's authoritative rate is exactly real-time × scale
// (tickOnce in lib/sim/engine.ts: advance = min(elapsed, MAX_TICK_ELAPSED_MS) × timeScale), so
// client-side extrapolation mirrors the server's own model. Every new snapshot re-anchors to
// server truth, bounding any drift by network latency only. ISS-117 (v0.52.0): the scale is the
// fixed WORLD_TIME_SCALE and the clock freezes while nothing is happening — mirroring the
// server's idle-pause instead of a player-controlled pause.
export interface SimClockAnchor {
// Server truth at anchor time — the snapshot's simState.clockMs.
clockMs: number;
// Date.now() when that snapshot was received.
at: number;
// Whether the world clock is advancing (any plane flying or in repair).
advancing: boolean;
}
// Pure core of the extrapolation — mirrors tickOnce: while advancing, game time advances by
// real-time × WORLD_TIME_SCALE; while idle it is frozen (advance 0). nowMs before `at` clamps to
// the anchor value instead of going backwards. Unit-testable in node.
export function advanceSimClock(anchor: SimClockAnchor, nowMs: number): number {
if (!anchor.advancing) return anchor.clockMs;
return anchor.clockMs + Math.max(0, nowMs - anchor.at) * WORLD_TIME_SCALE;
}
// ~10 fps display refresh — smooth for a seconds-precision wall clock without busy re-renders.
const DISPLAY_TICK_MS = 100;
// Returns the display clockMs to render, or null until the first snapshot arrives.
export function useSimClock(snapshot: StateSnapshot | null): number | null {
const anchorRef = useRef<SimClockAnchor | null>(null);
const [displayMs, setDisplayMs] = useState<number | null>(null);
// Re-sync on every snapshot: snap to server truth. The snapshot object is fresh per poll
// (useGameState), so this runs once per poll cycle as well.
useEffect(() => {
if (!snapshot) return;
// Mirror of the server's shouldAdvanceTime (lib/sim/engine.ts): frozen while no plane is
// flying or in repair. One line on purpose — keep it in sync with that function.
const advancing = snapshot.planes.some((p) => p.status === 'flying' || p.status === 'repairing');
anchorRef.current = { clockMs: snapshot.simState.clockMs, at: Date.now(), advancing };
setDisplayMs(advanceSimClock(anchorRef.current, Date.now()));
}, [snapshot]);
useEffect(() => {
const id = setInterval(() => {
const anchor = anchorRef.current;
if (anchor) setDisplayMs(advanceSimClock(anchor, Date.now()));
}, DISPLAY_TICK_MS);
return () => clearInterval(id);
}, []);
// First paint after a snapshot arrives happens before the re-sync effect runs — show the
// snapshot value directly so the clock never blanks for a frame.
return displayMs ?? (snapshot ? snapshot.simState.clockMs : null);
}