v0.62.0: fixed 24x world clock — frozen when fully grounded (#135)
CI / build + unit + e2e (pull_request) Successful in 14m3s

This commit is contained in:
2026-09-23 08:09:29 +00:00
parent e970f95036
commit bb4e3b96f3
14 changed files with 122 additions and 124 deletions
+7 -8
View File
@@ -2,16 +2,15 @@
import { useEffect, useRef, useState } from 'react';
import type { StateSnapshot } from './game/state';
import { IDLE_WORLD_TIME_SCALE, WORLD_TIME_SCALE } from './worldClock';
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; ISS-128 (v0.56.0): while nothing is happening it slows to
// IDLE_WORLD_TIME_SCALE (real time) instead of freezing — mirroring the server's idle slowdown,
// so polls re-anchor without a visible jump.
// fixed WORLD_TIME_SCALE; #135 (v0.62.0): while nothing is happening it stays FROZEN at the anchor
// value — mirroring the server's freeze, so polls re-anchor without any jump.
export interface SimClockAnchor {
// Server truth at anchor time — the snapshot's simState.clockMs.
@@ -23,11 +22,11 @@ export interface SimClockAnchor {
}
// Pure core of the extrapolation — mirrors tickOnce: while advancing, game time advances by
// real-time × WORLD_TIME_SCALE; while idle it slows to real-time × IDLE_WORLD_TIME_SCALE (ISS-128).
// real-time × WORLD_TIME_SCALE; while fully grounded it stays frozen at the anchor value (#135).
// nowMs before `at` clamps to the anchor value instead of going backwards. Unit-testable in node.
export function advanceSimClock(anchor: SimClockAnchor, nowMs: number): number {
const scale = anchor.advancing ? WORLD_TIME_SCALE : IDLE_WORLD_TIME_SCALE;
return anchor.clockMs + Math.max(0, nowMs - anchor.at) * scale;
if (!anchor.advancing) return anchor.clockMs; // frozen while everything is grounded (#135)
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.
@@ -43,7 +42,7 @@ export function useSimClock(snapshot: StateSnapshot | null): number | null {
useEffect(() => {
if (!snapshot) return;
// Mirror of the server's shouldAdvanceTime (lib/sim/engine.ts): full rate only while a plane
// is flying or in repair, idle real-time otherwise. One line on purpose — keep it in sync.
// is flying or in repair, frozen otherwise (#135). One line on purpose — keep it in sync.
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()));