aboutsummaryrefslogtreecommitdiff
path: root/internal/game/sys_energy_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-17 17:12:39 -0400
committerhistoria <[not public]>2026-07-17 17:12:39 -0400
commit6e6c4a157564100249bb58f412485055bb2d10cf (patch)
treed457414b989e737287384f87004bffa70ac23886 /internal/game/sys_energy_test.go
parente09e7badbc722f580a9d7674c46d6ff3cc320b26 (diff)
downloadthehouseoficarus-6e6c4a157564100249bb58f412485055bb2d10cf.tar.gz
feat: implement run energy, shorten walk time to 1-2 ticks between rooms, update graceful set
Diffstat (limited to 'internal/game/sys_energy_test.go')
-rw-r--r--internal/game/sys_energy_test.go250
1 files changed, 250 insertions, 0 deletions
diff --git a/internal/game/sys_energy_test.go b/internal/game/sys_energy_test.go
new file mode 100644
index 0000000..ff897f0
--- /dev/null
+++ b/internal/game/sys_energy_test.go
@@ -0,0 +1,250 @@
+package game
+
+import (
+ "testing"
+
+ "thehouseoficarus/internal/combat"
+ "thehouseoficarus/internal/game/hacking"
+ "thehouseoficarus/internal/item"
+ "thehouseoficarus/internal/net"
+ "thehouseoficarus/internal/player"
+)
+
+func energyTestGame() *Game {
+ return &Game{
+ Combat: combat.NewTracker(),
+ safespot: NewSafespotManager(),
+ hackingStates: make(map[string]*hacking.Session),
+ Hub: net.NewHub(),
+ }
+}
+
+// addSession registers a player session with the test game's hub so that
+// EnergyRegenTick iterates over it.
+func addSession(g *Game, p *player.Player) {
+ sess := &net.Session{Player: p}
+ g.Hub.Add(sess)
+}
+
+func TestMaxRunEnergy(t *testing.T) {
+ p := player.New("tester")
+ if p.MaxRunEnergy() != 3 { // agility level 1
+ t.Fatalf("fresh player max run energy = %d, want 3", p.MaxRunEnergy())
+ }
+ if p.RunEnergy != 3 {
+ t.Fatalf("fresh player run energy not initialized to max: %d", p.RunEnergy)
+ }
+ p.Skills[player.Agility] = player.XPForLevel(20)
+ if p.MaxRunEnergy() != 60 {
+ t.Fatalf("agility 20 max run energy = %d, want 60", p.MaxRunEnergy())
+ }
+}
+
+func TestMoveTicks(t *testing.T) {
+ g := energyTestGame()
+
+ // Single move, energy present -> 1 tick, uses energy.
+ p := player.New("a")
+ if ticks := g.moveTicks(p, false); ticks != 1 {
+ t.Errorf("single move with energy: ticks=%d want 1", ticks)
+ } else if !p.MoveUsesEnergy {
+ t.Error("single move with energy should set MoveUsesEnergy")
+ }
+
+ // Single move, no energy -> 2 ticks, no drain.
+ p2 := player.New("b")
+ p2.RunEnergy = 0
+ if ticks := g.moveTicks(p2, false); ticks != 2 {
+ t.Errorf("single move no energy: ticks=%d want 2", ticks)
+ } else if p2.MoveUsesEnergy {
+ t.Error("single move no energy should not set MoveUsesEnergy")
+ }
+
+ // Walk without full graceful set -> 2 ticks, no drain.
+ p3 := player.New("c")
+ p3.RunEnergy = 10
+ if ticks := g.moveTicks(p3, true); ticks != 2 {
+ t.Errorf("walk no full set: ticks=%d want 2", ticks)
+ } else if p3.MoveUsesEnergy {
+ t.Error("walk without full set should not drain energy")
+ }
+
+ // Cape of Agility -> 0 ticks, no drain regardless of walk.
+ p4 := player.New("d")
+ p4.Equipment[item.SlotBack] = capeOfAgilityID
+ if ticks := g.moveTicks(p4, false); ticks != 0 {
+ t.Errorf("cape single move: ticks=%d want 0", ticks)
+ } else if p4.MoveUsesEnergy {
+ t.Error("cape should not drain energy")
+ }
+ if ticks := g.moveTicks(p4, true); ticks != 0 {
+ t.Errorf("cape walk: ticks=%d want 0", ticks)
+ }
+
+ // GodMode -> 0 ticks, no drain.
+ p5 := player.New("e")
+ p5.GodMode = true
+ if ticks := g.moveTicks(p5, false); ticks != 0 {
+ t.Errorf("godmode: ticks=%d want 0", ticks)
+ } else if p5.MoveUsesEnergy {
+ t.Error("godmode should not drain energy")
+ }
+
+ // Full graceful set, walk, with energy -> 1 tick, drains.
+ p6 := player.New("f")
+ p6.Equipment[item.SlotBack] = "graceful_cape"
+ p6.Equipment[item.SlotHead] = "graceful_hat"
+ p6.Equipment[item.SlotTorso] = "graceful_torso"
+ p6.Equipment[item.SlotLegs] = "graceful_legs"
+ p6.Equipment[item.SlotHands] = "graceful_gloves"
+ p6.Equipment[item.SlotFeet] = "graceful_boots"
+ if got := gracefulCount(p6); got != 6 {
+ t.Fatalf("gracefulCount=%d want 6", got)
+ }
+ if ticks := g.moveTicks(p6, true); ticks != 1 {
+ t.Errorf("full-set walk with energy: ticks=%d want 1", ticks)
+ } else if !p6.MoveUsesEnergy {
+ t.Error("full-set walk with energy should drain")
+ }
+
+ // Full graceful set, walk, no energy -> 2 ticks, no drain.
+ p7 := player.New("g")
+ for slot, id := range p6.Equipment {
+ p7.Equipment[slot] = id
+ }
+ p7.RunEnergy = 0
+ if ticks := g.moveTicks(p7, true); ticks != 2 {
+ t.Errorf("full-set walk no energy: ticks=%d want 2", ticks)
+ } else if p7.MoveUsesEnergy {
+ t.Error("full-set walk with no energy should not drain")
+ }
+}
+
+func TestGracefulCountCapeExcludesFullSet(t *testing.T) {
+ // Cape of Agility shares the back slot — 5 graceful pieces + cape = 5,
+ // not 6, so no full-set bonus.
+ p := player.New("h")
+ p.Equipment[item.SlotBack] = capeOfAgilityID
+ p.Equipment[item.SlotHead] = "graceful_hat"
+ p.Equipment[item.SlotTorso] = "graceful_torso"
+ p.Equipment[item.SlotLegs] = "graceful_legs"
+ p.Equipment[item.SlotHands] = "graceful_gloves"
+ p.Equipment[item.SlotFeet] = "graceful_boots"
+ if got := gracefulCount(p); got != 5 {
+ t.Errorf("5 graceful + cape: count=%d want 5", got)
+ }
+ if hasCapeOfAgility(p) != true {
+ t.Error("should detect cape of agility")
+ }
+}
+
+func TestEnergyRegenIdle(t *testing.T) {
+ g := energyTestGame()
+ p := player.New("idle")
+ p.RunEnergy = 0
+ p.Skills[player.Agility] = player.XPForLevel(10) // max 30
+ addSession(g, p)
+
+ // Idle baseline: 1 energy per 5 ticks.
+ for i := 0; i < 4; i++ {
+ g.EnergyRegenTick()
+ }
+ if p.RunEnergy != 0 {
+ t.Errorf("after 4 idle ticks energy=%d want 0", p.RunEnergy)
+ }
+ g.EnergyRegenTick() // 5th tick
+ if p.RunEnergy != 1 {
+ t.Errorf("after 5 idle ticks energy=%d want 1", p.RunEnergy)
+ }
+}
+
+func TestEnergyRegenBusyCombat(t *testing.T) {
+ g := energyTestGame()
+ p := player.New("busy")
+ p.RunEnergy = 0
+ p.Skills[player.Agility] = player.XPForLevel(10)
+ addSession(g, p)
+ // Mark busy via combat tracker.
+ g.Combat.Enter(p.Name, "mob1")
+
+ // Busy baseline: 1 energy per 10 ticks.
+ for i := 0; i < 9; i++ {
+ g.EnergyRegenTick()
+ }
+ if p.RunEnergy != 0 {
+ t.Errorf("after 9 busy ticks energy=%d want 0", p.RunEnergy)
+ }
+ g.EnergyRegenTick() // 10th
+ if p.RunEnergy != 1 {
+ t.Errorf("after 10 busy ticks energy=%d want 1", p.RunEnergy)
+ }
+}
+
+func TestEnergyRegenGracefulBonus(t *testing.T) {
+ g := energyTestGame()
+ p := player.New("graceful")
+ p.RunEnergy = 0
+ p.Skills[player.Agility] = player.XPForLevel(10)
+ p.Equipment[item.SlotBack] = "graceful_cape"
+ p.Equipment[item.SlotHead] = "graceful_hat"
+ p.Equipment[item.SlotTorso] = "graceful_torso"
+ p.Equipment[item.SlotLegs] = "graceful_legs"
+ p.Equipment[item.SlotHands] = "graceful_gloves"
+ p.Equipment[item.SlotFeet] = "graceful_boots"
+ addSession(g, p)
+
+ // Full set idle rate = (1/5) * 1.48 * 1.10 = ~0.3256 per tick.
+ // First point should arrive between tick 3 and 5.
+ totalTicks := 0
+ for p.RunEnergy == 0 && totalTicks < 10 {
+ g.EnergyRegenTick()
+ totalTicks++
+ }
+ if totalTicks > 5 {
+ t.Errorf("full-set regen too slow: took %d ticks for first point", totalTicks)
+ }
+ if totalTicks < 3 {
+ t.Errorf("full-set regen unexpectedly fast: %d ticks", totalTicks)
+ }
+}
+
+func TestEnergyRegenCapsAtMax(t *testing.T) {
+ g := energyTestGame()
+ p := player.New("cap")
+ p.Skills[player.Agility] = player.XPForLevel(5) // max 15
+ p.RunEnergy = 15
+ addSession(g, p)
+ g.EnergyRegenTick()
+ if p.RunEnergy != 15 {
+ t.Errorf("energy above max not capped: %d want 15", p.RunEnergy)
+ }
+ if p.RunEnergyAccum != 0 {
+ t.Errorf("accumulator not reset at max: %v", p.RunEnergyAccum)
+ }
+}
+
+func TestClampRunEnergyOnAgilityLevelUp(t *testing.T) {
+ p := player.New("clamp")
+ p.Skills[player.Agility] = player.XPForLevel(10) // max 30
+ p.RunEnergy = 30 // at max
+
+ // Grant agility XP to reach level 11 (max 33). Energy stays at 30 (no auto-fill).
+ p.AddXP(player.Agility, player.XPForLevel(11)-p.Skills[player.Agility])
+ p.ClampRunEnergy()
+ if p.Level(player.Agility) != 11 {
+ t.Fatalf("expected agility 11, got %d", p.Level(player.Agility))
+ }
+ if p.RunEnergy != 30 {
+ t.Errorf("level-up should not auto-fill energy: got %d want 30", p.RunEnergy)
+ }
+ if p.MaxRunEnergy() != 33 {
+ t.Errorf("max after level-up = %d want 33", p.MaxRunEnergy())
+ }
+
+ // If somehow over max, it clamps down.
+ p.RunEnergy = 50
+ p.ClampRunEnergy()
+ if p.RunEnergy != 33 {
+ t.Errorf("clamp down failed: %d want 33", p.RunEnergy)
+ }
+}