adventure: count the treasures an adventurer has actually found
A treasure_found fact turns loot into a trophy: a story-grade find lands on the who page as a stat tile, a named row in a Treasures found showcase, and a trail entry linking back to the dispatch. A realm-first hoard rides the priority tier the way zone_first does, and gets the same star and callout. The item name travels in the fact's stakes field, which the events log now keeps (a new nullable column, backfilled to NULL for older rows). Counting is only ever from the fact, never the vault snapshot, so a bought sword is never a trophy and a history that predates the fact is a clean zero. This is a new event_type, so Pete's ingest must be deployed before gogobee emits one, or the first finds park forever on the retry ladder.
This commit is contained in:
@@ -19,7 +19,9 @@ import (
|
||||
|
||||
// AdvEvent is one game fact, kept as fact rather than as the sentence it was
|
||||
// rendered into. Mirrors web.AdvFact minus the transport-only fields (no_push,
|
||||
// stakes, class_race) that describe delivery rather than the event.
|
||||
// class_race) that describe delivery rather than the event. Stakes is the one
|
||||
// former transport field kept here: for a treasure_found it carries the item's
|
||||
// name, which is the fact, not the delivery.
|
||||
type AdvEvent struct {
|
||||
GUID string `json:"guid"`
|
||||
EventType string `json:"event_type"`
|
||||
@@ -33,6 +35,7 @@ type AdvEvent struct {
|
||||
Tally int `json:"tally"`
|
||||
Outcome string `json:"outcome"`
|
||||
Milestone string `json:"milestone"`
|
||||
Stakes string `json:"stakes"`
|
||||
Actors []string `json:"actors"`
|
||||
OccurredAt int64 `json:"occurred_at"`
|
||||
}
|
||||
@@ -50,10 +53,10 @@ func InsertAdventureEvent(e *AdvEvent) error {
|
||||
_, err = Get().Exec(`
|
||||
INSERT OR IGNORE INTO adventure_events
|
||||
(guid, event_type, tier, subject, opponent, boss, zone, region,
|
||||
level, tally, outcome, milestone, actors, occurred_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
level, tally, outcome, milestone, stakes, actors, occurred_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
e.GUID, e.EventType, e.Tier, e.Subject, e.Opponent, e.Boss, e.Zone,
|
||||
e.Region, e.Level, e.Tally, e.Outcome, e.Milestone, string(actors), e.OccurredAt)
|
||||
e.Region, e.Level, e.Tally, e.Outcome, e.Milestone, e.Stakes, string(actors), e.OccurredAt)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -74,30 +77,39 @@ type ZoneTally struct {
|
||||
|
||||
// TrophyCase is an adventurer's whole history, counted.
|
||||
//
|
||||
// Note what is *not* here: treasure. The event vocabulary gogobee sends has no
|
||||
// loot fact — items appear only in the owner-private inventory snapshot, which
|
||||
// is a current-state list with no history and no "found it in X on day 3". So
|
||||
// "treasures found" is not derivable from anything on the wire today; it needs a
|
||||
// new fact type upstream rather than a query here. Deliberately absent instead of
|
||||
// faked out of the vault contents, which would silently count a bought sword as
|
||||
// a trophy.
|
||||
// Treasure is counted only from the treasure_found fact, never from the vault
|
||||
// snapshot: a bought sword is not a trophy, and the snapshot is current-state
|
||||
// with no "found it in X on day 3". So a treasure tally that predates the first
|
||||
// treasure_found is a clean zero, not a back-derivation.
|
||||
type TrophyCase struct {
|
||||
Name string `json:"name"`
|
||||
Bosses []BossTally `json:"bosses,omitempty"`
|
||||
Zones []ZoneTally `json:"zones,omitempty"`
|
||||
BossKills int `json:"boss_kills"` // total, including firsts
|
||||
BossFirsts int `json:"boss_firsts"` // realm-firsts among them
|
||||
ZoneClears int `json:"zone_clears"`
|
||||
ZoneFirsts int `json:"zone_firsts"`
|
||||
Deaths int `json:"deaths"`
|
||||
Retreats int `json:"retreats"`
|
||||
RivalWins int `json:"rival_wins"`
|
||||
Milestones []string `json:"milestones,omitempty"`
|
||||
Survived int `json:"survived"` // mischief contracts walked away from
|
||||
Downed int `json:"downed"` // mischief contracts that landed
|
||||
Events int `json:"events"` // total facts on file
|
||||
FirstSeen int64 `json:"first_seen"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
Name string `json:"name"`
|
||||
Bosses []BossTally `json:"bosses,omitempty"`
|
||||
Zones []ZoneTally `json:"zones,omitempty"`
|
||||
Treasures []TreasureTally `json:"treasures,omitempty"`
|
||||
BossKills int `json:"boss_kills"` // total, including firsts
|
||||
BossFirsts int `json:"boss_firsts"` // realm-firsts among them
|
||||
ZoneClears int `json:"zone_clears"`
|
||||
ZoneFirsts int `json:"zone_firsts"`
|
||||
TreasuresFound int `json:"treasures_found"` // story-grade finds, total
|
||||
TreasureFirsts int `json:"treasure_firsts"` // realm-first hoards among them
|
||||
Deaths int `json:"deaths"`
|
||||
Retreats int `json:"retreats"`
|
||||
RivalWins int `json:"rival_wins"`
|
||||
Milestones []string `json:"milestones,omitempty"`
|
||||
Survived int `json:"survived"` // mischief contracts walked away from
|
||||
Downed int `json:"downed"` // mischief contracts that landed
|
||||
Events int `json:"events"` // total facts on file
|
||||
FirstSeen int64 `json:"first_seen"`
|
||||
LastSeen int64 `json:"last_seen"`
|
||||
}
|
||||
|
||||
// TreasureTally is one story-grade find: the item's name, the zone it came out
|
||||
// of, and whether it was a realm-first hoard. Unlike bosses and zones there is no
|
||||
// repeat count — a named treasure is found once, so each is its own row.
|
||||
type TreasureTally struct {
|
||||
Item string `json:"item"`
|
||||
Zone string `json:"zone"`
|
||||
First bool `json:"first"` // first in the realm to pull this hoard
|
||||
}
|
||||
|
||||
// EventsBySubject returns every fact about a character, newest first. This is the
|
||||
@@ -119,7 +131,7 @@ func EventsBySubject(name string, limit int) ([]AdvEvent, error) {
|
||||
}
|
||||
q := `
|
||||
SELECT guid, event_type, tier, subject, opponent, boss, zone, region,
|
||||
level, tally, outcome, milestone, actors, occurred_at
|
||||
level, tally, outcome, milestone, stakes, actors, occurred_at
|
||||
FROM adventure_events
|
||||
WHERE subject = ? OR opponent = ?
|
||||
ORDER BY occurred_at DESC`
|
||||
@@ -137,15 +149,15 @@ func EventsBySubject(name string, limit int) ([]AdvEvent, error) {
|
||||
var out []AdvEvent
|
||||
for rows.Next() {
|
||||
var e AdvEvent
|
||||
var tier, subject, opponent, boss, zone, region, outcome, milestone, actors sql.NullString
|
||||
var tier, subject, opponent, boss, zone, region, outcome, milestone, stakes, actors sql.NullString
|
||||
if err := rows.Scan(&e.GUID, &e.EventType, &tier, &subject, &opponent,
|
||||
&boss, &zone, ®ion, &e.Level, &e.Tally, &outcome, &milestone,
|
||||
&actors, &e.OccurredAt); err != nil {
|
||||
&stakes, &actors, &e.OccurredAt); 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 = outcome.String, milestone.String
|
||||
e.Outcome, e.Milestone, e.Stakes = outcome.String, milestone.String, stakes.String
|
||||
if actors.String != "" {
|
||||
_ = json.Unmarshal([]byte(actors.String), &e.Actors)
|
||||
}
|
||||
@@ -211,6 +223,17 @@ func BuildTrophyCase(name string, events []AdvEvent) TrophyCase {
|
||||
if e.EventType == "zone_first" {
|
||||
tc.ZoneFirsts++
|
||||
}
|
||||
case "treasure_found":
|
||||
tc.TreasuresFound++
|
||||
// A realm-first hoard rides the priority tier, the same way zone_first
|
||||
// does; a plain story-grade find is a bulletin.
|
||||
first := e.Tier == "priority"
|
||||
if first {
|
||||
tc.TreasureFirsts++
|
||||
}
|
||||
if e.Stakes != "" {
|
||||
tc.Treasures = append(tc.Treasures, TreasureTally{Item: e.Stakes, Zone: e.Zone, First: first})
|
||||
}
|
||||
case "death":
|
||||
tc.Deaths++
|
||||
case "retreat":
|
||||
|
||||
Reference in New Issue
Block a user