package main import "math/rand" // Game-feel timing constants. Respects PC4: all tunable numbers are named. const ( WeaponChargeMax = 8.0 ShieldRegen = 3.0 EnemyFireInterval = 6.0 HitFlashDuration = 0.2 PlayerHullMax = 20 EnemyHullMax = 15 ) // GameResult represents the current outcome of a combat encounter. type GameResult int const ( GameOngoing GameResult = iota GameVictory GameDefeat ) // CombatSide holds the mutable state for one participant in combat. type CombatSide struct { Hull int MaxHull int ShieldLayers int ShieldRegenT float64 HitFlashT float64 } // Combat holds the full state of one combat encounter. type Combat struct { Player CombatSide Enemy CombatSide PlayerWeaponT float64 EnemyFireT float64 Result GameResult } // NewCombat returns a fresh combat with default hull values and all timers at zero. func NewCombat() Combat { return Combat{ Player: CombatSide{Hull: PlayerHullMax, MaxHull: PlayerHullMax}, Enemy: CombatSide{Hull: EnemyHullMax, MaxHull: EnemyHullMax}, Result: GameOngoing, } } // playerShieldMax returns the maximum shield layers the player can maintain // given current systems power. Formula: shields power / 2 (integer division). // Pure query — no mutation. Respects GPC3. func playerShieldMax(sys []System) int { return sys[int(RoleShields)].PowerLevel / 2 } // resolveIncoming applies one incoming hit to a CombatSide. // evasionPct is an integer 0–100; if the rng rolls under it the shot is dodged. // When the shot lands: shields absorb first; if no shields, hull takes 1 damage // (clamped to 0) and HitFlashT is set. Respects IV2, IV3. func resolveIncoming(side *CombatSide, evasionPct int, rng *rand.Rand) { if rng.Intn(100) < evasionPct { return // dodged } if side.ShieldLayers > 0 { side.ShieldLayers-- return } if side.Hull > 0 { side.Hull-- } side.HitFlashT = HitFlashDuration } // updateCombat advances combat state by dt seconds. // No-ops if Result != GameOngoing. Respects IV2, IV3, IV4, IV5, IV6. func updateCombat(c *Combat, sys []System, dt float64, rng *rand.Rand) { if c.Result != GameOngoing { return } // Step 1: tick HitFlashT down on both sides, clamp ≥ 0. c.Player.HitFlashT -= dt if c.Player.HitFlashT < 0 { c.Player.HitFlashT = 0 } c.Enemy.HitFlashT -= dt if c.Enemy.HitFlashT < 0 { c.Enemy.HitFlashT = 0 } // Step 2: compute shield max and clamp layers. shieldMax := playerShieldMax(sys) if c.Player.ShieldLayers > shieldMax { c.Player.ShieldLayers = shieldMax } // Step 3: advance shield regen. c.Player.ShieldRegenT += dt for c.Player.ShieldRegenT >= ShieldRegen && c.Player.ShieldLayers < shieldMax { c.Player.ShieldLayers++ c.Player.ShieldRegenT -= ShieldRegen } // Step 4: advance player weapon charge. c.PlayerWeaponT += dt * float64(sys[int(RoleWeapons)].PowerLevel) if c.PlayerWeaponT >= WeaponChargeMax { resolveIncoming(&c.Enemy, 0, rng) c.PlayerWeaponT = 0 } // Step 5: advance enemy fire timer. c.EnemyFireT += dt if c.EnemyFireT >= EnemyFireInterval { evasionPct := 5 * sys[int(RoleEngines)].PowerLevel resolveIncoming(&c.Player, evasionPct, rng) c.EnemyFireT = 0 } // Step 6: win/lose check. if c.Enemy.Hull == 0 && c.Player.Hull > 0 { c.Result = GameVictory } else if c.Player.Hull == 0 { c.Result = GameDefeat } }