package game import ( "os" "testing" "thehouseoficarus/internal/combat" "thehouseoficarus/internal/engine" "thehouseoficarus/internal/game/hacking" "thehouseoficarus/internal/item" "thehouseoficarus/internal/net" "thehouseoficarus/internal/player" "thehouseoficarus/internal/world" ) type escapeTestConn struct { data []byte } func (c *escapeTestConn) ReadMessage() (string, error) { return "", nil } func (c *escapeTestConn) Write(b []byte) (int, error) { c.data = append(c.data, b...); return len(b), nil } func (c *escapeTestConn) Close() error { return nil } func (c *escapeTestConn) SetEcho(bool) error { return nil } func escapeTestGame() *Game { tmpDir, err := os.MkdirTemp("", "escape_test") if err != nil { panic(err) } dataDir := "../../data" return &Game{ Combat: combat.NewTracker(), safespot: NewSafespotManager(), hackingStates: make(map[string]*hacking.Session), Hub: net.NewHub(), Deps: Deps{ World: world.New(dataDir), MobStore: world.NewMobStore(dataDir), ItemStore: item.NewItemStore(dataDir), AccountStore: player.NewAccountStore(tmpDir), Ticks: engine.New(), }, } } func escapeTestSession(p *player.Player) *net.Session { return &net.Session{Player: p, Conn: &escapeTestConn{}} } func TestClearEscape(t *testing.T) { p := player.New("esc") p.EscapeDir = "north" p.EscapeTarget = 42 p.EscapeIsWalk = true p.ClearEscape() if p.EscapeDir != "" { t.Error("ClearEscape should clear EscapeDir") } if p.EscapeTarget != 0 { t.Error("ClearEscape should clear EscapeTarget") } if p.EscapeIsWalk { t.Error("ClearEscape should clear EscapeIsWalk") } } func TestClearMoveStateClearsEscape(t *testing.T) { p := player.New("esc2") p.EscapeDir = "south" p.EscapeTarget = 7 p.ClearMoveState() if p.EscapeDir != "" { t.Error("ClearMoveState should clear Escape fields") } } func TestBeginEscapeMoveWithEnergy(t *testing.T) { g := escapeTestGame() p := player.New("run") p.EscapeDir = "north" p.EscapeTarget = 10 p.EscapeIsWalk = false sess := escapeTestSession(p) g.beginEscapeMove(sess, p) if p.EscapeDir != "" { t.Error("beginEscapeMove should clear EscapeDir") } if p.MoveDirection != "north" { t.Errorf("beginEscapeMove should set MoveDirection, got %q", p.MoveDirection) } if p.MoveTarget != 10 { t.Errorf("beginEscapeMove should set MoveTarget, got %d", p.MoveTarget) } // With energy (agility 1 gives 3 run energy): 1 tick. if p.MoveTicks != 1 { t.Errorf("expected MoveTicks=1 with energy, got %d", p.MoveTicks) } } func TestBeginEscapeMoveWithoutEnergy(t *testing.T) { g := escapeTestGame() p := player.New("walk") p.RunEnergy = 0 p.EscapeDir = "east" p.EscapeTarget = 5 p.EscapeIsWalk = false sess := escapeTestSession(p) g.beginEscapeMove(sess, p) if p.MoveTicks != 2 { t.Errorf("expected MoveTicks=2 without energy, got %d", p.MoveTicks) } if p.EscapeDir != "" { t.Error("EscapeDir should be cleared") } } func TestBeginEscapeMoveWithCapeOfAgility(t *testing.T) { g := escapeTestGame() p := player.New("cape") p.Equipment[item.SlotBack] = capeOfAgilityID // moveTicks returns 0 for Cape of Agility, which triggers the instant // completeMove path in beginEscapeMove. The sys_energy_test.go tests // already cover moveTicks returning 0 for Cape. This test verifies // the call site doesn't regress. ticks := g.moveTicks(p, false) if ticks != 0 { t.Errorf("Cape of Agility should give 0 ticks, got %d", ticks) } } func TestApplyMobHitCancelsEscape(t *testing.T) { g := escapeTestGame() p := player.New("ouch") p.EscapeDir = "south" p.EscapeTarget = 3 p.WalkSequence = []string{"s", "s", "s"} g.Combat.Enter(p.Name, "testmob") mob := &world.MobInstance{ InstanceID: "testmob", DefID: "test_def", Name: "a goblin", } sess := escapeTestSession(p) g.applyMobHit(sess, p, mob, "slash", 10) if p.EscapeDir != "" { t.Error("mob hit should clear EscapeDir") } if p.EscapeTarget != 0 { t.Error("mob hit should clear EscapeTarget") } if len(p.WalkSequence) != 0 { t.Errorf("mob hit should cancel WalkSequence, got %d entries", len(p.WalkSequence)) } if cs := g.Combat.Get(p.Name); cs == nil || !cs.Active { t.Error("combat should still be active after cancelling escape on hit") } } func TestApplyMobMissFiresEscape(t *testing.T) { g := escapeTestGame() p := player.New("lucky") p.EscapeDir = "east" p.EscapeTarget = 8 p.EscapeIsWalk = false g.Combat.Enter(p.Name, "testmob") mob := &world.MobInstance{ InstanceID: "testmob", DefID: "test_def", Name: "a goblin", } sess := escapeTestSession(p) g.applyMobMiss(sess, p, mob) if p.EscapeDir != "" { t.Error("mob miss should trigger escape, clearing EscapeDir") } if p.MoveDirection != "east" { t.Errorf("MoveDirection should be set from escape, got %q", p.MoveDirection) } if p.MoveTicks != 1 { t.Errorf("expected MoveTicks=1 after miss-triggered escape, got %d", p.MoveTicks) } } func TestEscapeReaimSilently(t *testing.T) { p := player.New("reaim") p.EscapeDir = "north" p.EscapeTarget = 1 p.EscapeDir = "south" p.EscapeTarget = 2 if p.EscapeDir != "south" { t.Error("re-aim should update EscapeDir") } if p.EscapeTarget != 2 { t.Error("re-aim should update EscapeTarget") } } func TestDoAttackAbortsFlee(t *testing.T) { g := escapeTestGame() p := player.New("attacker") p.EscapeDir = "west" p.EscapeTarget = 4 g.Combat.Enter(p.Name, "testmob") sess := escapeTestSession(p) g.doAttack(sess, "goblin") if p.EscapeDir != "" { t.Error("doAttack should clear EscapeDir even when already in combat") } } func TestEscapeClearedOnStop(t *testing.T) { p := player.New("stopper") p.EscapeDir = "north" p.EscapeTarget = 5 p.ClearMoveState() if p.EscapeDir != "" { t.Error("ClearMoveState should clear Escape fields") } } func TestPlayerActionDisplayShowsPreparingToFlee(t *testing.T) { g := escapeTestGame() p := player.New("fleer") p.EscapeDir = "north" display := g.playerActionDisplay(p) if display != "preparing to flee" { t.Errorf("expected 'preparing to flee', got %q", display) } } func TestEndCombatFiresEscapeOnMobDeath(t *testing.T) { g := escapeTestGame() p := player.New("finisher") p.EscapeDir = "east" p.EscapeTarget = 10 mob := &world.MobInstance{ InstanceID: "finisher_mob", DefID: "test_def", Name: "a goblin", HP: 0, } sess := escapeTestSession(p) g.endCombat(sess, p, mob) if p.EscapeDir != "" { t.Error("endCombat should fire escape, clearing EscapeDir") } if p.MoveTicks != 1 { t.Errorf("expected MoveTicks=1 after endCombat escape, got %d", p.MoveTicks) } if p.MoveDirection != "east" { t.Errorf("expected MoveDirection from escape, got %q", p.MoveDirection) } } func TestEndCombatDoesNotFireEscapeIfMobAlive(t *testing.T) { g := escapeTestGame() p := player.New("patient") p.EscapeDir = "east" p.EscapeTarget = 10 mob := &world.MobInstance{ InstanceID: "alive_mob", DefID: "test_def", Name: "a goblin", HP: 5, } sess := escapeTestSession(p) g.endCombat(sess, p, mob) if p.EscapeDir != "east" { t.Error("escape should still be pending when mob lives") } if p.MoveDirection != "" { t.Error("MoveDirection should NOT be set when escape not fired") } }