# 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.