mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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:
197
internal/plugin/adventure_activity_unified.go
Normal file
197
internal/plugin/adventure_activity_unified.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogobee/internal/db"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AdvDailyActivity is one row of "what a player did" on a given UTC date.
|
||||||
|
// Unifies the legacy adventure_activity_log table with the D&D-layer zone
|
||||||
|
// run / expedition log tables so the daily report can render a coherent
|
||||||
|
// view regardless of which subsystem the action ran through.
|
||||||
|
type AdvDailyActivity struct {
|
||||||
|
UserID id.UserID
|
||||||
|
Source string // "zone_run" | "expedition" | "rest_legacy" | "legacy"
|
||||||
|
Activity string // "zone" | "expedition" | "rest" | legacy activity_type
|
||||||
|
Location string // zone display name or legacy location
|
||||||
|
Outcome string // "completed" | "abandoned" | "boss_defeated" | "in_progress" | "rest" | legacy outcome
|
||||||
|
Summary string // one-line human description
|
||||||
|
LootValue int64 // 0 for D&D-layer entries (loot stays in run/expedition state)
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadAdvDailyActivity unions activity rows from every action surface for
|
||||||
|
// the given UTC date (format "2006-01-02"). Returns map keyed by user_id.
|
||||||
|
//
|
||||||
|
// Sources (in chronological order on output):
|
||||||
|
// - adventure_activity_log (legacy, mostly !rest post-Phase-R)
|
||||||
|
// - dnd_zone_run (zone runs whose last_action_at falls on date)
|
||||||
|
// - dnd_expedition_log (expedition log entries dated today, deduped per
|
||||||
|
// expedition into one rollup line)
|
||||||
|
//
|
||||||
|
// Each user's slice is sorted oldest→newest. Empty slice when the player
|
||||||
|
// had no activity on this date.
|
||||||
|
func loadAdvDailyActivity(date string) (map[id.UserID][]AdvDailyActivity, error) {
|
||||||
|
out := make(map[id.UserID][]AdvDailyActivity)
|
||||||
|
d := db.Get()
|
||||||
|
|
||||||
|
// 1. Legacy adventure_activity_log
|
||||||
|
rows, err := d.Query(`
|
||||||
|
SELECT user_id, activity_type, COALESCE(location,''), outcome, loot_value, logged_at
|
||||||
|
FROM adventure_activity_log
|
||||||
|
WHERE logged_at >= ? AND logged_at < DATE(?, '+1 day')
|
||||||
|
ORDER BY logged_at`, date, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("legacy log: %w", err)
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
uid, activity, location, outcome string
|
||||||
|
loot int64
|
||||||
|
ts time.Time
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&uid, &activity, &location, &outcome, &loot, &ts); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, fmt.Errorf("legacy log scan: %w", err)
|
||||||
|
}
|
||||||
|
userID := id.UserID(uid)
|
||||||
|
entry := AdvDailyActivity{
|
||||||
|
UserID: userID,
|
||||||
|
Source: "legacy",
|
||||||
|
Activity: activity,
|
||||||
|
Location: location,
|
||||||
|
Outcome: outcome,
|
||||||
|
LootValue: loot,
|
||||||
|
Timestamp: ts,
|
||||||
|
}
|
||||||
|
if activity == string(AdvActivityRest) {
|
||||||
|
entry.Source = "rest_legacy"
|
||||||
|
entry.Summary = "Rested. Streak preserved."
|
||||||
|
} else {
|
||||||
|
entry.Summary = fmt.Sprintf("%s in %s (%s)", activity, location, outcome)
|
||||||
|
}
|
||||||
|
out[userID] = append(out[userID], entry)
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
|
// 2. dnd_zone_run — rows touched today.
|
||||||
|
rows, err = d.Query(`
|
||||||
|
SELECT user_id, zone_id, current_room, total_rooms, abandoned,
|
||||||
|
boss_defeated, completed_at, last_action_at
|
||||||
|
FROM dnd_zone_run
|
||||||
|
WHERE last_action_at >= ? AND last_action_at < DATE(?, '+1 day')
|
||||||
|
ORDER BY last_action_at`, date, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("zone runs: %w", err)
|
||||||
|
}
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
uid, zoneID string
|
||||||
|
currentRoom, totalRooms int
|
||||||
|
abandoned, bossDefeated int
|
||||||
|
completedAt *time.Time
|
||||||
|
lastAction time.Time
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&uid, &zoneID, ¤tRoom, &totalRooms,
|
||||||
|
&abandoned, &bossDefeated, &completedAt, &lastAction); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, fmt.Errorf("zone run scan: %w", err)
|
||||||
|
}
|
||||||
|
userID := id.UserID(uid)
|
||||||
|
zoneDef := zoneOrFallback(ZoneID(zoneID))
|
||||||
|
|
||||||
|
var outcome, summary string
|
||||||
|
switch {
|
||||||
|
case bossDefeated == 1:
|
||||||
|
outcome = "boss_defeated"
|
||||||
|
summary = fmt.Sprintf("Cleared %s — boss down (%d/%d rooms).", zoneDef.Display, currentRoom, totalRooms)
|
||||||
|
case completedAt != nil && abandoned == 0:
|
||||||
|
outcome = "completed"
|
||||||
|
summary = fmt.Sprintf("Cleared %s (%d/%d rooms).", zoneDef.Display, currentRoom, totalRooms)
|
||||||
|
case abandoned == 1:
|
||||||
|
outcome = "abandoned"
|
||||||
|
summary = fmt.Sprintf("Withdrew from %s at room %d/%d.", zoneDef.Display, currentRoom, totalRooms)
|
||||||
|
default:
|
||||||
|
outcome = "in_progress"
|
||||||
|
summary = fmt.Sprintf("Mid-run in %s (%d/%d rooms).", zoneDef.Display, currentRoom, totalRooms)
|
||||||
|
}
|
||||||
|
out[userID] = append(out[userID], AdvDailyActivity{
|
||||||
|
UserID: userID,
|
||||||
|
Source: "zone_run",
|
||||||
|
Activity: "zone",
|
||||||
|
Location: zoneDef.Display,
|
||||||
|
Outcome: outcome,
|
||||||
|
Summary: summary,
|
||||||
|
Timestamp: lastAction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
|
// 3. dnd_expedition_log — pick the most-recent player-visible entry
|
||||||
|
// per expedition for users active today. We rollup per expedition
|
||||||
|
// rather than per row so the report doesn't list every camp/combat
|
||||||
|
// beat separately.
|
||||||
|
rows, err = d.Query(`
|
||||||
|
SELECT e.user_id, e.zone_id, e.current_day, e.status,
|
||||||
|
l.entry_type, l.summary, l.timestamp
|
||||||
|
FROM dnd_expedition_log l
|
||||||
|
JOIN dnd_expedition e ON e.expedition_id = l.expedition_id
|
||||||
|
WHERE l.timestamp >= ? AND l.timestamp < DATE(?, '+1 day')
|
||||||
|
AND l.entry_type IN ('action','combat','event','recap')
|
||||||
|
ORDER BY l.timestamp`, date, date)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("expedition log: %w", err)
|
||||||
|
}
|
||||||
|
type expKey struct {
|
||||||
|
uid, zoneID string
|
||||||
|
}
|
||||||
|
mostRecentExp := make(map[expKey]AdvDailyActivity)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
uid, zoneID, status, entryType, summary string
|
||||||
|
currentDay int
|
||||||
|
ts time.Time
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&uid, &zoneID, ¤tDay, &status,
|
||||||
|
&entryType, &summary, &ts); err != nil {
|
||||||
|
rows.Close()
|
||||||
|
return nil, fmt.Errorf("expedition log scan: %w", err)
|
||||||
|
}
|
||||||
|
userID := id.UserID(uid)
|
||||||
|
zoneDef := zoneOrFallback(ZoneID(zoneID))
|
||||||
|
k := expKey{string(userID), zoneID}
|
||||||
|
mostRecentExp[k] = AdvDailyActivity{
|
||||||
|
UserID: userID,
|
||||||
|
Source: "expedition",
|
||||||
|
Activity: "expedition",
|
||||||
|
Location: zoneDef.Display,
|
||||||
|
Outcome: status,
|
||||||
|
Summary: fmt.Sprintf("Expedition in %s, day %d — %s",
|
||||||
|
zoneDef.Display, currentDay, summary),
|
||||||
|
Timestamp: ts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
for _, entry := range mostRecentExp {
|
||||||
|
out[entry.UserID] = append(out[entry.UserID], entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort each user's slice oldest→newest so callers can pick the
|
||||||
|
// representative entry deterministically (typically the latest).
|
||||||
|
for uid := range out {
|
||||||
|
entries := out[uid]
|
||||||
|
// Insertion-sort is fine here — slices are tiny.
|
||||||
|
for i := 1; i < len(entries); i++ {
|
||||||
|
for j := i; j > 0 && entries[j-1].Timestamp.After(entries[j].Timestamp); j-- {
|
||||||
|
entries[j-1], entries[j] = entries[j], entries[j-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out[uid] = entries
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -711,11 +711,24 @@ func renderAdvOnboardingDM(userID id.UserID) string {
|
|||||||
// ── Daily Summary ────────────────────────────────────────────────────────────
|
// ── Daily Summary ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
type AdvPlayerDaySummary struct {
|
type AdvPlayerDaySummary struct {
|
||||||
DisplayName string
|
DisplayName string
|
||||||
Level int
|
|
||||||
MiningSkill int
|
// D&D layer (preferred render). HasDnDChar=false → fall back to legacy
|
||||||
ForagingSkill int
|
// skill-line render and "needs !setup" hint.
|
||||||
FishingSkill int
|
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
|
Activity string
|
||||||
Location string
|
Location string
|
||||||
Outcome string
|
Outcome string
|
||||||
@@ -729,6 +742,36 @@ type AdvPlayerDaySummary struct {
|
|||||||
DeathLocation string
|
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 {
|
func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewardSummary, players []AdvPlayerDaySummary, holidayName string) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
|
|
||||||
@@ -809,8 +852,7 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
|
|||||||
if !diedOnAdventure {
|
if !diedOnAdventure {
|
||||||
icon = "⚔️"
|
icon = "⚔️"
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("%s **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
|
sb.WriteString(fmt.Sprintf("%s **%s** — %s\n", icon, p.DisplayName, advPlayerHeadline(p)))
|
||||||
icon, p.DisplayName, p.Level, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
|
|
||||||
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
|
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
|
||||||
sb.WriteString(fmt.Sprintf(" Outcome: %s\n", p.SummaryLine))
|
sb.WriteString(fmt.Sprintf(" Outcome: %s\n", p.SummaryLine))
|
||||||
if !diedOnAdventure {
|
if !diedOnAdventure {
|
||||||
@@ -832,8 +874,7 @@ func renderAdvDailySummary(date string, tb *TwinBeeResult, tbRewards TwinBeeRewa
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.WriteString(fmt.Sprintf("⚔️ **%s** — Combat Lv.%d | Mining Lv.%d | Forage Lv.%d | Fishing Lv.%d\n",
|
sb.WriteString(fmt.Sprintf("⚔️ **%s** — %s\n", p.DisplayName, advPlayerHeadline(p)))
|
||||||
p.DisplayName, p.Level, p.MiningSkill, p.ForagingSkill, p.FishingSkill))
|
|
||||||
if p.Location != "" {
|
if p.Location != "" {
|
||||||
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
|
sb.WriteString(fmt.Sprintf(" Went to: %s\n", p.Location))
|
||||||
sb.WriteString(fmt.Sprintf(" Outcome: %s\n\n", p.SummaryLine))
|
sb.WriteString(fmt.Sprintf(" Outcome: %s\n\n", p.SummaryLine))
|
||||||
|
|||||||
@@ -205,28 +205,25 @@ func (p *AdventurePlugin) postDailySummary() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load logs for today and yesterday — players may act across the UTC boundary
|
// Load activity for today and yesterday — players may act across the UTC
|
||||||
|
// boundary. Activity unifies legacy adventure_activity_log + dnd_zone_run
|
||||||
|
// + dnd_expedition_log so the report sees every action regardless of
|
||||||
|
// which subsystem produced it.
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
todayLogs, _ := loadAdvLogsForDate(now.Format("2006-01-02"))
|
todayActs, _ := loadAdvDailyActivity(now.Format("2006-01-02"))
|
||||||
yesterdayLogs, _ := loadAdvLogsForDate(now.AddDate(0, 0, -1).Format("2006-01-02"))
|
yesterdayActs, _ := loadAdvDailyActivity(now.AddDate(0, 0, -1).Format("2006-01-02"))
|
||||||
todayLogs = append(todayLogs, yesterdayLogs...)
|
activityMap := make(map[id.UserID][]AdvDailyActivity)
|
||||||
// Group logs per user — holiday days may produce 2 entries per user
|
for uid, e := range yesterdayActs {
|
||||||
logsPerUser := make(map[id.UserID][]*AdvDayLog)
|
activityMap[uid] = append(activityMap[uid], e...)
|
||||||
for i := range todayLogs {
|
}
|
||||||
uid := todayLogs[i].UserID
|
for uid, e := range todayActs {
|
||||||
logsPerUser[uid] = append(logsPerUser[uid], &todayLogs[i])
|
activityMap[uid] = append(activityMap[uid], e...)
|
||||||
}
|
}
|
||||||
// logMap picks the last (most recent) log entry for summary display
|
|
||||||
// lootSums aggregates loot across all actions (relevant on holiday double-action days)
|
|
||||||
logMap := make(map[id.UserID]*AdvDayLog)
|
|
||||||
lootSums := make(map[id.UserID]int64)
|
lootSums := make(map[id.UserID]int64)
|
||||||
for uid, logs := range logsPerUser {
|
for uid, acts := range activityMap {
|
||||||
logMap[uid] = logs[len(logs)-1]
|
for _, a := range acts {
|
||||||
var total int64
|
lootSums[uid] += a.LootValue
|
||||||
for _, l := range logs {
|
|
||||||
total += l.LootValue
|
|
||||||
}
|
}
|
||||||
lootSums[uid] = total
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isHol, holName := isHolidayToday()
|
isHol, holName := isHolidayToday()
|
||||||
@@ -237,15 +234,25 @@ func (p *AdventurePlugin) postDailySummary() {
|
|||||||
dispName, _ := loadDisplayName(c.UserID)
|
dispName, _ := loadDisplayName(c.UserID)
|
||||||
ps := AdvPlayerDaySummary{
|
ps := AdvPlayerDaySummary{
|
||||||
DisplayName: dispName,
|
DisplayName: dispName,
|
||||||
Level: dndLevelForUser(c.UserID),
|
Level: c.CombatLevel,
|
||||||
MiningSkill: c.MiningSkill,
|
MiningSkill: c.MiningSkill,
|
||||||
ForagingSkill: c.ForagingSkill,
|
ForagingSkill: c.ForagingSkill,
|
||||||
FishingSkill: c.FishingSkill,
|
FishingSkill: c.FishingSkill,
|
||||||
}
|
}
|
||||||
|
if dnd, _ := LoadDnDCharacter(c.UserID); dnd != nil {
|
||||||
|
ps.HasDnDChar = true
|
||||||
|
ps.DnDLevel = dnd.Level
|
||||||
|
ps.DnDRace = string(dnd.Race)
|
||||||
|
ps.DnDClass = string(dnd.Class)
|
||||||
|
ps.HPCurrent = dnd.HPCurrent
|
||||||
|
ps.HPMax = dnd.HPMax
|
||||||
|
}
|
||||||
|
|
||||||
// Holiday action count from log entries
|
acts := activityMap[c.UserID]
|
||||||
|
|
||||||
|
// Holiday action count from activity entries
|
||||||
if isHol {
|
if isHol {
|
||||||
ps.HolidayActions = len(logsPerUser[c.UserID])
|
ps.HolidayActions = len(acts)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.Alive {
|
if !c.Alive {
|
||||||
@@ -255,19 +262,19 @@ func (p *AdventurePlugin) postDailySummary() {
|
|||||||
if c.DeadUntil != nil {
|
if c.DeadUntil != nil {
|
||||||
ps.DeadUntil = c.DeadUntil.Format("15:04") + " UTC"
|
ps.DeadUntil = c.DeadUntil.Format("15:04") + " UTC"
|
||||||
}
|
}
|
||||||
// Check if they died today
|
if len(acts) > 0 {
|
||||||
if log, ok := logMap[c.UserID]; ok {
|
last := acts[len(acts)-1]
|
||||||
ps.Activity = log.ActivityType
|
ps.Activity = last.Activity
|
||||||
ps.Location = log.Location
|
ps.Location = last.Location
|
||||||
ps.Outcome = log.Outcome
|
ps.Outcome = last.Outcome
|
||||||
ps.LootValue = lootSums[c.UserID] // aggregate across all actions
|
ps.LootValue = lootSums[c.UserID]
|
||||||
ps.SummaryLine = advSummaryOneLiner(c.UserID, AdvActivityType(log.ActivityType), AdvOutcomeType(log.Outcome), lootSums[c.UserID], log.Location)
|
ps.SummaryLine = last.Summary
|
||||||
}
|
}
|
||||||
players = append(players, ps)
|
players = append(players, ps)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.HasActedToday() {
|
if len(acts) == 0 {
|
||||||
ps.IsResting = true
|
ps.IsResting = true
|
||||||
if len(SummaryResting) > 0 {
|
if len(SummaryResting) > 0 {
|
||||||
ps.SummaryLine = SummaryResting[time.Now().Nanosecond()%len(SummaryResting)]
|
ps.SummaryLine = SummaryResting[time.Now().Nanosecond()%len(SummaryResting)]
|
||||||
@@ -276,14 +283,13 @@ func (p *AdventurePlugin) postDailySummary() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Active player with today's log
|
// Active player — represent with most-recent activity row.
|
||||||
if log, ok := logMap[c.UserID]; ok {
|
last := acts[len(acts)-1]
|
||||||
ps.Activity = log.ActivityType
|
ps.Activity = last.Activity
|
||||||
ps.Location = log.Location
|
ps.Location = last.Location
|
||||||
ps.Outcome = log.Outcome
|
ps.Outcome = last.Outcome
|
||||||
ps.LootValue = lootSums[c.UserID] // aggregate across all actions
|
ps.LootValue = lootSums[c.UserID]
|
||||||
ps.SummaryLine = advSummaryOneLiner(c.UserID, AdvActivityType(log.ActivityType), AdvOutcomeType(log.Outcome), lootSums[c.UserID], log.Location)
|
ps.SummaryLine = last.Summary
|
||||||
}
|
|
||||||
|
|
||||||
players = append(players, ps)
|
players = append(players, ps)
|
||||||
}
|
}
|
||||||
|
|||||||
101
internal/plugin/proddb_daily_report_test.go
Normal file
101
internal/plugin/proddb_daily_report_test.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gogobee/internal/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestProdDB_DailyReportPreview renders the Adv 2.0 daily summary against a
|
||||||
|
// copy of the prod DB and prints it. Manual smoke for the D&D-aware
|
||||||
|
// renderer.
|
||||||
|
func TestProdDB_DailyReportPreview(t *testing.T) {
|
||||||
|
src := "/home/reala-misaki/git/gogobee/data/gogobee.db"
|
||||||
|
if _, err := os.Stat(src); err != nil {
|
||||||
|
t.Skip("prod db not present")
|
||||||
|
}
|
||||||
|
dir := t.TempDir()
|
||||||
|
dst := filepath.Join(dir, "gogobee.db")
|
||||||
|
copyTestFile(t, src, dst)
|
||||||
|
|
||||||
|
db.Close()
|
||||||
|
if err := db.Init(dir); err != nil {
|
||||||
|
t.Fatalf("db.Init: %v", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(db.Close)
|
||||||
|
|
||||||
|
bootstrapPlayerMetaFromLegacy()
|
||||||
|
|
||||||
|
chars, err := loadAllAdvCharacters()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("loadAllAdvCharacters: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the most recent activity date in the DB so we render against real
|
||||||
|
// data instead of an empty "today".
|
||||||
|
now := time.Now().UTC()
|
||||||
|
dateKey := now.Format("2006-01-02")
|
||||||
|
if acts, _ := loadAdvDailyActivity(dateKey); len(acts) == 0 {
|
||||||
|
// No activity today; walk back up to 7 days for a date that has rows.
|
||||||
|
for i := 1; i <= 7; i++ {
|
||||||
|
cand := now.AddDate(0, 0, -i).Format("2006-01-02")
|
||||||
|
if acts, _ := loadAdvDailyActivity(cand); len(acts) > 0 {
|
||||||
|
dateKey = cand
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Logf("rendering report for date=%s", dateKey)
|
||||||
|
|
||||||
|
todayActs, _ := loadAdvDailyActivity(dateKey)
|
||||||
|
activityMap := make(map[string][]AdvDailyActivity)
|
||||||
|
for uid, e := range todayActs {
|
||||||
|
activityMap[string(uid)] = e
|
||||||
|
}
|
||||||
|
|
||||||
|
var players []AdvPlayerDaySummary
|
||||||
|
for _, c := range chars {
|
||||||
|
dispName, _ := loadDisplayName(c.UserID)
|
||||||
|
ps := AdvPlayerDaySummary{
|
||||||
|
DisplayName: dispName,
|
||||||
|
Level: c.CombatLevel,
|
||||||
|
MiningSkill: c.MiningSkill,
|
||||||
|
ForagingSkill: c.ForagingSkill,
|
||||||
|
FishingSkill: c.FishingSkill,
|
||||||
|
}
|
||||||
|
if dnd, _ := LoadDnDCharacter(c.UserID); dnd != nil {
|
||||||
|
ps.HasDnDChar = true
|
||||||
|
ps.DnDLevel = dnd.Level
|
||||||
|
ps.DnDRace = string(dnd.Race)
|
||||||
|
ps.DnDClass = string(dnd.Class)
|
||||||
|
ps.HPCurrent = dnd.HPCurrent
|
||||||
|
ps.HPMax = dnd.HPMax
|
||||||
|
}
|
||||||
|
|
||||||
|
acts := activityMap[string(c.UserID)]
|
||||||
|
if !c.Alive {
|
||||||
|
ps.IsDead = true
|
||||||
|
ps.DeathSource = c.DeathSource
|
||||||
|
ps.DeathLocation = c.DeathLocation
|
||||||
|
if c.DeadUntil != nil {
|
||||||
|
ps.DeadUntil = c.DeadUntil.Format("15:04") + " UTC"
|
||||||
|
}
|
||||||
|
} else if len(acts) == 0 {
|
||||||
|
ps.IsResting = true
|
||||||
|
ps.SummaryLine = "Stayed in town. The world's still here tomorrow."
|
||||||
|
} else {
|
||||||
|
last := acts[len(acts)-1]
|
||||||
|
ps.Activity = last.Activity
|
||||||
|
ps.Location = last.Location
|
||||||
|
ps.Outcome = last.Outcome
|
||||||
|
ps.SummaryLine = last.Summary
|
||||||
|
}
|
||||||
|
players = append(players, ps)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := renderAdvDailySummary(dateKey, nil, TwinBeeRewardSummary{}, players, "")
|
||||||
|
t.Logf("\n%s", out)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user