@@ 73,3 73,100 @@ Third milestone of the `ftl-shape` vertical slice. Adds the data + UI layer for
### Unknowns
- UK1 — Whether 30% dimming on unpowered rooms is enough visual signal at 640×360 zoom. Resolved at verify by browser smoke.
+
+## Plan
+
+Approach: TDD-first on the two pure units (systems data, then HUD geometry), then one integration phase that wires `drawHud`, room dimming, and right-click input. Greenfield feature; no backwards-compat risk.
+
+### PH1 — Systems data model (TDD)
+
+- **1.1** `systems_test.go` (create) — tests first, must fail before 1.2.
+ - `TestNewStartingSystems_layout` — returns 5 systems; indexed by `int(RoomRole)` so `sys[int(RolePilot)].Role == RolePilot` etc.; caps are Pilot 1, Weapons 4, Shields 4, MedBay 2, Engines 4; all `PowerLevel == 0`.
+ - `TestReactorUsed_zero` — fresh systems → `reactorUsed == 0`.
+ - `TestReactorUsed_sums` — set arbitrary `PowerLevel`s → returns the sum.
+ - `TestAddPower_happy` — level<cap, reactor not full → returns true, level becomes `n+1`.
+ - `TestAddPower_systemAtCap` — level==MaxLevel → returns false, level unchanged.
+ - `TestAddPower_reactorFull` — `reactorUsed == ReactorCap` and target system below its cap → returns false, level unchanged.
+ - `TestAddPower_invariantsHold` — after every `addPower` call (pass/fail), `reactorUsed ≤ ReactorCap` and every `0 ≤ PowerLevel ≤ MaxLevel`. Covers IV3, IV4.
+ - `TestRemovePower_happy` — level>0 → returns true, level becomes `n-1`.
+ - `TestRemovePower_zeroLevel` — level==0 → returns false, level unchanged.
+- **1.2** `systems.go` (create)
+ - `package main`; no Ebitengine import. Respects: IV1.
+ - `const ReactorCap = 8`.
+ - `type System struct { Role RoomRole; PowerLevel, MaxLevel int }`.
+ - `func NewStartingSystems() []System` — slice of 5 entries indexed by `int(RoomRole)`; caps as in the test.
+ - `func reactorUsed(sys []System) int` — sum.
+ - `func addPower(sys []System, role RoomRole, reactorCap int) bool` — locate by `Role` field (linear scan, 5 elements); reject if at cap or `reactorUsed == reactorCap`; else `++PowerLevel`. Respects: IV2, IV3, IV4.
+ - `func removePower(sys []System, role RoomRole) bool` — locate by `Role`; reject if level==0; else `--PowerLevel`. Respects: IV2, IV4.
+- Commit: `systems-power: System data + addPower/removePower with tests`
+
+### PH2 — HUD geometry & hit-test (TDD)
+
+- **2.1** `hud_test.go` (create) — tests first, must fail before 2.2.
+ - `TestHudHitTest_aboveHud` — `py < HudY` → `ok=false`.
+ - `TestHudHitTest_leftOfColumns` — `px < HudX` (reactor-readout band) → `ok=false`.
+ - `TestHudHitTest_rightOfColumns` — `px >= HudX + 5*HudColW` → `ok=false`.
+ - `TestHudHitTest_eachColumnCenter` — pixel at the centre of column `i` → returns `RoomRole(i)`, `ok=true`. Loop over the 5 roles.
+ - `TestHudHitTest_columnEdges` — left edge of column `i` (inclusive) → role `i`; right edge (exclusive) → role `i+1` or `ok=false` for the last column.
+- **2.2** `hud.go` (create)
+ - `package main`; no Ebitengine import. Respects: IV1, IV6.
+ - `const ( HudY = 272; HudX = 200; HudColW = 40; HudColCount = 5 )`.
+ - `func hudHitTest(px, py int) (RoomRole, bool)` — gate on `py >= HudY` and `HudX <= px < HudX+HudColCount*HudColW`; column index `(px-HudX)/HudColW`; return `RoomRole(idx), true`. No gutter inside the strip — columns are flush to keep hit-test simple.
+- Commit: `systems-power: HUD geometry + hudHitTest with tests`
+
+### PH3 — Render & input integration (manual smoke)
+
+- **3.1** `render.go` (modify)
+ - In `roleColor`: extract / introduce `dimRoleColor(role RoomRole) color.RGBA` returning ~60% brightness of the powered tint (one switch, parallel to `roleColor`). Used by drawRoom when system unpowered. Respects: IV5.
+ - Modify `drawRoom(screen, r Room, powered bool)` (signature change) — picks `roleColor(r.Role)` if `powered`, else `dimRoleColor(r.Role)`. Border + label unchanged.
+ - Modify `drawShip(screen, s Ship, sys []System)` (signature change) — for each room, compute `powered := sys[int(r.Role)].PowerLevel > 0`, call `drawRoom(screen, r, powered)`. Respects: PC3 (read-only).
+ - Add `func drawHud(screen *ebiten.Image, sys []System)`:
+ - Reactor readout: `ebitenutil.DebugPrintAt(screen, fmt.Sprintf("Reactor: %d/%d", reactorUsed(sys), ReactorCap), 8, HudY+8)`.
+ - Per column `i` (0..4): role label at `(HudX + i*HudColW + 4, HudY+4)`; cells stacked bottom-up inside the column band, cell size 12×10 with 2 px gap, drawn from row 0 (bottom-most) up to `MaxLevel-1`. Cell `j < PowerLevel` filled with `roleColor`; cell `j ≥ PowerLevel` filled with `dimRoleColor`; 1-px white outline on every cell. Respects: PC3 (read-only), IV6.
+- **3.2** `main.go` (modify)
+ - `Game` gains `systems []System`. Init in `main()` via `NewStartingSystems()`.
+ - `Update`:
+ - On `inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft)`: read `cx, cy := ebiten.CursorPosition()`. If `cy >= HudY` → call `hudHitTest(cx, cy)`; if hit, `addPower(g.systems, role, ReactorCap)`, return nil. Else (HUD miss while in HUD band): no-op, return nil. If `cy < HudY` → fall through to existing crew-select / move logic, unchanged.
+ - On `inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonRight)`: if `cy >= HudY` and `hudHitTest` hits → `removePower(g.systems, role)`. Else no-op.
+ - `Draw`: replace `drawShip(screen, g.ship)` with `drawShip(screen, g.ship, g.systems)`; after the existing crew loop, call `drawHud(screen, g.systems)`. Respects: IV5.
+- **3.3** `web/index.html` (modify)
+ - Change `<body>` → `<body oncontextmenu="return false">` so right-click doesn't open the OS menu in the browser. Respects: AS2.
+- Commit: `systems-power: drawHud, room dimming, right-click input`
+
+### Test strategy
+
+Per Design `TDD: yes`. Phase 1 and Phase 2 tests are written first and must fail on the pre-implementation tree. Phase 3 has no automated tests — input and rendering need real mouse / display. Manual acceptance via `make play-web`:
+
+- Click each system column in the HUD: a power cell lights up, reactor readout increments, the corresponding room in the ship view brightens.
+- Right-click an HUD column: power cell unlights, reactor readout decrements, room dims.
+- Click a system column at its `MaxLevel`: no change.
+- Allocate up to `ReactorCap=8`: further left-clicks on any column become no-ops until power is removed elsewhere.
+- Click in the ship area (above HUD) with crew selected: crew movement still works (no regression).
+- Right-click in the ship area: no-op (no regression, no context menu in browser).
+
+### Order & dependencies
+
+- PH1 and PH2 are independent — different files, no shared symbols. Can be implemented in either order or in parallel.
+- PH3 depends on both: it consumes `addPower`, `removePower`, `reactorUsed`, `NewStartingSystems`, `ReactorCap`, `hudHitTest`, `HudY`, `HudX`, `HudColW`, `HudColCount`.
+
+### Risks / rollback
+
+- RK1 — `MouseButtonRight` not firing in WASM despite the `oncontextmenu` patch. Mitigation: verify in browser smoke first; if broken, fall back to `Shift+left-click` for power-down (one-line input branch swap, no data-model change).
+- RK2 — Dimmed rooms (60% brightness) too subtle at 640×360 — covered by UK1, resolved at verify; if subtle, bump to 50% or add a small overlay tag.
+- Rollback: each phase is its own commit; revert PH3 alone leaves PH1+PH2 dead-code but harmless (unexported helpers compile-clean), then revert PH2 / PH1 in turn.
+
+### Interfaces
+
+- IF1 — `func addPower(sys []System, role RoomRole, reactorCap int) bool` — defined in `systems.go`, consumed by `main.go` left-click handler.
+- IF2 — `func removePower(sys []System, role RoomRole) bool` — defined in `systems.go`, consumed by `main.go` right-click handler.
+- IF3 — `func reactorUsed(sys []System) int` — defined in `systems.go`, consumed by `render.go:drawHud` for the readout.
+- IF4 — `func hudHitTest(px, py int) (RoomRole, bool)` — defined in `hud.go`, consumed by `main.go` input handler.
+- IF5 — HUD constants `HudY, HudX, HudColW, HudColCount` and `ReactorCap` — defined in `hud.go` / `systems.go`, consumed by `render.go:drawHud` and `main.go` input gate.
+- IF6 — `type System struct{...}` and `func NewStartingSystems() []System` — defined in `systems.go`, consumed by `main.go` (init + input) and `render.go:drawShip / drawHud`.
+
+### Interface graph
+
+- PH1 -> IF1, IF2, IF3, IF6 @ systems.go, systems_test.go
+- PH2 -> IF4, IF5 @ hud.go, hud_test.go
+- PH3 IF1, IF2, IF3, IF4, IF5, IF6 -> @ render.go, main.go, web/index.html
+