From 56f896b94143b4272afa4f1ee9cd85e883035765 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Fri, 22 May 2026 07:10:51 -0700 Subject: [PATCH] Pet arrival: fire after run narration, not before The run-complete emergence seam rolled the pet-arrival DM synchronously, then handed the run narration to streamFlow's paced (fire-and-forget) streamer. The 'animal in your house' prompt landed ahead of the queued 'Run complete' + loot beats. Add streamFlowThen to run the roll after the final message is delivered; fix the same inverted order in tryAutoRun. --- internal/plugin/dnd_expedition_cmd.go | 10 +++++++--- internal/plugin/dnd_zone_cmd.go | 24 ++++++++++++++++++++++++ internal/plugin/expedition_autorun.go | 16 ++++++++-------- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/internal/plugin/dnd_expedition_cmd.go b/internal/plugin/dnd_expedition_cmd.go index 63e6348..6ad2020 100644 --- a/internal/plugin/dnd_expedition_cmd.go +++ b/internal/plugin/dnd_expedition_cmd.go @@ -560,11 +560,15 @@ func (p *AdventurePlugin) expeditionCmdRun(ctx MessageContext) error { // surfaces the player alive just like an extract or abandon — roll pet // arrival here too. The roll lives in the real callers, not in // runAutopilotWalk, so the sim path (which calls the walk directly) - // never fires arrival DMs. See maybeRollPetArrivalOnEmerge. + // never fires arrival DMs. See maybeRollPetArrivalOnEmerge. Defer it + // behind the paced stream so the "animal in your house" DM lands after + // the "Run complete" beat, not before it. + var after func() if r.reason == stopComplete { - p.maybeRollPetArrivalOnEmerge(ctx.Sender) + uid := ctx.Sender + after = func() { p.maybeRollPetArrivalOnEmerge(uid) } } - return p.streamFlow(ctx.Sender, r.stream, r.finalMsg) + return p.streamFlowThen(ctx.Sender, r.stream, r.finalMsg, after) } // runAutopilotWalk runs the autopilot loop up to maxRooms times and diff --git a/internal/plugin/dnd_zone_cmd.go b/internal/plugin/dnd_zone_cmd.go index 90c4a60..5e101a2 100644 --- a/internal/plugin/dnd_zone_cmd.go +++ b/internal/plugin/dnd_zone_cmd.go @@ -817,6 +817,30 @@ func (p *AdventurePlugin) streamFlow(userID id.UserID, phaseMessages []string, f return nil } +// streamFlowThen behaves like streamFlow but runs after() once the final +// message has actually been delivered. Emergence follow-ups (e.g. the pet +// arrival DM) must land strictly *after* the paced run narration — rolling +// them before streamFlow returns races the streamer's goroutine and surfaces +// "there's an animal in your house" ahead of the "Run complete" beat the +// player is still waiting on. after may be nil. +func (p *AdventurePlugin) streamFlowThen(userID id.UserID, phaseMessages []string, finalMessage string, after func()) error { + if len(phaseMessages) == 0 { + err := p.SendDM(userID, finalMessage) + if after != nil { + after() + } + return err + } + done := p.sendZoneCombatMessages(userID, phaseMessages, finalMessage) + if after != nil { + go func() { + <-done + after() + }() + } + return nil +} + // resolveRoom dispatches to the per-room-type resolver. Returns staged // messages (intro, phases, outcome) so combat rooms can be paced with // inter-phase delays — see resolveCombatRoom for the contract. For diff --git a/internal/plugin/expedition_autorun.go b/internal/plugin/expedition_autorun.go index 0acf346..60cbc97 100644 --- a/internal/plugin/expedition_autorun.go +++ b/internal/plugin/expedition_autorun.go @@ -165,17 +165,17 @@ func (p *AdventurePlugin) tryAutoRun(e *Expedition, now time.Time) error { } // Emergence seam: a run-complete reached by the background ticker is // still a live emergence — roll pet arrival. See maybeRollPetArrivalOnEmerge. + // Deferred until after the run-summary DM below so the "animal in your + // house" prompt lands after the summary, not ahead of it. + if shouldDMAutoRun(r) { + body := renderAutoRunDM(r) + if err := p.SendDM(uid, body); err != nil { + slog.Warn("expedition: autorun DM", "user", uid, "err", err) + } + } if r.reason == stopComplete { p.maybeRollPetArrivalOnEmerge(uid) } - if !shouldDMAutoRun(r) { - return nil - } - - body := renderAutoRunDM(r) - if err := p.SendDM(uid, body); err != nil { - slog.Warn("expedition: autorun DM", "user", uid, "err", err) - } return nil }