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