v0.65.0: per-airport job pools with real-time refresh — open jobs spawn per pool airport (grounded/flying/queued locations) in a fixed POOL_BANDS composition, expire on the wall clock after JOB_LIFETIME_MS even while the world is frozen, and refill taken slots after a per-band cooldown; /api/state + SSE filter open jobs to pool origins and expose expiresRealMs for the right-pane countdown
CI / build + unit + e2e (pull_request) Failing after 17m26s

This commit is contained in:
2026-09-23 13:09:49 +00:00
parent 9c2c64fe89
commit 12d900b22a
24 changed files with 556 additions and 344 deletions
+41 -17
View File
@@ -5,6 +5,7 @@ import { getGameDb } from '@/lib/db/client';
import * as schema from '@/lib/db/schema';
import { catalogEntries, FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION, type CatalogEntry } from './catalog';
import { fuelAtWeighted, progressAt, positionAt, windBurnFactor } from '@/lib/sim/movement';
import { JOB_LIFETIME_MS } from './seed';
// Read-only game-state snapshot for client polling (GET /api/state).
@@ -49,6 +50,20 @@ export function parseIcaoQueue(raw: string | null): string[] {
}
}
// v0.65.0: the airports where a plane can take work — grounded there, flying TO it (its destination
// joins the pool while airborne), or queued for it. Repairing planes count nowhere: a fleet in the
// shop has no pools until one frees up. Shared by the snapshot's visibility filter and the engine's
// pool spawner so both agree on exactly which airports keep open jobs.
export function poolAirports(planes: Array<{ status: string; locationIcao: string; destIcao: string | null; queue: string | null }>): Set<string> {
const set = new Set<string>();
for (const p of planes) {
if (p.status === 'available') set.add(p.locationIcao);
else if (p.status === 'flying' && p.destIcao) set.add(p.destIcao);
for (const icao of parseIcaoQueue(p.queue)) set.add(icao);
}
return set;
}
export interface PackageState {
id: string;
originIcao: string;
@@ -69,6 +84,9 @@ export interface PackageState {
status: 'open' | 'loaded' | 'delivered' | 'expired' | 'abandoned' | 'ignored';
planeId: string | null;
createdSimTime: number;
// Wall-clock expiry of an OPEN job (v0.65.0) — createdRealMs + JOB_LIFETIME_MS, shown as a live
// countdown in the right pane; null for every non-open status (loaded cargo still uses deadlineSimTime).
expiresRealMs: number | null;
}
export interface StateSnapshot {
@@ -121,6 +139,11 @@ export async function getStateSnapshot(): Promise<StateSnapshot> {
return from && to ? haversineKm(from.lat, from.lon, to.lat, to.lon) : 0;
};
// v0.65.0: pool visibility — open packages appear only where a plane can take work (poolAirports);
// every other status is always visible (on-board cargo, history). Filtered rows are absent from the
// body entirely, so clients never see jobs they could not dispatch.
const pool = poolAirports(planeRows);
// Analytic movement (v0.7.0): a flying plane's stored lat/lon/fuel are its
// departure values — live position and fuel are pure functions of the clock.
const livePlane = (p: schema.PlaneRow) => {
@@ -171,22 +194,23 @@ export async function getStateSnapshot(): Promise<StateSnapshot> {
repairUntilSimTime: p.repairUntilSimTime,
};
}),
packages: packageRows.map((p) => ({
id: p.id,
originIcao: p.originIcao,
originName: nameOf(p.originIcao),
destIcao: p.destIcao,
destName: nameOf(p.destIcao),
reward: p.reward,
cargoType: p.cargoType,
cargoCount: p.cargoCount,
weightKg: p.weightKg,
special: p.special,
distanceKm: legDistanceKm(p.originIcao, p.destIcao), // v0.48.0 (ISS-095)
deadlineSimTime: p.deadlineSimTime,
status: p.status,
planeId: p.planeId,
createdSimTime: p.createdSimTime,
})),
packages: packageRows.filter((p) => p.status !== 'open' || pool.has(p.originIcao)).map((p) => ({
id: p.id,
originIcao: p.originIcao,
originName: nameOf(p.originIcao),
destIcao: p.destIcao,
destName: nameOf(p.destIcao),
reward: p.reward,
cargoType: p.cargoType,
cargoCount: p.cargoCount,
weightKg: p.weightKg,
special: p.special,
distanceKm: legDistanceKm(p.originIcao, p.destIcao), // v0.48.0 (ISS-095)
deadlineSimTime: p.deadlineSimTime,
status: p.status,
planeId: p.planeId,
createdSimTime: p.createdSimTime,
expiresRealMs: p.status === 'open' ? p.createdRealMs + JOB_LIFETIME_MS : null,
})),
};
}