Fixed world clock: no player time controls (ISS-117)
CI / build + unit + e2e (pull_request) Failing after 12m10s

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.
This commit is contained in:
2026-09-22 10:50:21 +00:00
parent d31bc5d60f
commit a0560ec767
46 changed files with 631 additions and 1502 deletions
+12 -8
View File
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { WORLD_TIME_SCALE } from '@/lib/worldClock';
import { haversineKm } from '@/lib/geo';
import { PLANE_TYPES, repRate, STARTER_MODEL } from './catalog';
import { SPECIALS } from './specials';
@@ -18,6 +19,7 @@ import {
REWARD_PER_KM,
rewardFor,
STARTING_CASH,
STARTING_CLOCK_MS,
STARTING_REPUTATION,
type Rng,
type SeedAirport,
@@ -73,7 +75,7 @@ describe('buildSeed', () => {
expect(c.originIcao).not.toBe(c.destIcao);
expect(c.status).toBe('open');
expect(c.planeId).toBeNull();
expect(c.createdSimTime).toBe(NOW);
expect(c.createdSimTime).toBe(STARTING_CLOCK_MS); // game-clock origin, not wall time (ISS-117)
}
expect(new Set(seed.packages.map((c) => c.id)).size).toBe(seed.packages.length);
});
@@ -101,22 +103,23 @@ describe('buildSeed', () => {
}
});
it('sets deadlines inside the 6–72h game-time window after now (scaled by any special status)', () => {
it('sets deadlines inside the 6–72h game-time window after the game-clock origin (scaled by any special status)', () => {
const seed = seedWith(42);
for (const c of seed.packages) {
// v0.37.0 (ISS-073): a special's deadlineScale tightens the drawn window (e.g. priority → 3–36h).
// ISS-117: the window is anchored at STARTING_CLOCK_MS, not wall time.
const scale = c.special ? SPECIALS[c.special]?.deadlineScale ?? 1 : 1;
expect(c.deadlineSimTime).toBeGreaterThan(NOW + 6 * 3_600_000 * scale - 1);
expect(c.deadlineSimTime).toBeLessThanOrEqual(NOW + 72 * 3_600_000 * scale);
expect(c.deadlineSimTime).toBeGreaterThan(STARTING_CLOCK_MS + 6 * 3_600_000 * scale - 1);
expect(c.deadlineSimTime).toBeLessThanOrEqual(STARTING_CLOCK_MS + 72 * 3_600_000 * scale);
}
});
it('seeds the starting account and sim state', () => {
const seed = seedWith(42);
expect(seed.account).toEqual({ cash: STARTING_CASH, reputation: STARTING_REPUTATION });
// lastRealTickMs starts at the seed moment so the first tick-on-read advances ~0;
// lastSelectedTimeScale mirrors the seeded speed (v0.30.0, ISS-057).
expect(seed.simState).toEqual({ clockMs: NOW, timeScale: 1, paused: false, lastRealTickMs: NOW, lastSelectedTimeScale: 1 });
// ISS-117 (v0.52.0): game clock starts at year 2000 with the fixed world rate; lastRealTickMs is
// the REAL seed moment so the first tick-on-read advances ~0. paused/lastSelectedTimeScale are legacy columns.
expect(seed.simState).toEqual({ clockMs: STARTING_CLOCK_MS, timeScale: WORLD_TIME_SCALE, paused: false, lastRealTickMs: NOW, lastSelectedTimeScale: WORLD_TIME_SCALE });
});
it('is fully deterministic for a fixed RNG and inputs', () => {
@@ -186,7 +189,8 @@ describe('generatePackage (shared seed/spawn builder)', () => {
const count = PACKAGE_COUNT_MIN + Math.floor(rngA() * (PACKAGE_COUNT_MAX - PACKAGE_COUNT_MIN + 1));
const expected: string[] = [];
for (let i = 0; i < count; i++) {
expected.push(JSON.stringify(generatePackage({ id: `package-${i + 1}`, airports: AIRPORTS, rng: rngA, nowMs: NOW })));
// ISS-117: buildSeed anchors packages at the game-clock origin, not the wall-time input.
expected.push(JSON.stringify(generatePackage({ id: `package-${i + 1}`, airports: AIRPORTS, rng: rngA, nowMs: STARTING_CLOCK_MS })));
}
const seed = buildSeed({ airports: AIRPORTS, rng: mulberry32(42), nowMs: NOW });
expect(seed.packages.map((c) => JSON.stringify(c))).toEqual(expected);