115 lines
4.1 KiB
TypeScript
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,
|
|
})),
|
|
};
|
|
}
|