adventure: give every dispatch a card that looks like what it is
Every dispatch rendered the same image: one violet gradient, a swapped
emoji, a label. A death, a first-ever clear and a legendary hoard were
visually identical — and this image is not decoration. It is the
og:image on every link Pete puts in Matrix and the thumbnail on every
feed card. The most interesting thing that has ever happened in the
realm looked exactly like the most routine.
The card is now keyed on the dispatch GUID instead of the event type,
so the renderer can read the fact behind it and put the actual nouns on
it — the boss's name, the zone, the item, the level. Each family gets
its own palette, and treasure is tinted by the rarity gogobee already
computes and currently spends on an adjective in a sentence.
A realm-first gets visible ceremony. The priority/bulletin split is
already computed upstream and until now it only decided whether Matrix
got pinged; a thing nobody has ever done should also look different
from the ninth time somebody did it. It earns a ribbon on the card and
a badge and a heavier ring in the feed.
Siege cards carry the HP bar, which is the W1 deferral landing. The
siege fact has the boss and the defender count but never the HP, so the
bar comes from the war-room snapshot: the live row while that boss is
still camped, the history once it has closed. When neither has it yet —
the real two-minute window between a win being filed and the push that
explains it — the card renders barless rather than wrong, and caches
for five minutes instead of a day so the unfurl isn't pinned that way.
Nothing needs backfilling. A story with a type-keyed image_url still
serves, and a dispatch with no stored fact degrades to the old emblem.
Two things learned by looking at the rendered output rather than at the
tests, both of which unit tests would never have caught: the feed
thumbnail is a 16/10 crop of a 1200x630 card and was eating the chip
("EGENDARY"), hence advSafeX; and an empty bar under a victory headline
reads at a glance as a wipe, hence the event-aware caption.
Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The durable record of what has actually happened in the realm.
|
||||
@@ -60,6 +61,73 @@ func InsertAdventureEvent(e *AdvEvent) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// AdventureEventByGUID returns the stored fact behind one dispatch, or nil when
|
||||
// there isn't one. Missing is normal, not an error: the story row is inserted
|
||||
// first and the fact record is best-effort (see handleAdventureIngest), and every
|
||||
// dispatch that predates the fact table has a story and no fact at all. Callers
|
||||
// render the thinner event_type-only view in that case.
|
||||
func AdventureEventByGUID(guid string) (*AdvEvent, error) {
|
||||
if guid == "" {
|
||||
return nil, nil
|
||||
}
|
||||
var e AdvEvent
|
||||
var tier, subject, opponent, boss, zone, region, outcome, milestone, stakes, actors sql.NullString
|
||||
err := Get().QueryRow(`
|
||||
SELECT guid, event_type, tier, subject, opponent, boss, zone, region,
|
||||
level, tally, outcome, milestone, stakes, actors, occurred_at
|
||||
FROM adventure_events WHERE guid = ?`, guid).Scan(
|
||||
&e.GUID, &e.EventType, &tier, &subject, &opponent, &boss, &zone, ®ion,
|
||||
&e.Level, &e.Tally, &outcome, &milestone, &stakes, &actors, &e.OccurredAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
e.Tier, e.Subject, e.Opponent = tier.String, subject.String, opponent.String
|
||||
e.Boss, e.Zone, e.Region = boss.String, zone.String, region.String
|
||||
e.Outcome, e.Milestone, e.Stakes = outcome.String, milestone.String, stakes.String
|
||||
if actors.String != "" {
|
||||
_ = json.Unmarshal([]byte(actors.String), &e.Actors)
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// AdventureEventFacets returns the (event_type, tier, outcome) of many dispatches
|
||||
// in one read, keyed by guid. It exists so a feed page can tint two dozen cards
|
||||
// by what they actually are without paying a query per card — the pool is
|
||||
// MaxOpenConns(1), so N round trips would serialize behind each other.
|
||||
//
|
||||
// Guids absent from the map are dispatches with no fact row; the caller leaves
|
||||
// those untinted rather than guessing.
|
||||
func AdventureEventFacets(guids []string) map[string]AdvEvent {
|
||||
out := make(map[string]AdvEvent, len(guids))
|
||||
if len(guids) == 0 {
|
||||
return out
|
||||
}
|
||||
q := `SELECT guid, event_type, tier, outcome FROM adventure_events WHERE guid IN (?` +
|
||||
strings.Repeat(`,?`, len(guids)-1) + `)`
|
||||
args := make([]any, len(guids))
|
||||
for i, g := range guids {
|
||||
args[i] = g
|
||||
}
|
||||
rows, err := Get().Query(q, args...)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var e AdvEvent
|
||||
var tier, outcome sql.NullString
|
||||
if err := rows.Scan(&e.GUID, &e.EventType, &tier, &outcome); err != nil {
|
||||
return out
|
||||
}
|
||||
e.Tier, e.Outcome = tier.String, outcome.String
|
||||
out[e.GUID] = e
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// BossTally is one monster and how many times this adventurer has put it down.
|
||||
type BossTally struct {
|
||||
Boss string `json:"boss"`
|
||||
|
||||
Reference in New Issue
Block a user