mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
N3/P6a: let a member find the run they're standing in
Every ownership lookup in the adventure module keys on a user id, and a party member owns no row: not the expedition, not the zone run. P4 gave them activeExpeditionFor; this gives them activeZoneRunFor, and gives the DM seams the audience they never had. - activeZoneRunFor(user) -> (run, isLeader, err). An owner's lookup is exactly getActiveZoneRun, side effects and all -- in particular the §4.3 idle reap, which force-extracts the wrapping expedition. A member must never re-enter it, or glancing at the map would end the leader's run. Pinned. - expeditionAudience / fanOutExpeditionDM. Briefing, recap and digest all DM'd id.UserID(e.UserID) alone. They now loop the roster, which partyMemberIDs collapses to exactly the owner when there is none -- so a solo expedition sends the same bytes to the same user it always has. The briefing's body is expedition-scoped but its pet prefix is not: each member has their own pet and their own sheet, so the roll rides a per-reader decorator (the shape P5 settled on for combat narration). The digest's A6 event anchor rolls per member for the same reason. - releaseParty on every terminal transition. A seated member is barred from adventuring elsewhere, so a roster outliving its expedition strands the party. Deliberately NOT on 'extracting': that is a 7-day resumable limbo and !resume must bring everyone back. The roster clears when the window lapses to 'failed', which routes through completeExpedition like the rest. Rosters are still empty in production -- nothing seats a member yet -- so every loop here has exactly one element and the whole change is a no-op until P6b. Golden byte-identical, go test ./... green.
This commit is contained in:
@@ -403,32 +403,14 @@ func (p *AdventurePlugin) deliverBriefing(e *Expedition, now time.Time) error {
|
||||
body += "\n" + ml
|
||||
}
|
||||
|
||||
if uid := id.UserID(e.UserID); uid != "" {
|
||||
// The legacy overworld morning DM is skipped while underground, so
|
||||
// its 25% morning pet event fires here instead, granting the one-day
|
||||
// defense buff (reset at midnight via resetAllPetMorningDefense).
|
||||
// Pet *arrival* is handled separately on the emergence seam below —
|
||||
// not queued here — so we only roll the morning event.
|
||||
if pet, perr := loadPetState(uid); perr == nil {
|
||||
if petEvent := petMorningEvent(pet); petEvent != "" {
|
||||
if char, cerr := loadAdvCharacter(uid); cerr == nil {
|
||||
char.PetMorningDefense = true
|
||||
if serr := saveAdvCharacter(char); serr != nil {
|
||||
slog.Warn("expedition: save pet morning defense", "user", uid, "err", serr)
|
||||
}
|
||||
}
|
||||
body = fmt.Sprintf("🐾 *%s*\n\n%s", petEvent, body)
|
||||
}
|
||||
}
|
||||
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.fanOutExpeditionDM(e, body, p.briefingPetPrefix)
|
||||
// Emergence seam: a briefing-time forced extraction (starvation / abyss
|
||||
// collapse) surfaces the players 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. Every member emerges, so every member rolls.
|
||||
if e.Status == ExpeditionStatusAbandoned {
|
||||
for _, uid := range expeditionAudience(e) {
|
||||
p.maybeRollPetArrivalOnEmerge(uid)
|
||||
}
|
||||
}
|
||||
@@ -534,22 +516,9 @@ func (p *AdventurePlugin) deliverBriefingEventAnchored(e *Expedition, priorBrief
|
||||
body += "\n" + ml
|
||||
}
|
||||
|
||||
if uid := id.UserID(e.UserID); uid != "" {
|
||||
if pet, perr := loadPetState(uid); perr == nil {
|
||||
if petEvent := petMorningEvent(pet); petEvent != "" {
|
||||
if char, cerr := loadAdvCharacter(uid); cerr == nil {
|
||||
char.PetMorningDefense = true
|
||||
if serr := saveAdvCharacter(char); serr != nil {
|
||||
slog.Warn("expedition: save pet morning defense", "user", uid, "err", serr)
|
||||
}
|
||||
}
|
||||
body = fmt.Sprintf("🐾 *%s*\n\n%s", petEvent, body)
|
||||
}
|
||||
}
|
||||
if err := p.SendDM(uid, body); err != nil {
|
||||
slog.Warn("expedition: send briefing DM", "user", uid, "err", err)
|
||||
}
|
||||
if forced && e.Status == ExpeditionStatusAbandoned {
|
||||
p.fanOutExpeditionDM(e, body, p.briefingPetPrefix)
|
||||
if forced && e.Status == ExpeditionStatusAbandoned {
|
||||
for _, uid := range expeditionAudience(e) {
|
||||
p.maybeRollPetArrivalOnEmerge(uid)
|
||||
}
|
||||
}
|
||||
@@ -624,11 +593,7 @@ func (p *AdventurePlugin) deliverRecap(e *Expedition, now time.Time) error {
|
||||
body += "\n" + renderNightCheck(*night)
|
||||
}
|
||||
|
||||
if uid := id.UserID(e.UserID); uid != "" {
|
||||
if err := p.SendDM(uid, body); err != nil {
|
||||
slog.Warn("expedition: send recap DM", "user", uid, "err", err)
|
||||
}
|
||||
}
|
||||
p.fanOutExpeditionDM(e, body, nil)
|
||||
if err := appendExpeditionLog(e.ID, e.CurrentDay, "recap",
|
||||
fmt.Sprintf("evening recap — %d log entries today", len(dayEntries)), line); err != nil {
|
||||
return err
|
||||
@@ -636,6 +601,31 @@ func (p *AdventurePlugin) deliverRecap(e *Expedition, now time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// briefingPetPrefix folds the reader's own 25% morning pet event onto the front
|
||||
// of a briefing and grants the resulting one-day defense buff (reset at midnight
|
||||
// by resetAllPetMorningDefense). The legacy overworld morning DM is skipped
|
||||
// while underground, so this is where that roll lives.
|
||||
//
|
||||
// It is per-reader rather than per-expedition: each member of a party keeps
|
||||
// their own pet, and the buff lands on their own character sheet.
|
||||
func (p *AdventurePlugin) briefingPetPrefix(uid id.UserID, body string) string {
|
||||
pet, err := loadPetState(uid)
|
||||
if err != nil {
|
||||
return body
|
||||
}
|
||||
petEvent := petMorningEvent(pet)
|
||||
if petEvent == "" {
|
||||
return body
|
||||
}
|
||||
if char, cerr := loadAdvCharacter(uid); cerr == nil {
|
||||
char.PetMorningDefense = true
|
||||
if serr := saveAdvCharacter(char); serr != nil {
|
||||
slog.Warn("expedition: save pet morning defense", "user", uid, "err", serr)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("🐾 *%s*\n\n%s", petEvent, body)
|
||||
}
|
||||
|
||||
// pickMorningBriefing returns a flavor line based on day-band: Day 1, 3, 7,
|
||||
// 14, 21 each have their own pool; everything else uses the generic pool.
|
||||
func pickMorningBriefing(day int) string {
|
||||
|
||||
Reference in New Issue
Block a user