aboutsummaryrefslogtreecommitdiff
path: root/internal/game/sys_escape_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/sys_escape_test.go')
-rw-r--r--internal/game/sys_escape_test.go225
1 files changed, 214 insertions, 11 deletions
diff --git a/internal/game/sys_escape_test.go b/internal/game/sys_escape_test.go
index f07701c..f8cd2e1 100644
--- a/internal/game/sys_escape_test.go
+++ b/internal/game/sys_escape_test.go
@@ -18,9 +18,12 @@ type escapeTestConn struct {
}
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 (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")
@@ -125,14 +128,18 @@ func TestBeginEscapeMoveWithCapeOfAgility(t *testing.T) {
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)
+ // moveTicks returns 0 for Cape of Agility. beginEscapeMove is only ever
+ // called when an escape has been stashed, and Cape-of-Agility / GodMode
+ // never enter the stash path (doMove's ticks<=0 short-circuit fires
+ // first), so this test just verifies the moveTicks call site compiles
+ // against the two-value signature.
+ ticks, usesEnergy := g.moveTicks(p, false)
if ticks != 0 {
t.Errorf("Cape of Agility should give 0 ticks, got %d", ticks)
}
+ if usesEnergy {
+ t.Error("Cape of Agility should not use energy")
+ }
}
func TestApplyMobHitCancelsEscape(t *testing.T) {
@@ -240,14 +247,210 @@ func TestEscapeClearedOnStop(t *testing.T) {
}
}
-func TestPlayerActionDisplayShowsPreparingToFlee(t *testing.T) {
+// TestPlayerActionDisplayNoPreparingToFlee verifies the (previously-unreachable)
+// "preparing to flee" display branch has been removed. While waiting to flee
+// the player is still in combat, so playerActionDisplay's in-combat branch
+// governs (returning "fighting a X" — exercised indirectly through combat
+// tests). A pure EscapeDir set with no combat engagement now yields "".
+func TestPlayerActionDisplayNoPreparingToFlee(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)
+ if display == "preparing to flee" {
+ t.Errorf("the stale 'preparing to flee' branch should be removed; got %q", display)
+ }
+}
+
+// TestEscapeReliefFiresOnThirdHit verifies the soft-lock relief: the third
+// consecutive escape-canceling mob hit in the same combat triggers the flee
+// as if the mob had missed, instead of cancelling.
+func TestEscapeReliefFiresOnThirdHit(t *testing.T) {
+ g := escapeTestGame()
+ p := player.New("relief")
+ p.HP = 100
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+
+ mob := &world.MobInstance{
+ InstanceID: "relief_mob",
+ DefID: "relief_def",
+ Name: "a goblin",
+ }
+ g.Combat.Enter(p.Name, "relief_mob")
+ sess := escapeTestSession(p)
+
+ // Three escape-canceling hits. Between hits the test re-arms the escape
+ // (in the real flow the player re-issues a direction). maxHit=1 makes
+ // RollDamage deterministic (always 1) so HP=100 survives cleanly.
+ for i := 0; i < 3; i++ {
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ }
+
+ if p.EscapeFailCount != 0 {
+ t.Errorf("after relief fired, EscapeFailCount should reset, got %d", p.EscapeFailCount)
+ }
+ if p.EscapeDir != "" {
+ t.Errorf("relief should have cleared EscapeDir, got %q", p.EscapeDir)
+ }
+ if p.MoveDirection != "north" {
+ t.Errorf("relief should set MoveDirection=north, got %q", p.MoveDirection)
+ }
+ if p.MoveTicks == 0 {
+ t.Error("relief should set MoveTicks > 0 to actually flee")
+ }
+}
+
+// TestEscapeReliefDoesNotFireBeforeThirdHit verifies the relief waits until
+// the 3rd hit; the 1st and 2nd should still cancel normally.
+func TestEscapeReliefDoesNotFireBeforeThirdHit(t *testing.T) {
+ g := escapeTestGame()
+ p := player.New("patient")
+ p.HP = 100
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+
+ mob := &world.MobInstance{
+ InstanceID: "relief_mob2",
+ DefID: "relief_def2",
+ Name: "a goblin",
+ }
+ g.Combat.Enter(p.Name, "relief_mob2")
+ sess := escapeTestSession(p)
+
+ // First hit: cancels normally.
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ if p.EscapeFailCount != 1 {
+ t.Errorf("after 1st hit, EscapeFailCount=%d want 1", p.EscapeFailCount)
+ }
+ if p.EscapeDir != "" {
+ t.Error("1st hit should cancel escape (ClearEscape)")
+ }
+ if p.MoveTicks != 0 {
+ t.Error("1st hit should not start movement")
+ }
+
+ // Second hit: cancels again.
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ if p.EscapeFailCount != 2 {
+ t.Errorf("after 2nd hit, EscapeFailCount=%d want 2", p.EscapeFailCount)
+ }
+ if p.EscapeDir != "" {
+ t.Error("2nd hit should cancel escape (ClearEscape)")
+ }
+ if p.MoveTicks != 0 {
+ t.Error("2nd hit should not start movement")
+ }
+}
+
+// TestEscapeFailCountResetsOnSuccessfulMissFlee verifies the counter resets
+// after a successful flee path (miss) — so the next flee attempt in the same
+// combat starts fresh at 0.
+func TestEscapeFailCountResetsOnSuccessfulMissFlee(t *testing.T) {
+ g := escapeTestGame()
+ p := player.New("reclucky")
+ p.HP = 100
+
+ g.Combat.Enter(p.Name, "relief_mob3")
+ mob := &world.MobInstance{
+ InstanceID: "relief_mob3",
+ DefID: "relief_def3",
+ Name: "a goblin",
+ }
+
+ sess := escapeTestSession(p)
+
+ // Two escape-canceling hits.
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ if p.EscapeFailCount != 2 {
+ t.Fatalf("expected EscapeFailCount=2 after 2 hits, got %d", p.EscapeFailCount)
+ }
+
+ // Then a miss triggers a successful flee — counter must reset.
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobMiss(sess, p, mob)
+ if p.EscapeFailCount != 0 {
+ t.Errorf("EscapeFailCount should reset on successful miss-flee, got %d", p.EscapeFailCount)
+ }
+ if p.MoveTicks == 0 {
+ t.Error("miss should have fired beginEscapeMove (MoveTicks > 0)")
+ }
+}
+
+// TestEscapeFailCountResetsOnCombatEntry verifies the counter resets when a
+// new combat starts (startCombat), so a fresh engagement begins at 0.
+func TestEscapeFailCountResetsOnCombatEntry(t *testing.T) {
+ g := escapeTestGame()
+ p := player.New("reset")
+ p.HP = 100
+ p.EscapeFailCount = 7 // carryover from prior engagement
+
+ mob := &world.MobInstance{
+ InstanceID: "fresh_mob",
+ DefID: "fresh_def",
+ Name: "a goblin",
+ HP: 10,
+ }
+ sess := escapeTestSession(p)
+ g.startCombat(sess, p, mob)
+
+ if p.EscapeFailCount != 0 {
+ t.Errorf("startCombat should reset EscapeFailCount, got %d", p.EscapeFailCount)
+ }
+}
+
+// TestEscapeReliefSuppressedOnKillingBlow verifies that when the 3rd hit
+// would simultaneously kill the player (HP drops to 0), the relief is
+// suppressed — the player dies cleanly (the live mob-attack tick subscriber
+// notices HP<=0 and calls endCombat → killPlayer) rather than seeing a
+// misleading "power through" message.
+//
+// We exercise applyMobHit directly (no subscriber), so killPlayer is not
+// invoked here; we instead verify that the relief branch did NOT fire
+// (MoveTicks stays 0).
+func TestEscapeReliefSuppressedOnKillingBlow(t *testing.T) {
+ g := escapeTestGame()
+ p := player.New("doomed")
+ p.HP = 3 // exactly enough to absorb three maxHit==1 (deterministic) hits
+
+ mob := &world.MobInstance{
+ InstanceID: "lethal_mob",
+ DefID: "lethal_def",
+ Name: "a goblin",
+ }
+ g.Combat.Enter(p.Name, "lethal_mob")
+ sess := escapeTestSession(p)
+
+ // maxHit=1 → RollDamage returns exactly 1 each call; deterministic.
+ for i := 0; i < 2; i++ {
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+ }
+ // Before the 3rd hit: HP=1. After damage: HP=0 → relief must NOT fire.
+ p.EscapeDir = "north"
+ p.EscapeTarget = 11
+ g.applyMobHit(sess, p, mob, "slash", 1)
+
+ if p.EscapeDir != "" {
+ t.Error("killing blow should still ClearEscape (EscapeDir)")
+ }
+ if p.MoveTicks != 0 {
+ t.Error("killing blow should NOT start movement (relief must be suppressed)")
+ }
+ if p.EscapeFailCount != 3 {
+ t.Errorf("EscapeFailCount after killing blow should be 3, got %d", p.EscapeFailCount)
}
}