adventure: give the realm a map, a board and a history
Everything Pete published so far was either the present moment (the roster, the Siege bar) or one thing that happened (a dispatch, a run report). None of it said what this place IS — how many zones there are, which are harder, or whether anybody has ever actually beaten them. Three pages off one snapshot, because they are one question and every number on all three comes off the same scan of the same run history upstream: /adventure/realm the world, in difficulty order, with who is inside it /adventure/standings the board, plus Pete's own duel record /adventure/firsts the hall of firsts, as a dated history The map is deliberately not who_map.go's layout engine. That lays out a graph, and the realm has no edges — zones aren't connected, you pick one and go. Forcing a graph onto a set would imply a topology the game doesn't have. What it has is an order, so tier bands are what get drawn. A zone nobody has ever cleared looks different rather than saying so in small text, and that styling keys off the clear count, never off whether a name is attached — otherwise an opt-out would silently redraw a conquered place as one nobody has come out of. The per-kind first dot goes through a custom property instead of a .firsts-entry-zone::before rule: input.css is in Tailwind's content glob, so a hand-written class survives the purge only if its literal name can be lifted out of the file, and a name glued to ::before cannot. It failed silently. Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{{/* The realm nav, shared by the map, the board, the hall of firsts and the war
|
||||
room, so the four read as one place rather than four orphans hanging off the
|
||||
dispatch feed.
|
||||
|
||||
It lives in its own partial rather than in realm.html because each page gets
|
||||
its own parsed template set (see server.go): a {{define}} in one page's file
|
||||
is invisible to the others, and the only way to share a block is to list it
|
||||
as a shared file. Passed the whole pageData, so it can mark the current tab
|
||||
off .Path. */}}
|
||||
{{define "realmnav"}}
|
||||
<nav class="mb-5 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm">
|
||||
<a href="/adventure" class="inline-flex items-center gap-1.5 font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
||||
<span aria-hidden="true">←</span> All dispatches
|
||||
</a>
|
||||
<span class="text-[color:var(--ink)]/20" aria-hidden="true">·</span>
|
||||
<a href="/adventure/realm" class="realm-tab{{if eq .Path "/adventure/realm"}} realm-tab-on{{end}}">The map</a>
|
||||
<a href="/adventure/standings" class="realm-tab{{if eq .Path "/adventure/standings"}} realm-tab-on{{end}}">The board</a>
|
||||
<a href="/adventure/firsts" class="realm-tab{{if eq .Path "/adventure/firsts"}} realm-tab-on{{end}}">Hall of firsts</a>
|
||||
<a href="/adventure/siege" class="realm-tab{{if eq .Path "/adventure/siege"}} realm-tab-on{{end}}">The Siege</a>
|
||||
</nav>
|
||||
{{end}}
|
||||
@@ -0,0 +1,76 @@
|
||||
{{define "title"}}Hall of firsts — {{.SiteTitle}}{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<article class="mt-2 mb-10 max-w-3xl mx-auto">
|
||||
{{template "realmnav" .}}
|
||||
|
||||
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||
<div class="absolute -top-10 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">📜</div>
|
||||
<div class="relative">
|
||||
<p class="text-sm uppercase tracking-[0.2em] opacity-80">📜 Hall of firsts</p>
|
||||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">Everything that has only ever happened once.</h1>
|
||||
<p class="mt-3 opacity-90 max-w-2xl">
|
||||
The first time anyone walked out of a place alive. The first time a thing
|
||||
came out of the ground. Each of these happened exactly once in the history
|
||||
of the realm and can't happen again.
|
||||
</p>
|
||||
{{if .Firsts.Total}}
|
||||
<div class="mt-6 flex flex-wrap gap-x-8 gap-y-3 text-xs uppercase tracking-wider opacity-85">
|
||||
<span>Entries · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Firsts.Total}}</span></span>
|
||||
<span>Places opened · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Firsts.Zones}}</span></span>
|
||||
{{if .Firsts.Others}}<span>Things found · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Firsts.Others}}</span></span>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and .Firsts.Known .Firsts.Stale}}
|
||||
<p class="mt-4 text-xs bg-black/25 rounded-xl px-3 py-2">
|
||||
Ledger as of {{.Firsts.LastSeenAgo}}. Nothing in a history book goes stale exactly, but a new entry might not be here yet.
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .Firsts.Years}}
|
||||
{{range .Firsts.Years}}
|
||||
<section class="mt-8">
|
||||
<h2 class="font-display text-2xl font-bold mb-4 tabular-nums">
|
||||
{{if .Year}}{{.Year}}{{else}}Before the records{{end}}
|
||||
</h2>
|
||||
<ol class="firsts-ledger">
|
||||
{{range .Firsts}}
|
||||
<li class="firsts-entry firsts-entry-{{.Kind}}">
|
||||
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||
<span class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 font-semibold">{{.Label}}</span>
|
||||
<span class="font-display font-bold text-base">{{.Display}}</span>
|
||||
{{if .Tier}}<span class="text-[11px] text-[color:var(--ink)]/35 tabular-nums">T{{.Tier}}</span>{{end}}
|
||||
</div>
|
||||
<p class="mt-0.5 text-sm text-[color:var(--ink)]/60">
|
||||
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold text-[color:var(--ink)]/80 hover:text-theme-adventure transition">{{.Holder}}</a>
|
||||
{{else if .Holder}}<span class="font-semibold text-[color:var(--ink)]/80">{{.Holder}}</span>
|
||||
{{else}}<span class="italic text-[color:var(--ink)]/40">nobody left who'll admit to it</span>{{end}}
|
||||
{{if .When}}<span class="text-[color:var(--ink)]/35"> · {{.When}}</span>{{end}}
|
||||
</p>
|
||||
</li>
|
||||
{{end}}
|
||||
</ol>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
<p class="mt-8 text-xs text-[color:var(--ink)]/40 text-center max-w-xl mx-auto">
|
||||
An entry with no name is one where the record has outlived the record-holder —
|
||||
a thing found and long since given away, or somebody who'd rather I didn't say.
|
||||
The claim still stands.
|
||||
</p>
|
||||
|
||||
{{else}}
|
||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||||
<p class="text-sm text-[color:var(--ink)]/60">
|
||||
{{if .Firsts.Known}}
|
||||
Nothing's happened for the first time yet. Everything is about to be a first.
|
||||
{{else}}
|
||||
Haven't had the ledger through from the field yet.
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
@@ -0,0 +1,113 @@
|
||||
{{define "title"}}The realm — {{.SiteTitle}}{{end}}
|
||||
|
||||
{{/* One zone. The whole design problem of this page is in this block: a zone
|
||||
nobody has ever beaten has to LOOK different, not just say so in small
|
||||
text, because "nobody has ever done this" is the single most interesting
|
||||
fact the realm has to offer. */}}
|
||||
{{define "realmzone"}}
|
||||
<li class="realm-zone{{if .Unbeaten}} realm-zone-unbeaten{{end}}{{if .Busy}} realm-zone-busy{{end}}">
|
||||
<div class="flex items-baseline justify-between gap-3">
|
||||
<h3 class="font-display font-bold text-base flex-1 min-w-0 truncate">{{.Display}}</h3>
|
||||
{{if .Levels}}<span class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/40 shrink-0">{{.Levels}}</span>{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Atmosphere}}
|
||||
<p class="mt-1 text-xs text-[color:var(--ink)]/55 leading-relaxed">{{.Atmosphere}}</p>
|
||||
{{end}}
|
||||
|
||||
<div class="mt-2.5 text-xs">
|
||||
{{if .Unbeaten}}
|
||||
<p class="realm-unbeaten-line">Nobody has ever come back out of it.</p>
|
||||
{{else}}
|
||||
<p class="text-[color:var(--ink)]/60">
|
||||
{{/* FirstClearBy empty with Clears > 0 is the anonymised case: the zone
|
||||
has been beaten, the clearer opted out. It must not read as unbeaten. */}}
|
||||
First through:
|
||||
{{if .FirstClearToken}}<a href="/adventure/who/{{.FirstClearToken}}" class="font-semibold hover:text-theme-adventure transition">{{.FirstClearBy}}</a>
|
||||
{{else if .FirstClearBy}}<span class="font-semibold">{{.FirstClearBy}}</span>
|
||||
{{else}}<span class="italic text-[color:var(--ink)]/45">somebody who'd rather not say</span>{{end}}
|
||||
{{if .FirstWhen}}<span class="text-[color:var(--ink)]/40"> · {{.FirstWhen}}</span>{{end}}
|
||||
</p>
|
||||
<p class="mt-0.5 text-[color:var(--ink)]/45 tabular-nums">
|
||||
{{.Clears}} clear{{if ne .Clears 1}}s{{end}} by {{.Clearers}} adventurer{{if ne .Clearers 1}}s{{end}}
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .Occupants}}
|
||||
<div class="mt-2.5 pt-2.5 border-t border-[color:var(--ink)]/10">
|
||||
<p class="text-[11px] uppercase tracking-wider text-theme-adventure font-semibold">In there now</p>
|
||||
<p class="mt-1 text-xs">
|
||||
{{range $i, $o := .Occupants}}{{if $i}}<span class="text-[color:var(--ink)]/30">, </span>{{end}}<!--
|
||||
-->{{if $o.Token}}<a href="/adventure/who/{{$o.Token}}" class="font-semibold hover:text-theme-adventure transition">{{$o.Name}}</a>{{else}}<span class="font-semibold">{{$o.Name}}</span>{{end}}<!--
|
||||
-->{{if $o.Day}}<span class="text-[color:var(--ink)]/40"> (day {{$o.Day}})</span>{{end}}{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<article class="mt-2 mb-10 max-w-5xl mx-auto">
|
||||
{{template "realmnav" .}}
|
||||
|
||||
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||
<div class="absolute -top-10 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">🗺️</div>
|
||||
<div class="relative">
|
||||
<p class="text-sm uppercase tracking-[0.2em] opacity-80">🗺️ The realm</p>
|
||||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">Everywhere you can go, and what came back.</h1>
|
||||
<p class="mt-3 opacity-90 max-w-2xl">
|
||||
Every place in the world, in the order it gets harder. Some of these have been
|
||||
walked a hundred times. Some of them have never been walked at all.
|
||||
</p>
|
||||
|
||||
{{if .Realm.Known}}
|
||||
<div class="mt-6 flex flex-wrap gap-x-8 gap-y-3 text-xs uppercase tracking-wider opacity-85">
|
||||
<span>Places · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Realm.ZoneCount}}</span></span>
|
||||
<span>Beaten · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Realm.ClearedZones}}</span></span>
|
||||
<span>Never beaten · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Realm.Unbeaten}}</span></span>
|
||||
<span>Out there now · <span class="font-semibold tabular-nums normal-case tracking-normal text-base">{{.Realm.OutThere}}</span></span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if and .Realm.Known .Realm.Stale}}
|
||||
<p class="mt-4 text-xs bg-black/25 rounded-xl px-3 py-2">
|
||||
The wire's been quiet — this is the realm as of {{.Realm.LastSeenAgo}}. The places
|
||||
haven't moved, but who's out in them might have.
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if not .Realm.Known}}
|
||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||||
<p class="text-sm text-[color:var(--ink)]/60">
|
||||
I haven't had the survey through from the field yet. When it lands, the whole
|
||||
world lives on this page.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{range .Realm.Tiers}}
|
||||
<section class="mt-8{{if .Postgame}} realm-band-postgame rounded-3xl p-5 sm:p-6{{end}}">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 mb-1">
|
||||
<h2 class="font-display text-xl sm:text-2xl font-bold">{{.Label}}</h2>
|
||||
<span class="text-xs text-[color:var(--ink)]/45 tabular-nums shrink-0">
|
||||
{{.Cleared}} of {{len .Zones}} beaten
|
||||
</span>
|
||||
</div>
|
||||
{{if .Blurb}}<p class="text-sm text-[color:var(--ink)]/55 mb-4 max-w-2xl">{{.Blurb}}</p>{{end}}
|
||||
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{{range .Zones}}{{template "realmzone" .}}{{end}}
|
||||
</ul>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .Realm.Known}}
|
||||
<p class="mt-8 text-xs text-[color:var(--ink)]/40 text-center">
|
||||
Say <code class="rounded bg-[color:var(--ink)]/8 px-1.5 py-0.5 font-mono">!expedition start</code>
|
||||
to me in Matrix to pick one and go.
|
||||
</p>
|
||||
{{end}}
|
||||
</article>
|
||||
{{end}}
|
||||
@@ -22,11 +22,9 @@
|
||||
{{define "main"}}
|
||||
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="siege"
|
||||
data-active="{{if .Siege.Active}}1{{else}}0{{end}}" data-ends-at="{{.Siege.EndsAt}}">
|
||||
<nav class="mb-4">
|
||||
<a href="/adventure" class="inline-flex items-center gap-1.5 text-sm font-semibold text-[color:var(--ink)]/60 hover:text-[color:var(--ink)] transition">
|
||||
<span aria-hidden="true">←</span> All dispatches
|
||||
</a>
|
||||
</nav>
|
||||
{{/* The war room joined the realm nav when the realm pages landed: it is one
|
||||
of the four standing pages about this place, not a one-off. */}}
|
||||
{{template "realmnav" .}}
|
||||
|
||||
{{if .Siege.Active}}
|
||||
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden {{if not .Siege.Stale}}siege-live{{end}}">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
{{define "title"}}The board — {{.SiteTitle}}{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<article class="mt-2 mb-10 max-w-4xl mx-auto">
|
||||
{{template "realmnav" .}}
|
||||
|
||||
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
||||
<div class="absolute -top-10 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">🏆</div>
|
||||
<div class="relative">
|
||||
<p class="text-sm uppercase tracking-[0.2em] opacity-80">🏆 The board</p>
|
||||
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">Who's actually been getting on with it.</h1>
|
||||
<p class="mt-3 opacity-90 max-w-2xl">
|
||||
Ranked by how deep anyone has got, then by how much of the realm they've
|
||||
put behind them. Lifetime totals — nothing here decays, and nothing here
|
||||
moves while you're not playing.
|
||||
</p>
|
||||
{{if and .Standings.Known .Standings.Stale}}
|
||||
<p class="mt-4 text-xs bg-black/25 rounded-xl px-3 py-2">
|
||||
Last count came in {{.Standings.LastSeenAgo}}. Nothing on this board moves fast, but it isn't live either.
|
||||
</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{if .Standings.Rows}}
|
||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 shadow-pete overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="text-[11px] uppercase tracking-wider text-[color:var(--ink)]/45 border-b-2 border-[color:var(--ink)]/10">
|
||||
<th class="text-left font-semibold px-4 py-3 w-10">#</th>
|
||||
<th class="text-left font-semibold px-2 py-3">Adventurer</th>
|
||||
<th class="text-right font-semibold px-2 py-3" title="The deepest tier they have actually beaten a boss in">Deepest</th>
|
||||
<th class="text-right font-semibold px-2 py-3" title="Distinct zones cleared">Zones</th>
|
||||
<th class="text-right font-semibold px-2 py-3" title="Total successful clears, repeats included">Clears</th>
|
||||
<th class="text-right font-semibold px-2 py-3" title="Things nobody in the realm had ever done before">Firsts</th>
|
||||
<th class="text-right font-semibold px-2 py-3" title="Total damage dealt to every Siege boss, all time">Siege</th>
|
||||
<th class="text-right font-semibold px-4 py-3" title="Deaths I've reported">Deaths</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Standings.Rows}}
|
||||
<tr class="border-b border-[color:var(--ink)]/5 last:border-0 hover:bg-[color:var(--ink)]/[0.03] transition">
|
||||
<td class="px-4 py-3 tabular-nums text-[color:var(--ink)]/35 font-semibold">{{.Rank}}</td>
|
||||
<td class="px-2 py-3 min-w-0">
|
||||
{{if .Token}}<a href="/adventure/who/{{.Token}}" class="font-semibold hover:text-theme-adventure transition">{{.Name}}</a>
|
||||
{{else}}<span class="font-semibold">{{.Name}}</span>{{end}}
|
||||
<span class="block text-xs text-[color:var(--ink)]/40">
|
||||
lv {{.Level}}{{if .ClassRace}} · {{.ClassRace}}{{end}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-3 text-right tabular-nums">
|
||||
{{if .DeepestTier}}<span class="standings-tier standings-tier-{{.DeepestTier}}">T{{.DeepestTier}}</span>
|
||||
{{else}}<span class="text-[color:var(--ink)]/25">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-2 py-3 text-right tabular-nums{{if not .Zones}} text-[color:var(--ink)]/25{{end}}">{{if .Zones}}{{.Zones}}{{else}}—{{end}}</td>
|
||||
<td class="px-2 py-3 text-right tabular-nums{{if not .Clears}} text-[color:var(--ink)]/25{{end}}">{{if .Clears}}{{.Clears}}{{else}}—{{end}}</td>
|
||||
<td class="px-2 py-3 text-right tabular-nums">
|
||||
{{if .Firsts}}<span class="standings-firsts">{{.Firsts}}</span>{{else}}<span class="text-[color:var(--ink)]/25">—</span>{{end}}
|
||||
</td>
|
||||
<td class="px-2 py-3 text-right tabular-nums{{if not .SiegeDamage}} text-[color:var(--ink)]/25{{end}}">
|
||||
{{if .SiegeDamage}}{{.SiegeDamage}}{{else}}—{{end}}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right tabular-nums{{if not .Deaths}} text-[color:var(--ink)]/25{{end}}">{{if .Deaths}}{{.Deaths}}{{else}}—{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs text-[color:var(--ink)]/40 px-1">
|
||||
Deepest is the hardest tier they've actually put a boss down in, not the hardest
|
||||
one they've walked into. Deaths is my own count, off the dispatches I've filed.
|
||||
</p>
|
||||
|
||||
{{else}}
|
||||
<div class="mt-6 rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||||
<p class="text-sm text-[color:var(--ink)]/60">
|
||||
{{if .Standings.Known}}
|
||||
Nobody on the board yet. Make a character and it fills up.
|
||||
{{else}}
|
||||
Haven't had the count through from the field yet.
|
||||
{{end}}
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{/* Pete keeps score on himself. He can be hired onto an expedition and he
|
||||
duels; a paper that ranks everybody else and quietly leaves itself off the
|
||||
table is doing something slightly dishonest. */}}
|
||||
<section class="mt-8 rounded-3xl bg-[color:var(--card)] border-2 border-theme-adventure/30 p-6 shadow-pete">
|
||||
<h2 class="font-display text-xl font-bold">And my own record, since you asked</h2>
|
||||
{{if .Standings.PeteFought}}
|
||||
<p class="mt-2 text-sm text-[color:var(--ink)]/70">
|
||||
<span class="font-semibold text-theme-adventure tabular-nums text-lg">{{.Standings.PeteWins}}</span> won,
|
||||
<span class="font-semibold tabular-nums text-lg">{{.Standings.PeteLosses}}</span> lost.
|
||||
I file those myself, which you're welcome to hold against me.
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="mt-2 text-sm text-[color:var(--ink)]/60">
|
||||
No bouts yet. I'll report them when there are, wins and the other kind.
|
||||
</p>
|
||||
{{end}}
|
||||
</section>
|
||||
</article>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user