From 3ed2e1d8e05c0826a416c567608d3a174206b823 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Thu, 21 May 2026 22:51:00 -0700 Subject: [PATCH] Pets: roll arrival on expedition emergence, not the 08:00 morning DM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pet-arrival roll only ran in sendMorningDMs (the 08:00 overworld morning DM), which is skipped for anyone underground. Expedition players are almost never in the overworld at 08:00, so the roll never reached them — the encounter never fired despite the conditions being met. Move the roll onto the emergence seam via a shared helper maybeRollPetArrivalOnEmerge, called when a player surfaces alive (voluntary extract, abandon, survived forced extraction) and on respawn for underground deaths. Remove the now-dead morning-DM arrival roll. Story-wise: while the player was out, an animal wandered into the empty house looking for food. The 25% morning pet event still rolls only in the overworld DM and has the same reachability gap; left for a follow-up. --- internal/plugin/adventure_pets.go | 19 +++++++++++++++++++ internal/plugin/adventure_scheduler.go | 17 +++++++++++------ internal/plugin/dnd_expedition_cmd.go | 9 +++++++-- internal/plugin/dnd_expedition_cycle.go | 8 ++++++++ internal/plugin/dnd_expedition_extract.go | 8 +++++++- 5 files changed, 52 insertions(+), 9 deletions(-) diff --git a/internal/plugin/adventure_pets.go b/internal/plugin/adventure_pets.go index f3f4825..efad499 100644 --- a/internal/plugin/adventure_pets.go +++ b/internal/plugin/adventure_pets.go @@ -206,6 +206,25 @@ func petShouldArrive(pet PetState, house HouseState) bool { return rand.Float64() < 0.30 } +// maybeRollPetArrivalOnEmerge rolls the pet-arrival check when a player +// surfaces from an expedition (voluntary extract, abandon, or a survived +// forced extraction) or revives after death. The arrival roll lives on the +// emergence seam — not the legacy 08:00 overworld morning DM — because +// expedition players are almost never in the overworld at the scheduled hour, +// so the morning roll never reached them. Story beat: while the player was +// underground, an animal wandered into the empty house looking for food. +// +// Safe to call unconditionally on any emergence: petShouldArrive gates on +// house tier / not-yet-arrived, and petArrivalDM won't clobber an existing +// pending interaction. +func (p *AdventurePlugin) maybeRollPetArrivalOnEmerge(userID id.UserID) { + pet, _ := loadPetState(userID) + house, _ := loadHouseState(userID) + if petShouldArrive(pet, house) { + p.petArrivalDM(userID) + } +} + // petArrivalDM sends the initial "there's an animal in your house" DM. func (p *AdventurePlugin) petArrivalDM(userID id.UserID) { // Don't overwrite an existing pending interaction diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index 7988f3f..0661818 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -82,6 +82,12 @@ func (p *AdventurePlugin) sendMorningDMs() { if err := p.SendDM(char.UserID, text); err != nil { slog.Error("adventure: failed to send respawn DM", "user", char.UserID, "err", err) } + + // Emergence seam (death case): a player who died underground + // "comes home" on respawn. This is the deferred half of the + // emergence roll — survived extractions roll at their exit + // site; deaths roll here. See maybeRollPetArrivalOnEmerge. + p.maybeRollPetArrivalOnEmerge(char.UserID) } // Babysitting: pet-care trickle (no harvest actions; safe-rest perk @@ -133,13 +139,12 @@ func (p *AdventurePlugin) sendMorningDMs() { continue } - // Pet arrival check (fires before normal morning DM) - house, _ := loadHouseState(char.UserID) + // Pet arrival no longer rolls here. The 08:00 overworld morning DM + // is skipped for anyone underground (expedition gate above), so it + // never reached expedition players. Arrival now fires on the + // emergence seam — see maybeRollPetArrivalOnEmerge, called from the + // extract/abandon/forced-extract and respawn paths. pet, _ := loadPetState(char.UserID) - if petShouldArrive(pet, house) { - p.petArrivalDM(char.UserID) - continue - } // Morning pet event petEvent := petMorningEvent(pet) diff --git a/internal/plugin/dnd_expedition_cmd.go b/internal/plugin/dnd_expedition_cmd.go index ba63c7c..fdc2e79 100644 --- a/internal/plugin/dnd_expedition_cmd.go +++ b/internal/plugin/dnd_expedition_cmd.go @@ -486,9 +486,14 @@ func (p *AdventurePlugin) expeditionCmdAbandon(ctx MessageContext) error { } _ = retireAllRegionRuns(exp) _ = appendExpeditionLog(exp.ID, exp.CurrentDay, "narrative", "expedition abandoned", "") - return p.SendDM(ctx.Sender, fmt.Sprintf( + if err := p.SendDM(ctx.Sender, fmt.Sprintf( "Expedition in **%s** abandoned on Day %d. Supplies are forfeit. The dungeon remembers.", - zone.Display, exp.CurrentDay)) + zone.Display, exp.CurrentDay)); err != nil { + return err + } + // Emergence seam: see maybeRollPetArrivalOnEmerge. + p.maybeRollPetArrivalOnEmerge(ctx.Sender) + return nil } // helper: ensure we don't shadow id.UserID import in test harness. diff --git a/internal/plugin/dnd_expedition_cycle.go b/internal/plugin/dnd_expedition_cycle.go index f706174..fa2df6d 100644 --- a/internal/plugin/dnd_expedition_cycle.go +++ b/internal/plugin/dnd_expedition_cycle.go @@ -303,6 +303,14 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error { if err := p.SendDM(uid, body); err != nil { slog.Warn("expedition: send briefing DM", "user", uid, "err", err) } + // Emergence seam: a briefing-time forced extraction (starvation / + // abyss collapse) surfaces the player alive — roll pet arrival. + // Combat/patrol deaths never reach deliverBriefing (the row is + // already abandoned), so an abandoned status here means a survived + // emergence; those death paths roll on respawn instead. + if e.Status == ExpeditionStatusAbandoned { + p.maybeRollPetArrivalOnEmerge(uid) + } } if err := appendExpeditionLog(e.ID, e.CurrentDay, "briefing", fmt.Sprintf("morning briefing — %.1f SU consumed overnight", burn), line); err != nil { diff --git a/internal/plugin/dnd_expedition_extract.go b/internal/plugin/dnd_expedition_extract.go index 401f988..899059e 100644 --- a/internal/plugin/dnd_expedition_extract.go +++ b/internal/plugin/dnd_expedition_extract.go @@ -179,7 +179,13 @@ func (p *AdventurePlugin) handleExtractCmd(ctx MessageContext, _ string) error { } b.WriteString(fmt.Sprintf("Loot, XP, and coins are kept. The dungeon stays where you left it — `!resume` within 7 days to come back. After %s the expedition expires.", (time.Now().UTC().Add(extractResumeWindow)).Format("2006-01-02 15:04 MST"))) - return p.SendDM(ctx.Sender, b.String()) + if err := p.SendDM(ctx.Sender, b.String()); err != nil { + return err + } + // Emergence seam: surfacing from a run is when an animal may have moved + // into the empty house. + p.maybeRollPetArrivalOnEmerge(ctx.Sender) + return nil } // ── !resume command ─────────────────────────────────────────────────────────