57 lines
2.4 KiB
TypeScript
57 lines
2.4 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 diff against, so no events.
|
|
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) and expired
|
|
// (open|loaded → expired). Ids that appear for the first time emit nothing.
|
|
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) 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;
|
|
}
|