~bigbes/game-prototype-ftl

ref: 7d266a3ed0ff36694b042467963d582a7b694405 game-prototype-ftl/combat.go -rw-r--r-- 3.3 KiB
7d266a3e — Eugene Blikh fix(wasm): set go.env to empty object 6 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
	}
}