mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
- Boss epilogues: a 2-3 sentence campaign capstone per zone boss, tying each kill to the Hollow King arc. Appended to the boss-down moment in both close-out paths (finishCombatSession solo, finishPartyWin party), gated on the boss room (!elite) so it fires for any boss kill — expedition or legacy !zone — and never for elites or the arena (which has no ZoneID entry). Forest of Shadows is the King himself; its epilogue frames the fall as a shed shell, leaving the arc for the finale. - TwinBee digest reactions: a journal page found mid-expedition writes a "journal" log beat; the end-of-day digest emits one first-person, deterministically-picked TwinBee line reacting to the day's pages. No net-new DM — it rides the existing night-camp digest. Golden byte-identical; go test ./... green. Claude-Session: https://claude.ai/code/session_017mEwUmmS7aQTP2NQXj6rUa
140 lines
3.8 KiB
Go
140 lines
3.8 KiB
Go
package plugin
|
|
|
|
import (
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
)
|
|
|
|
// Long-expedition D4-a — end-of-day digest renderer.
|
|
//
|
|
// When the autopilot pitches a Night=true camp the dungeon has effectively
|
|
// closed the day. Instead of dripping per-tick auto-walk DMs through the
|
|
// player's channel, D4-a suppresses those mid-day surfaces and rolls them
|
|
// into a single rollup posted alongside the night-camp block.
|
|
//
|
|
// Data source: dnd_expedition_log. The autorun ticker now writes a `walk`
|
|
// entry per successful background walk; combined with the existing
|
|
// harvest / interrupt / threat / milestone / narrative entries this gives
|
|
// the digest enough structure to summarize the day in counts + a small
|
|
// number of headline lines, without re-rendering the full per-room
|
|
// narration that the per-tick stream used to carry.
|
|
//
|
|
// Tone: terse, TwinBee first-person (feedback_twinbee_voice), no trailing
|
|
// recap paragraph (feedback_skip_recaps). The night-camp block follows
|
|
// immediately after.
|
|
|
|
// renderEndOfDayDigest pulls every log entry for (expID, prevDay) and
|
|
// returns a markdown rollup. Returns "" when prevDay has no entries —
|
|
// the caller should fall back to the bare camp block in that case.
|
|
func renderEndOfDayDigest(expID string, prevDay int) string {
|
|
entries, err := dayExpeditionLog(expID, prevDay)
|
|
if err != nil {
|
|
slog.Warn("expedition: digest log fetch", "expedition", expID, "day", prevDay, "err", err)
|
|
return ""
|
|
}
|
|
if len(entries) == 0 {
|
|
return ""
|
|
}
|
|
var (
|
|
walks int
|
|
harvests int
|
|
interrupts int
|
|
forageLines []string
|
|
threatLines []string
|
|
milestoneLine []string
|
|
narrativeBits []string
|
|
journalPages int
|
|
)
|
|
for _, e := range entries {
|
|
switch e.Type {
|
|
case "walk":
|
|
walks++
|
|
case "journal":
|
|
journalPages++
|
|
case "harvest":
|
|
// Only count successful gathers — failed rolls / errors are noise.
|
|
if strings.Contains(e.Summary, "success") {
|
|
harvests++
|
|
}
|
|
case "forage":
|
|
forageLines = append(forageLines, e.Summary)
|
|
case "interrupt":
|
|
interrupts++
|
|
case "threat":
|
|
if e.Flavor != "" {
|
|
threatLines = append(threatLines, e.Flavor)
|
|
} else if e.Summary != "" {
|
|
threatLines = append(threatLines, e.Summary)
|
|
}
|
|
case "milestone":
|
|
milestoneLine = append(milestoneLine, e.Summary)
|
|
case "narrative":
|
|
// Surface real narrative beats (camp broken / extraction prompts),
|
|
// skip housekeeping ones.
|
|
if strings.Contains(e.Summary, "autopilot broke camp") {
|
|
continue
|
|
}
|
|
narrativeBits = append(narrativeBits, e.Summary)
|
|
}
|
|
}
|
|
|
|
var b strings.Builder
|
|
b.WriteString(fmt.Sprintf("📜 *Day %d wraps up.*\n\n", prevDay))
|
|
bulleted := false
|
|
if walks > 0 {
|
|
b.WriteString(fmt.Sprintf("• Walked **%d** auto-tick%s through the dark.\n", walks, pluralS(walks)))
|
|
bulleted = true
|
|
}
|
|
if interrupts > 0 {
|
|
b.WriteString(fmt.Sprintf("• Handled **%d** interrupt%s along the way.\n", interrupts, pluralS(interrupts)))
|
|
bulleted = true
|
|
}
|
|
if harvests > 0 {
|
|
b.WriteString(fmt.Sprintf("• Came back with **%d** harvest%s.\n", harvests, pluralS(harvests)))
|
|
bulleted = true
|
|
}
|
|
for _, f := range forageLines {
|
|
b.WriteString("• ")
|
|
b.WriteString(f)
|
|
b.WriteString("\n")
|
|
bulleted = true
|
|
}
|
|
for _, t := range threatLines {
|
|
b.WriteString("• ")
|
|
b.WriteString(t)
|
|
b.WriteString("\n")
|
|
bulleted = true
|
|
}
|
|
for _, m := range milestoneLine {
|
|
b.WriteString("• ")
|
|
b.WriteString(m)
|
|
b.WriteString("\n")
|
|
bulleted = true
|
|
}
|
|
for _, n := range narrativeBits {
|
|
b.WriteString("• ")
|
|
b.WriteString(n)
|
|
b.WriteString("\n")
|
|
bulleted = true
|
|
}
|
|
if react := twinBeeJournalReaction(prevDay, journalPages); react != "" {
|
|
b.WriteString("• ")
|
|
b.WriteString(react)
|
|
b.WriteString("\n")
|
|
bulleted = true
|
|
}
|
|
if !bulleted {
|
|
// All entries were filtered out — fall back to the bare camp block.
|
|
return ""
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func pluralS(n int) string {
|
|
if n == 1 {
|
|
return ""
|
|
}
|
|
return "s"
|
|
}
|