/** * The body a snapshot must carry, stated once. * * The client renderer (`scene.js`, `topdown.js`, `presence.js`, `gun.js`) * reads fixed fields off every body in `snap.players` and off `snap.me`. A * game that renames one — `down` for `alive` — is not refused by anything at * runtime: every enemy simply draws as a corpse. So the shape is a contract * here, and each game tests its real snapshot against it. * * Every body in `snap.players` has: * id: string who; `me` is skipped in the scene by this id * name: string the tag over the body * x, y: number where, in world units * angle: number which way it faces, radians * alive: boolean false draws a corpse * hp: number 0..100; a bar shows under 100 * mate: boolean on the viewer's side * * and may have: * side: string which side it is on, for a game of three sides or * more; each non-mate side then draws in its own colour * (`static/engine/sides.js`). Leave it out and every * non-mate draws in the one enemy colour, as before. * * `snap.me` has, besides id, x, y, angle, alive and hp: * ammo: number rounds in the magazine * reloading: boolean mid-reload */ type Kind = "string" | "number" | "boolean"; const BODY: Record = { id: "string", name: "string", x: "number", y: "number", angle: "number", alive: "boolean", hp: "number", mate: "boolean", }; /** Fields a body may leave out, but must type right when it sends them. */ const BODY_OPTIONAL: Record = { side: "string", }; const ME: Record = { id: "string", x: "number", y: "number", angle: "number", alive: "boolean", hp: "number", ammo: "number", reloading: "boolean", }; function problems(value: unknown, shape: Record, label: string, optional: Record = {}): string[] { if (typeof value !== "object" || value === null) return [`${label} is not an object`]; const out: string[] = []; const fields = [...Object.entries(shape), ...Object.entries(optional)]; for (const [field, kind] of fields) { const got = (value as Record)[field]; if (got === undefined && field in optional) continue; if (typeof got !== kind || (kind === "number" && !Number.isFinite(got))) out.push(`${label}.${field} should be a ${kind}, is ${String(got)}`); } return out; } /** What is wrong with one body of `snap.players`; empty when it is fit to draw. */ export const bodyProblems = (body: unknown, label = "body"): string[] => problems(body, BODY, label, BODY_OPTIONAL); /** What is wrong with `snap.me`; empty when the gun and the camera can read it. */ export const meProblems = (me: unknown): string[] => problems(me, ME, "me"); /** Every problem in a snapshot's bodies and `me`. */ export function snapshotProblems(snap: { me?: unknown; players?: unknown }): string[] { const players = Array.isArray(snap.players) ? snap.players : null; return [ ...meProblems(snap.me), ...(players ? players.flatMap((p, i) => bodyProblems(p, `players[${i}]`)) : ["players is not an array"]), ]; }