adventure: show the party, the pets, and what you missed
Four small surfaces the game has had all along and the web has never shown. The party roster on the adventurer page is the one with a bug behind it. The board resolved an expedition by owner id, so a player seated on somebody else's run has been reading as "idle in town" while standing in a tier-4 dungeon. Seats now ride the roster detail beside the supply and threat numbers, and an opted-out player's seat is anonymised rather than dropped: a party of three rendered as a pair contradicts everything printed next to it. Pets show their levelling. They have earned XP from every won fight since that wiring was fixed and the only place a level ever appeared was a Matrix line that scrolled away. The threshold comes from the engine, in the engine's own centi-XP, because a copy of the curve here would drift the first time a band moved. "While you were away" is the one panel on the site about the reader rather than the realm. Two stamps behind it, not one: a single column would show the news, move the clock, and render an empty box over the same events on the reader's first refresh. And the story permalink names its region, which has been hardcoded empty behind a `// reserved` comment since the page shipped.
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"pete/internal/storage"
|
||||
@@ -34,6 +36,78 @@ type whoDetail struct {
|
||||
ThreatLevel int `json:"threat_level"`
|
||||
Room string `json:"room"`
|
||||
Map *whoMap `json:"map"`
|
||||
// Party is who else is down there, leader first. Absent on a solo run.
|
||||
Party []partySeat `json:"party"`
|
||||
}
|
||||
|
||||
// partySeat is one body on a shared expedition as gogobee described it. Kind is
|
||||
// "leader", "member" or "companion" — the game keeps those three carefully
|
||||
// distinct and so does this page: a companion fights but is nobody's account, so
|
||||
// he is named without a link and never counted as a player.
|
||||
//
|
||||
// A seat with a Kind but no Name is an opted-out player, kept on purpose. gogobee
|
||||
// anonymises rather than deletes here (the Siege contributor rule, not the realm
|
||||
// occupant rule), because a party of three rendered as a pair contradicts the
|
||||
// supply burn and threat level printed beside it. Render it as an unnamed seat;
|
||||
// never as an absent one, and never with a link.
|
||||
type partySeat struct {
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Token string `json:"token"`
|
||||
Level int `json:"level"`
|
||||
}
|
||||
|
||||
// Anonymous reports whether this seat belongs to a player who has opted out of
|
||||
// the news. Keyed on the name rather than the token because the two come apart
|
||||
// only in the direction that matters: gogobee never sends a token without a name.
|
||||
func (p partySeat) Anonymous() bool { return p.Kind != "companion" && p.Name == "" }
|
||||
|
||||
// petRow is one pet with its levelling made legible. gogobee sends centi-XP and
|
||||
// the engine's own threshold for the pet's current level band; the arithmetic
|
||||
// here is presentation only — a percentage for the bar and a decimal for the
|
||||
// label — and there is deliberately no copy of the curve on this side.
|
||||
type petRow struct {
|
||||
storage.PetView
|
||||
// Capped is the level ceiling: gogobee reports 0 needed, which is not the same
|
||||
// as an empty bar and must not render as one.
|
||||
Capped bool
|
||||
// Percent is 0-100 for the bar's width. Clamped, because a pet can sit above
|
||||
// its own threshold for the moment between earning XP and the next level-up
|
||||
// pass, and a bar wider than its track breaks the layout rather than the maths.
|
||||
Percent int
|
||||
// Progress is the human label: "7.5 / 20".
|
||||
Progress string
|
||||
// NextLevel is what the bar is filling toward. 0 when capped.
|
||||
NextLevel int
|
||||
}
|
||||
|
||||
// petRows makes the pushed pets renderable. Pets are owner-only — the public
|
||||
// sheet has never carried them — so this runs behind the ownership join.
|
||||
func petRows(pets []storage.PetView) []petRow {
|
||||
if len(pets) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]petRow, 0, len(pets))
|
||||
for _, p := range pets {
|
||||
row := petRow{PetView: p, Capped: p.XPNeeded <= 0}
|
||||
if !row.Capped {
|
||||
row.Percent = min(100, max(0, p.XP*100/p.XPNeeded))
|
||||
row.Progress = fmt.Sprintf("%s / %s", centiXP(p.XP), centiXP(p.XPNeeded))
|
||||
row.NextLevel = p.Level + 1
|
||||
}
|
||||
out = append(out, row)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// centiXP renders the game's hundredths as the number a player recognises. A pet
|
||||
// earns 1.5 XP per action, so the halves are real and dropping them would make a
|
||||
// bar that visibly moved report the same figure twice.
|
||||
func centiXP(centi int) string {
|
||||
if centi%100 == 0 {
|
||||
return strconv.Itoa(centi / 100)
|
||||
}
|
||||
return strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", float64(centi)/100), "0"), ".")
|
||||
}
|
||||
|
||||
// whoMap is the fog-of-war zone graph as gogobee cut it: visited rooms with
|
||||
@@ -95,6 +169,7 @@ type whoPage struct {
|
||||
Worn []itemRow
|
||||
Backpack []itemRow
|
||||
VaultRows []itemRow
|
||||
PetRows []petRow
|
||||
BondsUsed int
|
||||
// History. Unlike everything above, these are not a gogobee snapshot — they
|
||||
// are counted from the facts Pete has been keeping since adventure_events
|
||||
@@ -244,6 +319,7 @@ func (s *Server) handleAdventureWho(w http.ResponseWriter, r *http.Request) {
|
||||
page.Worn = itemRows(self.Equipped, "worn")
|
||||
page.Backpack = itemRows(self.Inventory, "backpack")
|
||||
page.VaultRows = itemRows(self.Vault, "vault")
|
||||
page.PetRows = petRows(self.Pets)
|
||||
for _, it := range self.Equipped {
|
||||
if it.Attuned {
|
||||
page.BondsUsed++
|
||||
|
||||
Reference in New Issue
Block a user