From b3d4c616f59ad2519f3a0b77e3b47d6571cd2486 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Thu, 9 Jul 2026 19:29:17 -0400 Subject: feat: message actions are now sequences of messages with settable delays --- internal/game/act_interaction_seq_test.go | 216 ++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 internal/game/act_interaction_seq_test.go (limited to 'internal/game/act_interaction_seq_test.go') diff --git a/internal/game/act_interaction_seq_test.go b/internal/game/act_interaction_seq_test.go new file mode 100644 index 0000000..c3fc8fa --- /dev/null +++ b/internal/game/act_interaction_seq_test.go @@ -0,0 +1,216 @@ +package game + +import ( + "io" + "testing" + + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/combat" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +type nopConn struct{} + +func (nopConn) ReadMessage() (string, error) { return "", io.EOF } +func (nopConn) Write(p []byte) (int, error) { return len(p), nil } +func (nopConn) Close() error { return nil } +func (nopConn) SetEcho(bool) error { return nil } + +func newTestGame() *Game { + return &Game{ + GlobalFlags: NewGlobalFlagStore(), + Combat: combat.NewTracker(), + } +} + +func newTestGameWithStore(t *testing.T) *Game { + g := newTestGame() + g.AccountStore = player.NewAccountStore(t.TempDir()) + return g +} + +func newTestSession(p *player.Player, acc *player.Account) *net.Session { + if p.Options == nil { + p.Options = make(map[string]any) + } + return &net.Session{Player: p, Conn: nopConn{}, State: net.StateGame, Account: acc} +} + +func newTestPlayer(name string) *player.Player { + return &player.Player{ + Name: name, + RoomID: 100, + Skills: map[player.SkillName]int{player.Hitpoints: player.XPForLevel(99)}, + HP: 50, + Options: make(map[string]any), + } +} + +func TestRunInteractionNoMessagesAppliesInstantly(t *testing.T) { + g := newTestGameWithStore(t) + p := newTestPlayer("test") + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Heal: 10}, + } + + g.runInteraction(sess, p, step, 100, scopePlayer, nil) + + if p.Action != nil { + t.Error("expected no pending action for message-less interaction") + } + if p.HP != 60 { + t.Errorf("expected HP 60 after heal, got %d", p.HP) + } +} + +func TestRunInteractionWithMessagesStartsSequence(t *testing.T) { + g := newTestGame() + p := newTestPlayer("test") + p.Credits = 100 + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Credits: 50}, + Messages: []behavior.DelayedMessage{ + {Message: "You feel a strange energy.", Delay: 3}, + }, + } + + g.runInteraction(sess, p, step, 100, scopePlayer, nil) + + if p.Action == nil { + t.Fatal("expected pending action for message-bearing interaction") + } + if p.Action.Type != behavior.TypeInteractionSeq { + t.Errorf("expected TypeInteractionSeq, got %v", p.Action.Type) + } + if p.Action.WaitLeft != 3 { + t.Errorf("expected WaitLeft 3, got %d", p.Action.WaitLeft) + } + if p.Credits != 100 { + t.Errorf("expected credits unchanged (100), got %d", p.Credits) + } +} + +func TestAdvanceInteractionSeqDrainsZeroDelay(t *testing.T) { + g := newTestGameWithStore(t) + p := newTestPlayer("test") + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Heal: 10}, + Messages: []behavior.DelayedMessage{ + {Message: "First.", Delay: 0}, + {Message: "Second.", Delay: 0}, + {Message: "Third.", Delay: 0}, + }, + } + + g.startInteractionSeq(sess, p, step, 100, scopePlayer, nil) + + if p.Action == nil || p.Action.WaitLeft != 1 { + t.Fatalf("expected WaitLeft 1 (delay 0 floored), got Action:%v", p.Action) + } + + g.advanceInteractionSeq(sess, p) + + if p.Action != nil { + t.Error("expected action cancelled after draining all messages") + } + if p.HP != 60 { + t.Errorf("expected HP 60 after heal, got %d", p.HP) + } +} + +func TestAdvanceInteractionSeqHonorsDelay(t *testing.T) { + g := newTestGameWithStore(t) + p := newTestPlayer("test") + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Heal: 10}, + Messages: []behavior.DelayedMessage{ + {Message: "First.", Delay: 2}, + {Message: "Second.", Delay: 3}, + }, + } + + g.startInteractionSeq(sess, p, step, 100, scopePlayer, nil) + + if p.Action == nil || p.Action.WaitLeft != 2 { + t.Fatalf("expected WaitLeft 2, got %v", p.Action) + } + p.Action.WaitLeft = 1 + + g.advanceInteractionSeq(sess, p) + + if p.Action == nil { + t.Fatal("expected action still running (second message pending)") + } + if p.Action.WaitLeft != 3 { + t.Errorf("expected WaitLeft 3 (second message delay), got %d", p.Action.WaitLeft) + } + if p.HP != 50 { + t.Error("heal should not have fired yet") + } + + g.advanceInteractionSeq(sess, p) + + if p.Action != nil { + t.Error("expected action cancelled after last message + heal") + } + if p.HP != 60 { + t.Errorf("expected HP 60 after heal, got %d", p.HP) + } +} + +func TestInteractionSeqCancelPreventsTrailingEffects(t *testing.T) { + g := newTestGame() + p := newTestPlayer("test") + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Heal: 10}, + Messages: []behavior.DelayedMessage{ + {Message: "Hello.", Delay: 5}, + }, + } + + g.startInteractionSeq(sess, p, step, 100, scopePlayer, nil) + + if p.Action == nil { + t.Fatal("expected pending action") + } + + g.cancelAction(p) + + if p.Action != nil { + t.Error("expected action to be cancelled") + } + if p.HP != 50 { + t.Error("heal should not have fired after cancel") + } +} + +func TestApplyStepActionEmitsAllMessages(t *testing.T) { + g := newTestGameWithStore(t) + p := newTestPlayer("test") + sess := newTestSession(p, nil) + + step := &behavior.StepAction{ + NodeAction: behavior.NodeAction{Heal: 10}, + Messages: []behavior.DelayedMessage{ + {Message: "A.", Delay: 99}, + {Message: "B.", Delay: 99}, + }, + } + + g.applyStepAction(sess, p, step, 100, scopePlayer, nil) + + if p.HP != 60 { + t.Errorf("expected HP 60 after heal, got %d", p.HP) + } +} -- cgit v1.2.3