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