adventure: fix three review findings on the who page
- timelineLine named the viewer as their own rival on a lost duel: a rival_result arrives on the loser's page via the opponent column, so naming the opponent field pointed at themselves. Thread the page character's name through and name the other party. - buildMapView drew a frontier room twice when two visited rooms both had a door to it (Nodes carried the id twice). Place each id once. - proseGuard's board query ran before the IsGUIDSeen dedup, so a retried dispatch paid for it and threw it away. Check dedup first.
This commit is contained in:
@@ -108,6 +108,15 @@ func (s *Server) handleAdventureIngest(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "unknown event_type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// Idempotent: a re-delivered fact (gogobee retry) is a no-op success. Checked
|
||||
// before the prose-guard because the guard runs a board query (KnownCharacterNames);
|
||||
// a retried dispatch we already have a story for should cost nothing.
|
||||
if storage.IsGUIDSeen(f.GUID) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("duplicate"))
|
||||
return
|
||||
}
|
||||
|
||||
// Prefer gogobee's LLM prose when it is present and safe. Both fields must be
|
||||
// supplied — a half-authored dispatch is not a voice, and mixing an LLM
|
||||
// headline with a template lede reads as two writers. The guard is what makes
|
||||
@@ -122,13 +131,6 @@ func (s *Server) handleAdventureIngest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Idempotent: a re-delivered fact (gogobee retry) is a no-op success.
|
||||
if storage.IsGUIDSeen(f.GUID) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("duplicate"))
|
||||
return
|
||||
}
|
||||
|
||||
// A fact with no occurred_at would otherwise be stored at the Unix epoch:
|
||||
// dated 1970 on the permalink, pinned to the bottom of the section, and
|
||||
// outside every digest window. Treat "missing" as "now".
|
||||
|
||||
@@ -213,7 +213,7 @@ func (s *Server) handleAdventureWho(w http.ResponseWriter, r *http.Request) {
|
||||
if page.MoreHistory {
|
||||
events = events[:timelineCap]
|
||||
}
|
||||
page.Timeline = buildTimeline(s, events)
|
||||
page.Timeline = buildTimeline(s, entry.Name, events)
|
||||
}
|
||||
|
||||
// Owner enrichment: only when the signed-in user's localpart owns this exact
|
||||
@@ -279,14 +279,14 @@ func (s *Server) handleAdventureWhoAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// ever: Josie brings down the Rotmother"), and forty of those stacked in a column
|
||||
// read as a wall of shouting. The trail wants the noun, not the announcement. It
|
||||
// also saves a join back to stories for every row.
|
||||
func buildTimeline(s *Server, events []storage.AdvEvent) []timelineEntry {
|
||||
func buildTimeline(s *Server, subject string, events []storage.AdvEvent) []timelineEntry {
|
||||
out := make([]timelineEntry, 0, len(events))
|
||||
for _, e := range events {
|
||||
label, emoji := advEventMeta(e.EventType)
|
||||
out = append(out, timelineEntry{
|
||||
Emoji: emoji,
|
||||
Label: label,
|
||||
Line: timelineLine(e),
|
||||
Line: timelineLine(subject, e),
|
||||
When: time.Unix(e.OccurredAt, 0).UTC().Format("Jan 2, 2006"),
|
||||
Permalink: s.advPermalink(e.GUID),
|
||||
// A realm-first hoard is the treasure worth an eye; a plain find rides
|
||||
@@ -301,7 +301,7 @@ func buildTimeline(s *Server, events []storage.AdvEvent) []timelineEntry {
|
||||
// timelineLine is the short "what" of a fact: the monster, the zone, the rival.
|
||||
// Empty is fine — the label and emoji already carry the event, and inventing
|
||||
// filler for a fact that carries no nouns would just be noise.
|
||||
func timelineLine(e storage.AdvEvent) string {
|
||||
func timelineLine(subject string, e storage.AdvEvent) string {
|
||||
inZone := func(s string) string {
|
||||
if e.Zone == "" {
|
||||
return s
|
||||
@@ -321,8 +321,16 @@ func timelineLine(e storage.AdvEvent) string {
|
||||
}
|
||||
return e.Zone
|
||||
case "rival_result", "pete_duel_win", "pete_duel_loss":
|
||||
if e.Opponent != "" {
|
||||
return "vs " + e.Opponent
|
||||
// Name the *other* party. On the winner's page the rival is the opponent;
|
||||
// on the loser's page this fact arrives via the opponent column, so the
|
||||
// rival is the subject — naming e.Opponent there would point the viewer at
|
||||
// themselves. Pete duels carry no character opponent, so this stays empty.
|
||||
other := e.Opponent
|
||||
if e.Opponent == subject {
|
||||
other = e.Subject
|
||||
}
|
||||
if other != "" && other != subject {
|
||||
return "vs " + other
|
||||
}
|
||||
return ""
|
||||
case "milestone":
|
||||
|
||||
@@ -116,7 +116,16 @@ func buildMapView(m *whoMap) *mapView {
|
||||
// Group nodes by depth column, each column ordered by first appearance.
|
||||
maxDepth := 0
|
||||
byCol := map[int][]string{}
|
||||
placed := make(map[string]bool, len(m.Nodes))
|
||||
for _, n := range m.Nodes {
|
||||
// Two visited rooms can both list a door to the same withheld frontier
|
||||
// room, so the same id can appear twice in Nodes. Place it once, or it
|
||||
// draws as two overlapping discs and its edges resolve to whichever copy
|
||||
// landed last.
|
||||
if placed[n.ID] {
|
||||
continue
|
||||
}
|
||||
placed[n.ID] = true
|
||||
d := depth[n.ID]
|
||||
byCol[d] = append(byCol[d], n.ID)
|
||||
if d > maxDepth {
|
||||
|
||||
Reference in New Issue
Block a user