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
+8 -36
View File
@@ -21,7 +21,7 @@ import {
repRate,
SELL_FRACTION,
} from './catalog';
import { HUB_ICAO } from './seed';
import { HUB_ICAO, STARTING_CLOCK_MS } from './seed';
import {
abandonPackage,
buyPlane,
@@ -173,7 +173,7 @@ describe('flyPlane', () => {
const plane = (await inst.db.select().from(schema.planes))[0];
expect(plane.status).toBe('flying');
expect(plane.destIcao).toBe('LEMD');
expect(plane.departureSimTime).toBe(T0); // seeded clock, no tick in direct calls
expect(plane.departureSimTime).toBe(STARTING_CLOCK_MS); // seeded game clock (ISS-117), no tick in direct calls
const fc = await loadAirportsGeoJson();
const origin = fc.features.find((f) => f.properties.icao === HUB_ICAO)!.properties;
expect(plane.lat).toBe(origin.lat);
@@ -182,34 +182,6 @@ describe('flyPlane', () => {
expect(plane.fuel).toBe(PLANE_TYPES.atr72.maxFuel);
});
it('resumes a manual pause and restores lastSelectedTimeScale when the last grounded plane takes off (ISS-057)', async () => {
const inst = await freshDb('fly-resume.db');
// A landing slowed the sim to 1×; the player's last manually selected speed was 250.
await inst.db.update(schema.simState).set({ paused: true, timeScale: 1, lastSelectedTimeScale: 250 });
await flyPlane(inst, 'plane-starter', 'LEMD'); // sole grounded plane → airborne
const sim = (await inst.db.select().from(schema.simState))[0];
expect(sim.paused).toBe(false); // dispatch still resumes a manual pause…
expect(sim.timeScale).toBe(250); // …and restores the last selected speed
});
it('keeps 1× when another plane is still grounded after dispatch (ISS-057)', async () => {
const inst = await freshDb('fly-stayslow.db');
await inst.db.update(schema.simState).set({ timeScale: 1, lastSelectedTimeScale: 250 });
// A second grounded plane — the starter taking off leaves it on the ground.
await inst.db.insert(schema.planes).values({
id: 'plane-extra', model: 'atr72', status: 'available', fuel: 100, maxFuel: PLANE_TYPES.atr72.maxFuel,
locationIcao: HUB_ICAO, destIcao: null, lat: 50, lon: 8, departureSimTime: null,
});
await flyPlane(inst, 'plane-starter', 'LEMD');
const sim = (await inst.db.select().from(schema.simState))[0];
expect(sim.paused).toBe(false);
expect(sim.timeScale).toBe(1); // another plane on ground → stays slowed
});
it('rejects a flying plane with 409', async () => {
const inst = await freshDb('fly-flying.db');
await inst.db.update(schema.planes)
@@ -462,14 +434,14 @@ describe('cancelQueuedLeg (v0.25.0, ISS-043)', () => {
});
describe('abandonPackage', () => {
// A loaded package created at T0 with a 10h window; the seeded sim clock is T0.
// A loaded package created at the seeded game clock with a 10h window (ISS-117).
const REWARD = 1_000;
const WINDOW_MS = 10 * 3_600_000;
async function setupLoaded(name: string, status: 'loaded' | 'expired' = 'loaded') {
const inst = await freshDb(name);
// Always insert OPEN and load it through the real action…
await insertOpenPackage(inst, 'p-ab', { reward: REWARD, createdSimTime: T0, deadlineSimTime: T0 + WINDOW_MS });
await insertOpenPackage(inst, 'p-ab', { reward: REWARD, createdSimTime: STARTING_CLOCK_MS, deadlineSimTime: STARTING_CLOCK_MS + WINDOW_MS });
await loadPackage(inst, 'p-ab', 'plane-starter');
// …then flip to expired in place (what the tick would do once the deadline passes).
if (status === 'expired') {
@@ -494,7 +466,7 @@ describe('abandonPackage', () => {
it('scales to exactly 37.5% at the midpoint of the window', async () => {
const inst = await setupLoaded('ab-mid.db');
await inst.db.update(schema.simState).set({ clockMs: T0 + WINDOW_MS / 2 }); // runAction ticks first in prod
await inst.db.update(schema.simState).set({ clockMs: STARTING_CLOCK_MS + WINDOW_MS / 2 }); // runAction ticks first in prod
const { penalty } = await abandonPackage(inst, 'p-ab');
@@ -503,8 +475,8 @@ describe('abandonPackage', () => {
it('caps at exactly 60% at and past the deadline', async () => {
for (const [name, clock] of [
['ab-deadline.db', T0 + WINDOW_MS],
['ab-pastdeadline.db', T0 + WINDOW_MS * 2], // clamped — a not-yet-ticked loaded package past its deadline
['ab-deadline.db', STARTING_CLOCK_MS + WINDOW_MS],
['ab-pastdeadline.db', STARTING_CLOCK_MS + WINDOW_MS * 2], // clamped — a not-yet-ticked loaded package past its deadline
] as const) {
const inst = await setupLoaded(name);
await inst.db.update(schema.simState).set({ clockMs: clock });
@@ -735,7 +707,7 @@ describe('repairPlane (v0.39.0, ISS-069; gradual since v0.45.0, ISS-089)', () =>
const plane = (await inst.db.select().from(schema.planes))[0];
expect(plane.status).toBe('repairing');
expect(plane.health).toBe(42); // NOT restored yet — the engine's finishRepairs pass heals it gradually…
expect(plane.repairUntilSimTime).toBe(T0 + REPAIR_DURATION_MS); // …with the gate set from the seeded clock (no tick in direct calls)
expect(plane.repairUntilSimTime).toBe(STARTING_CLOCK_MS + REPAIR_DURATION_MS); // …with the gate set from the seeded game clock (ISS-117; no tick in direct calls)
const account = (await inst.db.select().from(schema.account))[0];
expect(account.cash).toBeCloseTo(cashBefore - REPAIR_COST, 6);
});