62 lines
2.7 KiB
TypeScript
62 lines
2.7 KiB
TypeScript
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;
|
|
}
|