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
}
}