@@ 0,0 1,102 @@
+# combat
+
+**Branch:** `combat`
+
+Fourth and last gameplay milestone of the `ftl-shape` vertical slice. Adds a single combat encounter against a placeholder enemy ship; power allocation drives the loop (Weapons charge faster with more power, Shields absorb hits and regenerate, Engines provide an evasion %); auto-fire on ready; win/lose detection with R-to-restart.
+
+## Design
+
+**Purpose.** Make the systems-power data layer pay off. The player and a placeholder enemy ship trade fire under power-driven mechanics; the encounter is short and deterministic-given-RNG, with a clean win/lose state and a one-key restart. Combat is the climactic milestone before `polish`.
+
+**In scope.**
+- Enemy ship rendered to the right of the player ship (same room layout, shifted; no systems / no UI of its own).
+- Player ship gets a single weapon ("Laser I", damage 1). Charge rate = `WeaponsPower` charge-units / sec, threshold `WeaponChargeMax = 8`. On full charge, fires automatically, deals 1 to enemy, resets.
+- Enemy AI is a fixed timer: fires every `EnemyFireInterval = 6 s` for 1 damage at the player.
+- Shields: layers = `floor(ShieldsPower / 2)` (so 0/1=none, 2/3=1 layer, 4=2 layers). Each layer absorbs one hit. Layers regenerate continuously, one per `ShieldRegen = 3 s` of elapsed time, capped at the current max.
+- Engines: evasion % = `EnginesPower * 5`. Each incoming hit rolls; on success the shot misses (no shield consumed, no hull damage).
+- Hulls: `PlayerHullMax = 20`, `EnemyHullMax = 15`. Damage clamps at 0.
+- Win/lose detection. Centered overlay text "VICTORY (R to restart)" / "DEFEAT (R to restart)". `R` resets the encounter (combat state, systems, crew).
+- Visual readouts: top-of-screen hull bars (player left, enemy right). A small horizontal weapon-charge bar near the Weapons HUD column. Shield-layer dots above the player Shields room. Evasion % text near the Engines room. A 200 ms red flash on the ship that just took a hull hit.
+
+**Out of scope.** Per-room targeting; system damage; crew taking damage / fleeing rooms; repair; multiple weapons; multiple enemy types or AI variation; missiles / drones / beams / hacking; FTL drive / map / events; sound / music; Pilot and MedBay gameplay effects (their HUD columns still allocate power but produce no effect — flagged for `polish`).
+
+**Chosen approach.**
+
+- **Data — new file `combat.go`** (pure, no Ebitengine):
+ - `type GameResult int` with `GameOngoing, GameVictory, GameDefeat`.
+ - `type CombatSide struct { Hull, MaxHull int; ShieldLayers int; ShieldRegenT float64; HitFlashT float64 }`.
+ - `type Combat struct { Player, Enemy CombatSide; PlayerWeaponT float64; EnemyFireT float64; Result GameResult }`.
+ - `func NewCombat() Combat` — initial state: player 20/20, enemy 15/15, all timers 0, `Result = GameOngoing`.
+ - `func updateCombat(c *Combat, sys []System, dt float64, rng *rand.Rand)` — full tick. If `Result != GameOngoing`, no-op. Otherwise:
+ 1. Tick `HitFlashT` down on both sides (clamp ≥ 0).
+ 2. Compute `playerShieldMax(sys)`; clamp `Player.ShieldLayers ≤ max`.
+ 3. Advance shield regen: `ShieldRegenT += dt`; while `ShieldRegenT >= ShieldRegen && ShieldLayers < max`: `++ShieldLayers; ShieldRegenT -= ShieldRegen`.
+ 4. Advance player weapon charge by `dt * float64(sys[int(RoleWeapons)].PowerLevel)`. On `>= WeaponChargeMax`: call `resolveIncoming(&c.Enemy, 0, rng)`, reset `PlayerWeaponT = 0`.
+ 5. Advance enemy fire timer by `dt`. On `>= EnemyFireInterval`: call `resolveIncoming(&c.Player, 5*sys[int(RoleEngines)].PowerLevel, rng)`, reset `EnemyFireT = 0`.
+ 6. Win/lose check: if `Enemy.Hull == 0 && Player.Hull > 0`: `Result = GameVictory`; if `Player.Hull == 0`: `Result = GameDefeat`.
+ - `func resolveIncoming(side *CombatSide, evasionPct int, rng *rand.Rand)` — `if rng.Intn(100) < evasionPct: return` (dodged). Else: if `ShieldLayers > 0`: `--ShieldLayers`; else `Hull = max(0, Hull-1); HitFlashT = HitFlashDuration`.
+ - `func playerShieldMax(sys []System) int` — `sys[int(RoleShields)].PowerLevel / 2`.
+ - Constants: `const ( WeaponChargeMax = 8.0; ShieldRegen = 3.0; EnemyFireInterval = 6.0; HitFlashDuration = 0.2; PlayerHullMax = 20; EnemyHullMax = 15 )`.
+
+- **Enemy ship layout — modify `ship.go`:**
+ - Add `func NewEnemyShip() Ship` — same 5-room layout shifted by `+24` tiles; enemy occupies tiles `[27, 38] × [6, 15]`, pixels `[432, 608] × [96, 240]`.
+
+- **Rendering — modify `render.go`:**
+ - Add `func drawEnemyShip(screen *ebiten.Image, s Ship, hitFlashT float64)` — every room rendered with full `roleColor` (no power dimming). When `hitFlashT > 0`: translucent red overlay across all rooms.
+ - Modify `drawShip` to take an extra `hitFlashT float64` arg → same red overlay when player takes a hull hit.
+ - Add `func drawHullBars(screen *ebiten.Image, c Combat)` — top-left "PLAYER 20/20" + filled bar; top-right "ENEMY 15/15" mirrored.
+ - Add `func drawWeaponCharge(screen *ebiten.Image, c Combat)` — horizontal fill bar near the Weapons HUD column, fraction = `PlayerWeaponT / WeaponChargeMax`.
+ - Add `func drawShieldsIndicator(screen *ebiten.Image, c Combat, sys []System)` — N dots above the player Shields room, lit = active layer, dim = max-but-regenerating slot.
+ - Add `func drawEvasionReadout(screen *ebiten.Image, sys []System)` — text "Evade: NN%" above the Engines room.
+ - Add `func drawResultOverlay(screen *ebiten.Image, result GameResult)` — semi-transparent dark fill + centered "VICTORY (R to restart)" / "DEFEAT (R to restart)".
+
+- **Input + integration — modify `main.go`:**
+ - `Game` gains `combat Combat`, `enemy Ship`, `rng *rand.Rand`.
+ - `main()` initialises `combat := NewCombat()`, `enemy := NewEnemyShip()`, `rng := rand.New(rand.NewSource(time.Now().UnixNano()))`.
+ - `Update`:
+ - When `combat.Result == GameOngoing`: call `updateCombat(&g.combat, g.systems, dt, g.rng)` after the existing crew tick.
+ - On `inpututil.IsKeyJustPressed(ebiten.KeyR)` and `combat.Result != GameOngoing`: full reset — `g.combat = NewCombat(); g.systems = NewStartingSystems(); g.crew = NewStartingCrew(); g.selectedCrew = -1`.
+ - HUD power-allocation clicks remain unchanged during ongoing combat. Gated to no-op when `Result != GameOngoing` to keep the post-game state stable until restart.
+ - `Draw`: existing `drawShip(screen, g.ship, g.systems, g.combat.Player.HitFlashT)`; new `drawEnemyShip(screen, g.enemy, g.combat.Enemy.HitFlashT)`; new HUD overlays (hull bars, weapon charge, shield dots, evasion %); existing `drawHud`; `drawCrew` for each crew; `drawResultOverlay` last.
+
+**Tradeoffs that settled the decisions.**
+- Auto-fire vs click-to-fire — with one weapon and no per-room targeting, manual fire adds an interaction step with no decision behind it. Auto-fire keeps the loop legible as resource flow.
+- Dumb-timer enemy vs charging-weapon enemy — symmetric AI doubles the test surface for no gameplay payoff at this scope. A timer is two state vars and one branch.
+- Damage 1, hulls 20/15 — ~15 enemy hits to win, ~20 player hits to lose; at default power split (4 weap / 2 shld / 2 eng) that's a ~30-60 s fight, the right length for "one combat encounter."
+- Shield max from `floor(power/2)` — mirrors FTL's "2 power per layer", keeps the existing `System` data unchanged, makes each shield power level visibly different.
+- Evasion as flat % roll — RNG is unavoidable for FTL-style evasion; seeded `*rand.Rand` makes the tick deterministic in tests.
+- `R` to restart vs auto-restart — explicit input prevents accidental loops; matches FTL's "main menu after defeat" condensed to one keypress.
+- Pilot + MedBay inert — their FTL effects (pilot evasion bonus, MedBay heal) require crew-positioning gameplay, which is `polish`-tier. Marking them inert keeps scope honest.
+
+**Backwards compat.** Greenfield additions; no public API; only behavior change is `Game.Update`'s ordering (combat tick interleaved with crew tick). HUD click handling, crew movement, power allocation semantics unchanged. `R` is a new key binding — not previously used anywhere in the project.
+
+**TDD: yes** — `updateCombat`, `resolveIncoming`, `playerShieldMax`, `NewCombat`, and the restart-reset block are pure functions with meaningful branching (idle vs ongoing, weapon-charge threshold, shield regen tick, hull underflow, evasion roll on/off, win/lose state transitions). Rendering and key input stay manual smoke. Seeded `*rand.Rand` makes evasion deterministic.
+
+### Invariants
+
+- IV1 — `combat.go` and `ship.go` must not import Ebitengine. Combat logic and ship layouts are pure.
+- IV2 — `updateCombat` and `resolveIncoming` are the only mutators of `Combat` and `CombatSide` fields. Renderer reads only.
+- IV3 — `0 ≤ Hull ≤ MaxHull` for both sides at all times.
+- IV4 — `0 ≤ ShieldLayers ≤ playerShieldMax(sys)` after every `updateCombat` call. Layers clamp down when power is removed.
+- IV5 — `Combat.Result` only transitions out of `GameOngoing` once per fight; once `GameVictory` or `GameDefeat`, stays until reset by R.
+- IV6 — `updateCombat(c, sys, dt, rng)` is deterministic for a given input state and `*rand.Rand` state; the only randomness source is the passed `*rand.Rand`.
+- IV7 — Single codebase builds unchanged for `GOOS=js GOARCH=wasm` and native; no `//go:build`, no platform-suffixed files.
+- IV8 — HUD geometry constants stay in `hud.go` (carried over from systems-power's IV6).
+
+### Principles
+
+- PC1 — Fail fast: invalid combat actions (e.g. R while ongoing) are silent no-ops, not partial state changes.
+- PC2 — Tests cover pure logic (combat tick, hit resolution, shield regen, charge, win/lose); rendering and input wiring stay manual.
+- PC3 — Separate data from rendering and input: `combat.go` doesn't know pixel layout; `render.go` reads `Combat` only.
+- PC4 — Game-feel numbers (hull max, charge max, shield regen, evasion factor, enemy fire interval) live as named constants, not magic literals scattered through tick logic.
+
+### Assumptions
+
+- AS1 — `inpututil.IsKeyJustPressed(ebiten.KeyR)` works under `GOOS=js GOARCH=wasm`. Same package as the mouse path that already works in WASM.
+- AS2 — `math/rand`'s `*rand.Rand` is portable to WASM. Verified by Go's WASM port matrix.
+- AS3 — A single 60 TPS Ebitengine tick rate gives smooth-enough charge / regen / flash animation at the chosen constants without sub-frame stepping.
+
+### Unknowns
+
+- UK1 — Whether the chosen game-feel numbers (hulls 20/15, weapon 8/charge, shields 3 s regen, enemy 6 s fire, evasion 5%/level) produce a "fair" feeling fight at the baseline 8-reactor allocation. Resolved at verify by playing through several encounters with different power setups.
+- UK2 — Whether the visible feedback for shield hits (layer count change) and evasion (no other signal beyond the enemy fire timer resetting) is legible at 640×360. Resolved at verify by browser smoke; if too subtle, add a text "MISS" / "BLOCK" toast.