From c5ee77c345b4cafe10cf29b6c17a69e78cc5c630 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Sun, 4 Oct 2026 18:16:48 -0400 Subject: fix: god mode instant move fix --- internal/game/combat_mob.go | 28 +++++++++++++++++----------- internal/game/sys_escape_test.go | 29 +++++++++++++++++++++++++++-- 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) + } +} -- cgit v1.2.3