aboutsummaryrefslogtreecommitdiff
path: root/internal/game/sys_escape_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-17 18:36:31 -0400
committerhistoria <[not public]>2026-07-17 18:36:31 -0400
commite6d0d46ac9977e77c7f19f41121ec466a3c53e56 (patch)
tree86063e0dfda2206ccdda1b8e1070dc4da7dea9d9 /internal/game/sys_escape_test.go
parent6e6c4a157564100249bb58f412485055bb2d10cf (diff)
downloadthehouseoficarus-e6d0d46ac9977e77c7f19f41121ec466a3c53e56.tar.gz
feat: update mob flee mechanics with new energy system
Diffstat (limited to 'internal/game/sys_escape_test.go')
-rw-r--r--internal/game/sys_escape_test.go303
1 files changed, 303 insertions, 0 deletions
diff --git a/internal/game/sys_escape_test.go b/internal/game/sys_escape_test.go
new file mode 100644
index 0000000..f07701c
--- /dev/null
+++ b/internal/game/sys_escape_test.go
@@ -0,0 +1,303 @@
+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")
+ }
+}