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) }