Files
Flight-Ops/lib/game/state.ts
T

115 lines
4.1 KiB
TypeScript

import { loadAirportsGeoJson } from '@/lib/airports';
import { haversineKm } from '@/lib/geo';
import { getGameDb } from '@/lib/db/client';
import * as schema from '@/lib/db/schema';
import { PLANE_TYPES } from './catalog';
import { fuelAt, progressAt, positionAt } from '@/lib/sim/movement';
// Read-only game-state snapshot for client polling (GET /api/state).
export interface PlaneState {
id: string;
model: string;
status: 'available' | 'flying';
fuel: number;
maxFuel: number;
locationIcao: string;
locationName: string;
contractId: string | null;
lat: number;
lon: number;
}
export interface ContractState {
id: string;
originIcao: string;
originName: string;
destIcao: string;
destName: string;
reward: number;
cargoType: string;
cargoCount: number;
deadlineSimTime: number;
status: 'open' | 'accepted' | 'completed' | 'expired';
planeId: string | null;
createdSimTime: number;
}
export interface StateSnapshot {
account: { cash: number; reputation: number };
simState: { clockMs: number; timeScale: number; paused: boolean };
planes: PlaneState[];
contracts: ContractState[];
}
export async function getStateSnapshot(): Promise<StateSnapshot> {
const { db } = await getGameDb();
// NOTE: use plain select(), not the relational db.query.* API — in drizzle-orm
// 0.45.x the sql-js driver drops the result mapper for relational queries and
// returns raw rows keyed by SQL column names (snake_case, no type decoding).
const [accountRows, simRows, planeRows, contractRows] = await Promise.all([
db.select().from(schema.account),
db.select().from(schema.simState),
db.select().from(schema.planes),
db.select().from(schema.contracts),
]);
const accountRow = accountRows[0];
const simRow = simRows[0];
if (!accountRow || !simRow) throw new Error('game state missing account/sim_state row');
const fc = await loadAirportsGeoJson();
const nameByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties.name]));
const nameOf = (icao: string) => nameByIcao.get(icao) ?? icao;
const coordsByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties]));
// 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 contractById = new Map(contractRows.map((c) => [c.id, c]));
const livePlane = (p: schema.PlaneRow) => {
if (p.status !== 'flying' || p.departureSimTime == null || !p.contractId) return null;
const contract = contractById.get(p.contractId);
const type = PLANE_TYPES[p.model];
const from = contract ? coordsByIcao.get(contract.originIcao) : undefined;
const to = contract ? coordsByIcao.get(contract.destIcao) : undefined;
if (!contract || !type || !from || !to) return null;
const distanceKm = haversineKm(from.lat, from.lon, to.lat, to.lon);
const progress = progressAt(p.departureSimTime, distanceKm, type.speedKmh, simRow.clockMs);
const pos = positionAt(from.lat, from.lon, to.lat, to.lon, progress);
return { ...pos, fuel: fuelAt(p.fuel, type.burnPerKm, distanceKm, progress) };
};
return {
account: { cash: accountRow.cash, reputation: accountRow.reputation },
simState: { clockMs: simRow.clockMs, timeScale: simRow.timeScale, paused: simRow.paused },
planes: planeRows.map((p) => {
const live = livePlane(p);
return {
id: p.id,
model: p.model,
status: p.status,
fuel: live?.fuel ?? p.fuel,
maxFuel: p.maxFuel,
locationIcao: p.locationIcao,
locationName: nameOf(p.locationIcao),
contractId: p.contractId,
lat: live?.lat ?? p.lat,
lon: live?.lon ?? p.lon,
};
}),
contracts: contractRows.map((c) => ({
id: c.id,
originIcao: c.originIcao,
originName: nameOf(c.originIcao),
destIcao: c.destIcao,
destName: nameOf(c.destIcao),
reward: c.reward,
cargoType: c.cargoType,
cargoCount: c.cargoCount,
deadlineSimTime: c.deadlineSimTime,
status: c.status,
planeId: c.planeId,
createdSimTime: c.createdSimTime,
})),
};
}