Adv 2.0 daily report: migrate to D&D-native render + unified activity source

Two longstanding warts surfaced when the user inspected today's TwinBee
daily report:

1. Active players were rendering "Acted today — no log recorded." The
   adventure_activity_log path only captures !rest now; harvest/zone/
   expedition action logging was lost during Phase R/L migration. The
   stale HasActedToday() check fired on fossil harvest_actions_used
   counters that nothing modern increments.

2. Per-player headline still rendered legacy four-skill line ("Combat
   Lv.X | Mining Lv.Y | Forage Lv.Z | Fishing Lv.W"). No D&D level,
   race, class, or HP — discoverability gap for the layer that's now
   canonical.

Fix both:

- adventure_activity_unified.go: new loadAdvDailyActivity(date) unions
  dnd_zone_run (zone runs touched today) + dnd_expedition_log
  (expedition activity rolled up per expedition) + legacy
  adventure_activity_log (mostly !rest). One source of truth.

- adventure_render.go: AdvPlayerDaySummary gains HasDnDChar/DnDLevel/
  DnDRace/DnDClass/HPCurrent/HPMax. New advPlayerHeadline helper
  renders "Lv.N Race Class — HP cur/max" when a dnd_character row
  exists, falls back to the legacy four-skill line plus an inline
  "(no D&D sheet — run !setup)" nudge for accounts that haven't
  completed setup yet. titleizeDnDToken("half_elf") → "Half-Elf".

- adventure_scheduler.go: postDailySummary swaps loadAdvLogsForDate
  for loadAdvDailyActivity. IsResting now derives from "len(acts)==0"
  rather than the fossil HasActedToday() counter check, so dead
  harvest_actions_used bytes can't trip the active-player branch.

- proddb_daily_report_test.go: renders the report against a copy of
  data/gogobee.db and prints it for visual confirmation. Verified
  output for 2026-05-09 shows real zone-run outcomes (Withdrew from
  Sunken Temple at room 1/6, etc.) instead of the previous "no log
  recorded" placeholder.

go vet + go test ./... clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 13:42:55 -07:00
parent c8849fd819
commit 35fa2b8323
4 changed files with 391 additions and 46 deletions

View File

@@ -711,11 +711,24 @@ func renderAdvOnboardingDM(userID id.UserID) string {
// ── Daily Summary ────────────────────────────────────────────────────────────
type AdvPlayerDaySummary struct {
DisplayName string
Level int
MiningSkill int
ForagingSkill int
FishingSkill int
DisplayName string
// D&D layer (preferred render). HasDnDChar=false → fall back to legacy
// skill-line render and "needs !setup" hint.
HasDnDChar bool
DnDLevel int
DnDRace string
DnDClass string
HPCurrent int
HPMax int
// Legacy AdventureCharacter skill levels (rendered as fallback when
// HasDnDChar is false).
Level int // legacy combat level
MiningSkill int
ForagingSkill int
FishingSkill int
Activity string
Location string
Outcome string
@@ -729,6 +742,36 @@ type AdvPlayerDaySummary struct {
DeathLocation string
}
// advPlayerHeadline renders the per-player headline for the daily report.
// D&D shape ("Lv.4 Half-Elf Cleric — HP 27/27") is preferred; falls back to
// the legacy four-skill line for accounts that haven't run !setup yet.
func advPlayerHeadline(p *AdvPlayerDaySummary) string {
if p.HasDnDChar {
race := titleizeDnDToken(p.DnDRace)
class := titleizeDnDToken(p.DnDClass)
return fmt.Sprintf("Lv.%d %s %s — HP %d/%d",
p.DnDLevel, race, class, p.HPCurrent, p.HPMax)
}
return fmt.Sprintf("Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d _(no D&D sheet — run `!setup`)_",
p.Level, p.MiningSkill, p.ForagingSkill, p.FishingSkill)
}
// titleizeDnDToken converts a snake_case D&D enum token like "half_elf" into
// a human-readable label like "Half-Elf". Single-word tokens just title-case.
func titleizeDnDToken(s string) string {
if s == "" {
return ""
}
parts := strings.Split(s, "_")
for i, p := range parts {
if p == "" {
continue
}
parts[i] = strings.ToUpper(p[:1]) + p[1:]
}
return strings.Join(parts, "-")
}
func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewardSummary, players []AdvPlayerDaySummary, holidayName string) string {
var sb strings.Builder
@@ -809,8 +852,7 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
if !diedOnAdventure {
icon = "⚔️"
}
sb.WriteString(fmt.Sprintf("%s **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
icon, p.DisplayName, p.Level, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
sb.WriteString(fmt.Sprintf("%s **%s** — %s\n", icon, p.DisplayName, advPlayerHeadline(p)))
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
sb.WriteString(fmt.Sprintf(" Outcome: %s\n", p.SummaryLine))
if !diedOnAdventure {
@@ -832,8 +874,7 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
continue
}
sb.WriteString(fmt.Sprintf("⚔️ **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
p.DisplayName, p.Level, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
sb.WriteString(fmt.Sprintf("⚔️ **%s** — %s\n", p.DisplayName, advPlayerHeadline(p)))
if p.Location != "" {
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
sb.WriteString(fmt.Sprintf(" Outcome: %s\n\n", p.SummaryLine))