orion-wars

Battle Arena Brief — archived implementation brief

Status (2026-09-03): implemented and superseded. This document records the original watch-only viewer assignment. The live browser suite now also has a scenario editor and interactive playfield; docs/playfield-contract.md, docs/scenario-format.md, arena/README.md and the code take precedence. Current harness budgets are 4 / 18 / 32 / 52 / 68 / 132, the live map is a 72×40 rectangle, roster ships use manifest icons (never chevrons), and only the replay viewer can operate without a server.

Build a simple browser arena for observing battles from Distant Sectors’ tactical combat engine. Watch-only for now; it will grow into the interactive tactical playfield later, so keep the bones clean.

Original architecture rules (historical)

  1. The engine stays headless. Everything under src/ runs with no DOM. The arena is a CLIENT that consumes recorded battle data. Do not import DOM-touching code into src/, and do not move engine logic into the viewer.
  2. Record, then replay. Do not couple the viewer to live engine internals. The pipeline is: a small Node script runs one battle and writes a replay JSON; the viewer is a static page that loads and animates that JSON.
  3. One permitted engine touch. runBattle(fleets, tuning, rng, opts) in src/tactical/resolver.js already accepts opts.log. You may add ONE optional callback, opts.onRound(turn, round, fleets), invoked at the end of each round (after the detonation pass), passing the live fleet arrays for snapshotting. Nothing else in src/ may change. If you also want weapon-fire events for animation, do NOT restructure fire() — derive what you can from opts.log lines and round-to-round state diffs, or add an equally minimal opts.onShot(event) guarded so it is a no-op when absent.
  4. Do not touch data/ (balance constants are under active tuning) and do not modify test/fleet-trial.js or test/harness.js.
  5. No build step, no frameworks, no npm dependencies. Plain ES modules, vanilla JS, one static HTML page. Canvas or SVG, your choice.

Part 1 — the recorder: test/record-battle.js

CLI: node test/record-battle.js --a EAR --b KRE --points 52 --seed mybattle --out arena/replay.json

Part 2 — the viewer: arena/index.html (+ arena/arena.js, arena/arena.css)

A static page; opening it with a replay loaded shows the battle.

Part 3 — convenience

Original acceptance checks

  1. node test/harness.js --quiet still passes (engine untouched apart from the optional callback).
  2. node test/fleet-trial.js --battles 30 output unchanged vs. before your change (the callback must be zero-cost when absent).
  3. Recording the same seed twice produces byte-identical JSON.
  4. The two bundled replays play start-to-finish in the viewer with no console errors, in a plain browser, no server.

Context for good taste (read, don’t re-litigate)


Addendum — weapon fire effects (ruling, 2026-08-30)

We want to SEE the shooting: beams and both missile types, with glows.

Replay data (one more minimal engine touch, sanctioned like onRound)

Add optional opts.onShot(event) to runBattle — a guarded no-op when absent, consuming NO rng draws (determinism must be untouched; acceptance re-checks byte-identical fleet-trial output). Suggested pattern: runBattle wraps the caller’s onShot in a closure that stamps the current {turn, round} before handing it down, so fire() needs no signature change.

Events (all carry turn, round):

The recorder captures events into the replay as shots: [...]; bump the replay meta.version and re-record the bundled replays. The viewer resolves positions from the round snapshots by ship id (a dead ship’s last snapshot position is fine for a fading missile).

Visuals (canvas, glows via radial gradients / shadowBlur)

Rules unchanged

src/ read-only EXCEPT the single onShot addition to resolver.js. data/, test/fleet-trial.js, test/harness.js untouched. Same four acceptance checks as the base brief, plus: byte-identical fleet-trial output proves the callback costs nothing when absent.