aboutsummaryrefslogtreecommitdiff
path: root/internal/game/sys_energy.go
blob: 7331fa8f4157aec278ae9f33a51dc4992b12c004 (plain)
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
package game

import (
	"thehouseoficarus/internal/item"
	"thehouseoficarus/internal/player"
)

// gracefulItems is the set of equipment IDs that count toward the Graceful
// outfit. Wearing pieces of the set increases run-energy regeneration speed
// (see EnergyRegenTick); wearing the complete 6-piece set grants an additional
// bonus. The Cape of Agility and graceful_cape share the back slot and are
// therefore mutually exclusive, so the full-set bonus requires graceful_cape.
var gracefulItems = map[string]bool{
	"graceful_hat":    true,
	"graceful_torso":  true,
	"graceful_legs":   true,
	"graceful_gloves": true,
	"graceful_boots":  true,
	"graceful_cape":   true,
}

const capeOfAgilityID = "cape_of_agility"

// gracefulCount returns how many pieces of the Graceful outfit the player is
// currently wearing (maximum 6).
func gracefulCount(p *player.Player) int {
	count := 0
	for _, id := range p.Equipment {
		if gracefulItems[id] {
			count++
		}
	}
	return count
}

// hasCapeOfAgility reports whether the Cape of Agility is worn in the back
// slot. The cape grants infinite run energy (movement is instant and never
// drains energy).
func hasCapeOfAgility(p *player.Player) bool {
	id, ok := p.Equipment[item.SlotBack]
	return ok && id == capeOfAgilityID
}

// moveTicks returns how many ticks the upcoming move will take, and sets
// p.MoveUsesEnergy to indicate whether completing the move should drain one
// point of run energy.
//
//   - GodMode and Cape of Agility: instant (0 ticks), never drains energy.
//   - A single move (north/e/w...): 1 tick if energy remains, otherwise 2.
//   - The "walk" command: 2 ticks per step and never drains energy unless the
//     player wears the complete Graceful outfit (or Cape of Agility), in which
//     case it behaves like a single run move (1 tick with energy, 2 without).
func (g *Game) moveTicks(p *player.Player, isWalk bool) int {
	if p.GodMode {
		p.MoveUsesEnergy = false
		return 0
	}
	if hasCapeOfAgility(p) {
		p.MoveUsesEnergy = false
		return 0
	}

	if isWalk && gracefulCount(p) != 6 {
		// Plain walk: slow, no energy cost.
		p.MoveUsesEnergy = false
		return 2
	}

	// Single move, or a run-walk enabled by the full Graceful set.
	if p.HasRunEnergy() {
		p.MoveUsesEnergy = true
		return 1
	}
	p.MoveUsesEnergy = false
	return 2
}

// isPlayerBusy reports whether the player is currently performing an action
// (combat, gathering, crafting, walking, hiding, hacking, etc.). A busy player
// regenerates run energy twice as slowly as an idle one.
func (g *Game) isPlayerBusy(p *player.Player) bool {
	if p.Action != nil || p.BackgroundAction != nil {
		return true
	}
	if g.Combat.Get(p.Name) != nil {
		return true
	}
	if p.MoveTicks > 0 || len(p.WalkSequence) > 0 {
		return true
	}
	if _, ok := g.safespot.Get(p.Name); ok {
		return true
	}
	if _, ok := g.hackingStates[p.Name]; ok {
		return true
	}
	return false
}

// EnergyRegenTick advances run-energy regeneration for every online player.
//
//	Idle:   1 energy per 5 ticks
//	Busy:   1 energy per 10 ticks
//	Graceful: +8% recovery rate per equipped piece
//	         full 6-piece set adds a further +10%
//
// Fractional recovery is accumulated across ticks in thousandths of an energy
// point (RunEnergyAccum, int) so that per-tick float drift never prevents a
// point from arriving exactly when its rate says it should.
func (g *Game) EnergyRegenTick() {
	if g.Hub == nil {
		return
	}
	const perEnergy = 1000
	for _, sess := range g.Hub.AllSessions() {
		p := sess.Player
		if p == nil {
			continue
		}
		maxE := p.MaxRunEnergy()
		if maxE <= 0 || p.RunEnergy >= maxE {
			p.RunEnergyAccum = 0
			continue
		}

		// Base recovery rate in thousandths of an energy point per tick.
		base := perEnergy / 5 // idle: 1/5
		if g.isPlayerBusy(p) {
			base = perEnergy / 10 // busy: 1/10
		}

		// Graceful: +8% per piece, +10% bonus for the complete 6-piece set.
		count := gracefulCount(p)
		mult := 1.0 + 0.08*float64(count)
		if count == 6 {
			mult *= 1.10
		}

		perTick := int(float64(base)*mult + 0.5) // round to nearest
		p.RunEnergyAccum += perTick
		for p.RunEnergyAccum >= perEnergy && p.RunEnergy < maxE {
			p.RunEnergyAccum -= perEnergy
			p.RunEnergy++
		}
		if p.RunEnergy >= maxE {
			p.RunEnergyAccum = 0
		}
	}
}