import type { StateSnapshot } from './game/state'; import { PLANE_TYPES } from './game/catalog'; import { formatCash } from './format'; // ISS-008 (v0.17.0): derive human-readable game events by diffing two consecutive /api/state // snapshots — there is no server event feed, so the client watches for transitions in the // fields it already receives. Pure and unit-tested; the shell calls it once per new snapshot // and turns each result into an info toast (plus a browser notification when hidden). export interface GameEvent { kind: 'info'; message: string; } const planeName = (model: string): string => PLANE_TYPES[model]?.name ?? model; export function diffSnapshots(prev: StateSnapshot | null, next: StateSnapshot): GameEvent[] { // No baseline yet (first snapshot after load) — nothing to compare against. Emitting a // "new package" event for every existing row would spam the stack on page load. if (prev === null) return []; const events: GameEvent[] = []; // Planes: landed (flying → available, new locationName is the arrival airport), bought or // sold (id set changed — ids are stable for a plane's whole life). const prevPlanes = new Map(prev.planes.map((p) => [p.id, p])); for (const plane of next.planes) { const before = prevPlanes.get(plane.id); if (!before) { events.push({ kind: 'info', message: `Bought ${planeName(plane.model)}` }); continue; } if (before.status === 'flying' && plane.status === 'available') { events.push({ kind: 'info', message: `${planeName(plane.model)} landed at ${plane.locationName}` }); } } for (const [id, before] of prevPlanes) { if (!next.planes.some((p) => p.id === id)) { events.push({ kind: 'info', message: `Sold ${planeName(before.model)}` }); } } // Packages: delivered (loaded → delivered, reward paid out in the same tick), expired // (open|loaded → expired), newly appeared (id only in next — terminal packages are never // deleted, so an id appearing is always a genuinely new one). const prevPackages = new Map(prev.packages.map((p) => [p.id, p])); for (const pkg of next.packages) { const before = prevPackages.get(pkg.id); if (!before) { events.push({ kind: 'info', message: `New package available: ${pkg.originName} → ${pkg.destName}` }); continue; } if (before.status === 'loaded' && pkg.status === 'delivered') { events.push({ kind: 'info', message: `Package delivered: +${formatCash(pkg.reward)}` }); } else if ((before.status === 'open' || before.status === 'loaded') && pkg.status === 'expired') { events.push({ kind: 'info', message: `Package expired: ${pkg.originName} → ${pkg.destName}` }); } } return events; }