aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-04 18:16:48 -0400
committerhistoria <[not public]>2026-10-04 18:16:48 -0400
commitc5ee77c345b4cafe10cf29b6c17a69e78cc5c630 (patch)
tree9e0fe2f8e3c6f9046dc998367b7a805e96eddbf4 /internal
parent93dcb67ffee05ebf1de0c9a78a8b906eaf161007 (diff)
downloadthehouseoficarus-c5ee77c345b4cafe10cf29b6c17a69e78cc5c630.tar.gz
fix: god mode instant move fix
Diffstat (limited to 'internal')
-rw-r--r--internal/game/combat_mob.go28
-rw-r--r--internal/game/sys_escape_test.go29
2 files changed, 44 insertions, 13 deletions
diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go
index b16e4e1..b717d8e 100644
--- a/internal/game/combat_mob.go
+++ b/internal/game/combat_mob.go
@@ -174,7 +174,7 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
sess.WriteLine(g.colorize(sess, "miss",
fmt.Sprintf("You power through %s's relentless attacks and just flee!",
mobDisplayName(mob, false))))
- g.beginEscapeMove(p)
+ g.beginEscapeMove(sess, p)
} else {
p.ClearEscape()
p.WalkSequence = nil
@@ -209,7 +209,7 @@ func (g *Game) applyMobMiss(sess *net.Session, p *player.Player, mob *world.MobI
sess.WriteLine(g.colorize(sess, "miss", fmt.Sprintf("%s misses you.", attacker)))
if p.EscapeDir != "" {
- g.beginEscapeMove(p)
+ g.beginEscapeMove(sess, p)
}
}
@@ -252,7 +252,7 @@ func (g *Game) endCombat(sess *net.Session, p *player.Player, mob *world.MobInst
}
if p.EscapeDir != "" {
- g.beginEscapeMove(p)
+ g.beginEscapeMove(sess, p)
return
}
@@ -459,14 +459,11 @@ func (g *Game) stopCombat(playerName string) {
// beginEscapeMove starts the actual movement after the escape gate (a mob
// miss, a mob death, or the 3-strike "power through" relief) has fired. It
// copies the stashed Escape* fields into the Move* fields, recomputes
-// moveTicks, sets MoveTicks for the normal 1-2 tick run delay, and records
-// whether completion should drain run energy.
-//
-// beginEscapeMove is only reachable when doMove's in-combat branch stashed the
-// escape, and that branch is itself gated on moveTicks returning > 0; the
-// Cape-of-Agility / GodMode 0-tick bypass never enters the stash path, so there
-// is no 0-tick branch to handle here.
-func (g *Game) beginEscapeMove(p *player.Player) {
+// moveTicks, and either completes an instant 0-tick move (Cape of Agility or
+// GodMode acquired while the escape was pending) or sets MoveTicks for the
+// normal 1-2 tick run delay and records whether completion should drain run
+// energy.
+func (g *Game) beginEscapeMove(sess *net.Session, p *player.Player) {
p.MoveDirection = p.EscapeDir
p.MoveTarget = p.EscapeTarget
p.MovePendingTrigger = p.EscapeTrigger
@@ -476,5 +473,14 @@ func (g *Game) beginEscapeMove(p *player.Player) {
ticks, usesEnergy := g.moveTicks(p, isWalk)
p.MoveUsesEnergy = usesEnergy
+ if ticks <= 0 {
+ // The player may have gained instant-move gear (Cape of Agility) or
+ // GodMode after the escape was stashed but before the gate fired, so
+ // moveTicks can now return 0 here even though doMove's stash branch
+ // required >0 at stash time. Complete the move instead of leaving it
+ // stuck at MoveTicks==0 forever.
+ g.completeMove(sess, p)
+ return
+ }
p.MoveTicks = ticks
}
diff --git a/internal/game/sys_escape_test.go b/internal/game/sys_escape_test.go
index 1fce4f4..61b50cd 100644
--- a/internal/game/sys_escape_test.go
+++ b/internal/game/sys_escape_test.go
@@ -89,7 +89,7 @@ func TestBeginEscapeMoveWithEnergy(t *testing.T) {
p.EscapeTarget = 10
p.EscapeIsWalk = false
- g.beginEscapeMove(p)
+ g.beginEscapeMove(escapeTestSession(p), p)
if p.EscapeDir != "" {
t.Error("beginEscapeMove should clear EscapeDir")
@@ -114,7 +114,7 @@ func TestBeginEscapeMoveWithoutEnergy(t *testing.T) {
p.EscapeTarget = 5
p.EscapeIsWalk = false
- g.beginEscapeMove(p)
+ g.beginEscapeMove(escapeTestSession(p), p)
if p.MoveTicks != 2 {
t.Errorf("expected MoveTicks=2 without energy, got %d", p.MoveTicks)
@@ -487,3 +487,28 @@ func TestEndCombatDoesNotFireEscapeIfMobAlive(t *testing.T) {
t.Error("MoveDirection should NOT be set when escape not fired")
}
}
+
+// TestBeginEscapeMoveGodModeCompletesInstantMove pins the regression where a
+// player who stashed a flee and then gained GodMode (or a Cape of Agility)
+// before the escape gate fired was left with MoveTicks==0 and never moved.
+func TestBeginEscapeMoveGodModeCompletesInstantMove(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("godflee")
+ h.seedCharacter("godflee", "godflee")
+ sess, _ := h.connect("godflee")
+ p := sess.Player
+ p.RoomID = 1
+ p.GodMode = true
+ p.EscapeDir = "north"
+ p.EscapeTarget = 2
+
+ h.g.RunOnOwner(func() { h.g.beginEscapeMove(sess, p) })
+
+ if p.RoomID != 2 {
+ t.Fatalf("0-tick (god) escape move should complete immediately: room=%d want 2", p.RoomID)
+ }
+ if p.MoveTicks != 0 || p.MoveDirection != "" || p.EscapeDir != "" {
+ t.Fatalf("move state should be cleared after instant escape: MoveTicks=%d MoveDirection=%q EscapeDir=%q",
+ p.MoveTicks, p.MoveDirection, p.EscapeDir)
+ }
+}