benjivers/val Public Public: anyone can read this; the crew can change it.
Files b42aa0d0f5a2520c6a2bd99fd90ea66615e6c986
| 1 | /** |
| 2 | * The body a snapshot must carry, stated once. |
| 3 | * |
| 4 | * The client renderer (`scene.js`, `topdown.js`, `presence.js`, `gun.js`) |
| 5 | * reads fixed fields off every body in `snap.players` and off `snap.me`. A |
| 6 | * game that renames one — `down` for `alive` — is not refused by anything at |
| 7 | * runtime: every enemy simply draws as a corpse. So the shape is a contract |
| 8 | * here, and each game tests its real snapshot against it. |
| 9 | * |
| 10 | * Every body in `snap.players` has: |
| 11 | * id: string who; `me` is skipped in the scene by this id |
| 12 | * name: string the tag over the body |
| 13 | * x, y: number where, in world units |
| 14 | * angle: number which way it faces, radians |
| 15 | * alive: boolean false draws a corpse |
| 16 | * hp: number 0..100; a bar shows under 100 |
| 17 | * mate: boolean on the viewer's side |
| 18 | * |
| 19 | * and may have: |
| 20 | * side: string which side it is on, for a game of three sides or |
| 21 | * more; each non-mate side then draws in its own colour |
| 22 | * (`static/engine/sides.js`). Leave it out and every |
| 23 | * non-mate draws in the one enemy colour, as before. |
| 24 | * |
| 25 | * `snap.me` has, besides id, x, y, angle, alive and hp: |
| 26 | * ammo: number rounds in the magazine |
| 27 | * reloading: boolean mid-reload |
| 28 | */ |
| 29 | |
| 30 | type Kind = "string" | "number" | "boolean"; |
| 31 | |
| 32 | const BODY: Record<string, Kind> = { |
| 33 | id: "string", |
| 34 | name: "string", |
| 35 | x: "number", |
| 36 | y: "number", |
| 37 | angle: "number", |
| 38 | alive: "boolean", |
| 39 | hp: "number", |
| 40 | mate: "boolean", |
| 41 | }; |
| 42 | |
| 43 | /** Fields a body may leave out, but must type right when it sends them. */ |
| 44 | const BODY_OPTIONAL: Record<string, Kind> = { |
| 45 | side: "string", |
| 46 | }; |
| 47 | |
| 48 | const ME: Record<string, Kind> = { |
| 49 | id: "string", |
| 50 | x: "number", |
| 51 | y: "number", |
| 52 | angle: "number", |
| 53 | alive: "boolean", |
| 54 | hp: "number", |
| 55 | ammo: "number", |
| 56 | reloading: "boolean", |
| 57 | }; |
| 58 | |
| 59 | function problems(value: unknown, shape: Record<string, Kind>, label: string, optional: Record<string, Kind> = {}): string[] { |
| 60 | if (typeof value !== "object" || value === null) return [`${label} is not an object`]; |
| 61 | const out: string[] = []; |
| 62 | const fields = [...Object.entries(shape), ...Object.entries(optional)]; |
| 63 | for (const [field, kind] of fields) { |
| 64 | const got = (value as Record<string, unknown>)[field]; |
| 65 | if (got === undefined && field in optional) continue; |
| 66 | if (typeof got !== kind || (kind === "number" && !Number.isFinite(got))) out.push(`${label}.${field} should be a ${kind}, is ${String(got)}`); |
| 67 | } |
| 68 | return out; |
| 69 | } |
| 70 | |
| 71 | /** What is wrong with one body of `snap.players`; empty when it is fit to draw. */ |
| 72 | export const bodyProblems = (body: unknown, label = "body"): string[] => problems(body, BODY, label, BODY_OPTIONAL); |
| 73 | |
| 74 | /** What is wrong with `snap.me`; empty when the gun and the camera can read it. */ |
| 75 | export const meProblems = (me: unknown): string[] => problems(me, ME, "me"); |
| 76 | |
| 77 | /** Every problem in a snapshot's bodies and `me`. */ |
| 78 | export function snapshotProblems(snap: { me?: unknown; players?: unknown }): string[] { |
| 79 | const players = Array.isArray(snap.players) ? snap.players : null; |
| 80 | return [ |
| 81 | ...meProblems(snap.me), |
| 82 | ...(players ? players.flatMap((p, i) => bodyProblems(p, `players[${i}]`)) : ["players is not an array"]), |
| 83 | ]; |
| 84 | } |