aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-07-17 19:23:23 -0400
committerhistoria <[not public]>2026-07-17 19:23:23 -0400
commited0d9332def127b2fefebe80a4992a875cc7e521 (patch)
tree745dc622e448ca7a16da11f1684d5ba819d6da97 /internal
parent3d90a18c15ca54f262cfa61edba4866c91f29802 (diff)
downloadthehouseoficarus-ed0d9332def127b2fefebe80a4992a875cc7e521.tar.gz
refactor: return (ticks, usesEnergy) in moveTicks, remove old flee code
Diffstat (limited to 'internal')
-rw-r--r--internal/game/cmd_move.go10
-rw-r--r--internal/game/combat_mob.go40
-rw-r--r--internal/game/sys_energy.go28
-rw-r--r--internal/game/sys_energy_test.go30
4 files changed, 62 insertions, 46 deletions
diff --git a/internal/game/cmd_move.go b/internal/game/cmd_move.go
index 576bdd5..761db2f 100644
--- a/internal/game/cmd_move.go
+++ b/internal/game/cmd_move.go
@@ -79,16 +79,23 @@ func (g *Game) doMove(sess *net.Session, dir string, isWalk bool) {
g.cancelAction(p)
}
- ticks := g.moveTicks(p, isWalk)
+ ticks, usesEnergy := g.moveTicks(p, isWalk)
p.MoveDirection = string(exitDir)
p.MoveTarget = targetID
if ticks <= 0 {
+ // Instant-move gear (Cape of Agility, GodMode): escape the miss-gate
+ // entirely. Stash the usesEnergy flag so completeMove drains if due.
+ p.MoveUsesEnergy = usesEnergy
g.completeMove(sess, p)
return
}
if inCombat {
+ // Defer the move: stash it in the Escape* fields and wait for a mob
+ // miss, mob death, or the 3-strike "power through" relief. Do NOT set
+ // MoveUsesEnergy here — no move is in flight yet, and beginEscapeMove
+ // recomputes (and stores) the flag when the gate fires.
firstEscape := p.EscapeDir == ""
p.EscapeDir = string(exitDir)
p.EscapeTarget = targetID
@@ -111,6 +118,7 @@ func (g *Game) doMove(sess *net.Session, dir string, isWalk bool) {
return
}
+ p.MoveUsesEnergy = usesEnergy
p.MoveTicks = ticks
}
diff --git a/internal/game/combat_mob.go b/internal/game/combat_mob.go
index 8b7d869..dabb7d7 100644
--- a/internal/game/combat_mob.go
+++ b/internal/game/combat_mob.go
@@ -169,9 +169,17 @@ func (g *Game) applyMobHit(sess *net.Session, p *player.Player, mob *world.MobIn
}
if p.EscapeDir != "" {
- p.ClearEscape()
- p.WalkSequence = nil
- sess.WriteLine("Can't escape!")
+ p.EscapeFailCount++
+ if p.EscapeFailCount >= 3 && p.HP > 0 {
+ sess.WriteLine(g.colorize(sess, "miss",
+ fmt.Sprintf("You power through %s's relentless attacks and just flee!",
+ mobDisplayName(mob, false))))
+ g.beginEscapeMove(sess, p)
+ } else {
+ p.ClearEscape()
+ p.WalkSequence = nil
+ sess.WriteLine("Can't escape!")
+ }
}
if p.Action != nil && p.Action.Type == behavior.TypeTriggerModule {
@@ -347,8 +355,8 @@ func (g *Game) killPlayer(sess *net.Session, p *player.Player, mob *world.MobIns
if mob.HP < 0 {
mob.HP = 0
}
- sess.WriteLine(fmt.Sprintf("Dead Man's Switch activates! %s takes %s damage!",
- mobDisplayName(mob, true), g.dmgDisplay(sess, retDmg)))
+ sess.WriteLine(fmt.Sprintf("Dead Man's Switch activates! %s takes %s damage!",
+ mobDisplayName(mob, true), g.dmgDisplay(sess, retDmg)))
}
}
}
@@ -414,21 +422,25 @@ func (g *Game) stopCombat(playerName string) {
g.Combat.Leave(playerName)
}
-// beginEscapeMove starts the actual movement after the escape gate (a mob miss
-// or mob death) has fired. It copies the stashed Escape* fields into the Move*
-// fields, recomputes moveTicks, and either sets MoveTicks for the normal
-// 1-2 tick run-delay or calls completeMove instantly for 0-tick gear.
+// 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(sess *net.Session, p *player.Player) {
p.MoveDirection = p.EscapeDir
p.MoveTarget = p.EscapeTarget
p.MovePendingTrigger = p.EscapeTrigger
isWalk := p.EscapeIsWalk
p.ClearEscape()
+ p.EscapeFailCount = 0
- ticks := g.moveTicks(p, isWalk)
- if ticks <= 0 {
- g.completeMove(sess, p)
- return
- }
+ ticks, usesEnergy := g.moveTicks(p, isWalk)
+ p.MoveUsesEnergy = usesEnergy
p.MoveTicks = ticks
}
diff --git a/internal/game/sys_energy.go b/internal/game/sys_energy.go
index 7331fa8..1144793 100644
--- a/internal/game/sys_energy.go
+++ b/internal/game/sys_energy.go
@@ -41,38 +41,34 @@ func hasCapeOfAgility(p *player.Player) bool {
return ok && id == capeOfAgilityID
}
-// moveTicks returns how many ticks the upcoming move will take, and sets
-// p.MoveUsesEnergy to indicate whether completing the move should drain one
-// point of run energy.
+// moveTicks returns how many ticks the upcoming move will take and whether
+// completing the move should drain one point of run energy.
//
// - GodMode and Cape of Agility: instant (0 ticks), never drains energy.
// - A single move (north/e/w...): 1 tick if energy remains, otherwise 2.
// - The "walk" command: 2 ticks per step and never drains energy unless the
// player wears the complete Graceful outfit (or Cape of Agility), in which
// case it behaves like a single run move (1 tick with energy, 2 without).
-func (g *Game) moveTicks(p *player.Player, isWalk bool) int {
+//
+// The caller is responsible for storing the returned usesEnergy flag into
+// p.MoveUsesEnergy only when a move is genuinely about to enter flight — never
+// while a move is merely being stashed as a pending flee.
+func (g *Game) moveTicks(p *player.Player, isWalk bool) (int, bool) {
if p.GodMode {
- p.MoveUsesEnergy = false
- return 0
+ return 0, false
}
if hasCapeOfAgility(p) {
- p.MoveUsesEnergy = false
- return 0
+ return 0, false
}
if isWalk && gracefulCount(p) != 6 {
- // Plain walk: slow, no energy cost.
- p.MoveUsesEnergy = false
- return 2
+ return 2, false
}
- // Single move, or a run-walk enabled by the full Graceful set.
if p.HasRunEnergy() {
- p.MoveUsesEnergy = true
- return 1
+ return 1, true
}
- p.MoveUsesEnergy = false
- return 2
+ return 2, false
}
// isPlayerBusy reports whether the player is currently performing an action
diff --git a/internal/game/sys_energy_test.go b/internal/game/sys_energy_test.go
index ff897f0..a1a5dab 100644
--- a/internal/game/sys_energy_test.go
+++ b/internal/game/sys_energy_test.go
@@ -45,48 +45,48 @@ func TestMoveTicks(t *testing.T) {
// Single move, energy present -> 1 tick, uses energy.
p := player.New("a")
- if ticks := g.moveTicks(p, false); ticks != 1 {
+ if ticks, usesEnergy := g.moveTicks(p, false); ticks != 1 {
t.Errorf("single move with energy: ticks=%d want 1", ticks)
- } else if !p.MoveUsesEnergy {
+ } else if !usesEnergy {
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 {
+ if ticks, usesEnergy := g.moveTicks(p2, false); ticks != 2 {
t.Errorf("single move no energy: ticks=%d want 2", ticks)
- } else if p2.MoveUsesEnergy {
+ } else if usesEnergy {
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 {
+ if ticks, usesEnergy := g.moveTicks(p3, true); ticks != 2 {
t.Errorf("walk no full set: ticks=%d want 2", ticks)
- } else if p3.MoveUsesEnergy {
+ } else if usesEnergy {
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 {
+ if ticks, usesEnergy := g.moveTicks(p4, false); ticks != 0 {
t.Errorf("cape single move: ticks=%d want 0", ticks)
- } else if p4.MoveUsesEnergy {
+ } else if usesEnergy {
t.Error("cape should not drain energy")
}
- if ticks := g.moveTicks(p4, true); ticks != 0 {
+ 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 {
+ if ticks, usesEnergy := g.moveTicks(p5, false); ticks != 0 {
t.Errorf("godmode: ticks=%d want 0", ticks)
- } else if p5.MoveUsesEnergy {
+ } else if usesEnergy {
t.Error("godmode should not drain energy")
}
@@ -101,9 +101,9 @@ func TestMoveTicks(t *testing.T) {
if got := gracefulCount(p6); got != 6 {
t.Fatalf("gracefulCount=%d want 6", got)
}
- if ticks := g.moveTicks(p6, true); ticks != 1 {
+ if ticks, usesEnergy := g.moveTicks(p6, true); ticks != 1 {
t.Errorf("full-set walk with energy: ticks=%d want 1", ticks)
- } else if !p6.MoveUsesEnergy {
+ } else if !usesEnergy {
t.Error("full-set walk with energy should drain")
}
@@ -113,9 +113,9 @@ func TestMoveTicks(t *testing.T) {
p7.Equipment[slot] = id
}
p7.RunEnergy = 0
- if ticks := g.moveTicks(p7, true); ticks != 2 {
+ if ticks, usesEnergy := g.moveTicks(p7, true); ticks != 2 {
t.Errorf("full-set walk no energy: ticks=%d want 2", ticks)
- } else if p7.MoveUsesEnergy {
+ } else if usesEnergy {
t.Error("full-set walk with no energy should not drain")
}
}