diff options
| -rw-r--r-- | internal/game/cmd_move.go | 10 | ||||
| -rw-r--r-- | internal/game/combat_mob.go | 40 | ||||
| -rw-r--r-- | internal/game/sys_energy.go | 28 | ||||
| -rw-r--r-- | internal/game/sys_energy_test.go | 30 |
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") } } |
