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.go | 97 ++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 internal/game/act_interaction_seq.go (limited to 'internal/game/act_interaction_seq.go') diff --git a/internal/game/act_interaction_seq.go b/internal/game/act_interaction_seq.go new file mode 100644 index 0000000..8b515c4 --- /dev/null +++ b/internal/game/act_interaction_seq.go @@ -0,0 +1,97 @@ +package game + +import ( + "thehouseoficarus/internal/behavior" + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +// interactionSeqData is the payload carried by p.Action.Data for an in-flight +// delayed-message interaction sequence (on_use, on_look, on_kill, or +// on_traverse). Messages are emitted one-by-one with per-message delays; the +// trailing StepAction effects fire only after every message has been shown. If +// the player is interrupted (move, quit, new action, combat) cancelAction +// clears the pending Action and the trailing effects never run. +type interactionSeqData struct { + Msgs []behavior.DelayedMessage + Idx int + Step *behavior.StepAction + RoomID int + Scope effectScope + FlagVal any +} + +// startInteractionSeq fires the first delayed message (on the next tick, +// matching the trigger scheduler's "delay = ticks to wait before this step +// fires" rule with a floor of 1 so the first message ALWAYS arrives next tick +// even when delay is 0). The trailing non-message effects run once the last +// message has been emitted. +func (g *Game) startInteractionSeq(sess *net.Session, p *player.Player, step *behavior.StepAction, roomID int, sc effectScope, flagValue any) { + g.cancelAction(p) + + firstDelay := 0 + if len(step.Messages) > 0 { + firstDelay = step.Messages[0].Delay + } + if firstDelay < 1 { + firstDelay = 1 + } + + // Strip Messages from step before stashing — applyStepAction would otherwise + // re-emit them when the trailing effects run. + clone := *step + clone.Messages = nil + + p.Action = &behavior.Action{ + Type: behavior.TypeInteractionSeq, + WaitLeft: firstDelay, + Data: &interactionSeqData{ + Msgs: step.Messages, + Idx: 0, + Step: &clone, + RoomID: roomID, + Scope: sc, + FlagVal: flagValue, + }, + } +} + +// advanceInteractionSeq is the per-tick handler for TypeInteractionSeq +// actions. Each invocation drains any zero-delay messages that are ready, emits +// the current delayed message, then sets the wait for the next message (if any). +// After the last message the trailing effects fire and the action is cancelled. +func (g *Game) advanceInteractionSeq(sess *net.Session, p *player.Player) { + d, ok := p.Action.Data.(*interactionSeqData) + if !ok { + g.cancelAction(p) + return + } + + var playerName string + if p != nil { + playerName = p.Name + } + + // Drain zero-delay messages that have accumulated, then emit the current + // (delayed) one. Matches the trigger scheduler's drain-zero-delay pattern. + for d.Idx < len(d.Msgs) { + msg := d.Msgs[d.Idx] + d.Idx++ + g.writePlayerMessage(sess, msg.Message, playerName, d.FlagVal) + + if d.Idx >= len(d.Msgs) { + break + } + next := d.Msgs[d.Idx].Delay + if next > 0 { + p.Action.WaitLeft = next + return + } + } + + // All messages emitted — fire trailing effects. + if d.Step != nil { + g.applyStepAction(sess, p, d.Step, d.RoomID, d.Scope, d.FlagVal) + } + g.cancelAction(p) +} -- cgit v1.2.3