Each roster name becomes /adventure/who/{token}. Anyone sees the public sheet —
stats and equipped gear, decoded from the detail_json gogobee now hangs on each
board entry — with a live JSON re-poll so an open tab tracks HP and room as they
move. The signed-in owner sees the same page enriched with their private
inventory, vault, house, and pets, unlocked by an ownership join in the new
player_self_detail table (localpart owns token) — Pete never reverses the
anonymous token to decide it. buyerLocalpart is extracted so the storefront and
the ownership check lowercase the session name the same way.
198 lines
9.9 KiB
HTML
198 lines
9.9 KiB
HTML
{{define "title"}}{{.Mark.Name}} — {{.SiteTitle}}{{end}}
|
|
|
|
{{define "main"}}
|
|
<article class="mt-2 mb-10 max-w-3xl mx-auto" id="who" data-token="{{.Mark.Token}}">
|
|
<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> The board
|
|
</a>
|
|
</nav>
|
|
|
|
<header class="rounded-3xl bg-theme-adventure text-white p-6 sm:p-10 shadow-pete relative overflow-hidden">
|
|
<div class="absolute -top-6 -right-6 text-[12rem] opacity-20 select-none" aria-hidden="true">{{if .Mark.OnRun}}⚔{{else}}🏠{{end}}</div>
|
|
<div class="relative">
|
|
<p class="text-sm uppercase tracking-[0.2em] opacity-80">adventurer</p>
|
|
<h1 class="font-display text-3xl sm:text-4xl font-bold mt-2 leading-tight">{{.Mark.Name}}</h1>
|
|
<p class="mt-2 opacity-90">Level {{.Mark.Level}} {{.Mark.ClassRace}}</p>
|
|
<p class="mt-4 text-xs uppercase tracking-wider opacity-75" id="who-where">
|
|
{{if .Mark.OnRun}}{{.Mark.Where}}{{else}}In town{{if .Mark.Idle}} · {{.Mark.Idle}}{{end}}{{end}}
|
|
</p>
|
|
</div>
|
|
</header>
|
|
|
|
{{if .HasDetail}}
|
|
<!-- Stats + gear: public, the same anonymity model as the board. -->
|
|
<section class="mt-8 grid gap-6 sm:grid-cols-2">
|
|
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
|
<h2 class="font-display text-xl font-bold mb-4">Vitals</h2>
|
|
<div class="flex items-baseline justify-between">
|
|
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Hit points</span>
|
|
<span class="font-semibold" id="who-hp">{{.Detail.HPCurrent}} / {{.Detail.HPMax}}{{if .Detail.TempHP}} <span class="text-theme-adventure">+{{.Detail.TempHP}}</span>{{end}}</span>
|
|
</div>
|
|
<div class="mt-2 h-2 rounded-full bg-[color:var(--ink)]/10 overflow-hidden">
|
|
<div class="h-full rounded-full bg-theme-adventure transition-all" id="who-hp-bar" style="width:0%"></div>
|
|
</div>
|
|
<div class="mt-4 flex items-baseline justify-between">
|
|
<span class="text-sm uppercase tracking-wider text-[color:var(--ink)]/50">Armor class</span>
|
|
<span class="font-semibold" id="who-ac">{{.Detail.ArmorClass}}</span>
|
|
</div>
|
|
|
|
<div class="mt-5 grid grid-cols-3 gap-2">
|
|
{{range .Abilities}}
|
|
<div class="rounded-2xl bg-[color:var(--ink)]/5 px-2 py-3 text-center">
|
|
<div class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/50">{{.Label}}</div>
|
|
<div class="font-display text-lg font-bold leading-none mt-1">{{.Score}}</div>
|
|
<div class="text-xs text-[color:var(--ink)]/60">{{.ModStr}}</div>
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if .Mark.OnRun}}
|
|
<div class="mt-5 pt-4 border-t border-[color:var(--ink)]/10 space-y-1.5 text-sm" id="who-expedition">
|
|
{{if .Detail.Room}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Room</span><span class="font-semibold" id="who-room">{{.Detail.Room}}</span></div>{{end}}
|
|
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Supplies</span><span class="font-semibold" id="who-supplies">{{.Detail.Supplies}}</span></div>
|
|
{{if .Detail.ThreatLevel}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Threat</span><span class="font-semibold" id="who-threat">{{.Detail.ThreatLevel}}</span></div>{{end}}
|
|
</div>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
|
<h2 class="font-display text-xl font-bold mb-4">Gear</h2>
|
|
{{if .Detail.Gear}}
|
|
<ul class="space-y-2">
|
|
{{range .Detail.Gear}}
|
|
<li class="flex items-baseline justify-between gap-3">
|
|
<span class="text-xs uppercase tracking-wider text-[color:var(--ink)]/45 w-16 shrink-0">{{.Slot}}</span>
|
|
<span class="font-semibold flex-1">{{.Name}}{{if .Masterwork}} <span title="masterwork">★</span>{{end}}</span>
|
|
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
{{else}}
|
|
<p class="text-sm text-[color:var(--ink)]/50">Traveling light — nothing equipped.</p>
|
|
{{end}}
|
|
</div>
|
|
</section>
|
|
{{else}}
|
|
<section class="mt-8 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">No detailed sheet on file for this adventurer yet — check back after the next snapshot.</p>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{if .HasSelf}}
|
|
<!-- Owner-only: this is you. Inventory, vault, house, pets — private, served
|
|
only because your signed-in localpart owns this page's token. -->
|
|
<section class="mt-8">
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class="rounded-full bg-theme-adventure text-white text-xs font-semibold px-3 py-1">Your adventurer</span>
|
|
<span class="text-xs text-[color:var(--ink)]/45">only you can see the panels below</span>
|
|
</div>
|
|
|
|
<div class="grid gap-6 sm:grid-cols-2">
|
|
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
|
<h2 class="font-display text-xl font-bold mb-4">Home</h2>
|
|
<div class="space-y-1.5 text-sm">
|
|
<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">House tier</span><span class="font-semibold">{{if .Self.House.Tier}}{{.Self.House.Tier}}{{else}}no house{{end}}</span></div>
|
|
{{if .Self.House.LoanBalance}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Loan balance</span><span class="font-semibold">{{.Self.House.LoanBalance}}</span></div>{{end}}
|
|
{{if .Self.House.Rate}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Rate</span><span class="font-semibold">{{.Self.House.Rate}}</span></div>{{end}}
|
|
{{if .Self.House.Autopay}}<div class="flex justify-between"><span class="text-[color:var(--ink)]/50">Autopay</span><span class="font-semibold">on</span></div>{{end}}
|
|
</div>
|
|
|
|
<h3 class="font-display text-lg font-bold mt-6 mb-3">Pets</h3>
|
|
{{if .Self.Pets}}
|
|
<ul class="space-y-2 text-sm">
|
|
{{range .Self.Pets}}
|
|
<li class="flex items-baseline justify-between gap-3">
|
|
<span class="font-semibold flex-1">{{if .Name}}{{.Name}}{{else}}your {{.Type}}{{end}} <span class="text-[color:var(--ink)]/45 font-normal">{{.Type}}</span></span>
|
|
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">lv {{.Level}}{{if .ArmorTier}} · barding T{{.ArmorTier}}{{end}}</span>
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
{{else}}
|
|
<p class="text-sm text-[color:var(--ink)]/50">No pets right now.</p>
|
|
{{end}}
|
|
</div>
|
|
|
|
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
|
<h2 class="font-display text-xl font-bold mb-4">Backpack</h2>
|
|
{{if .Self.Inventory}}
|
|
<ul class="space-y-1.5 text-sm max-h-72 overflow-y-auto pr-1">
|
|
{{range .Self.Inventory}}
|
|
<li class="flex items-baseline justify-between gap-3">
|
|
<span class="flex-1">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
|
|
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Value}} · {{.Value}}g{{end}}</span>
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
{{else}}
|
|
<p class="text-sm text-[color:var(--ink)]/50">Backpack's empty.</p>
|
|
{{end}}
|
|
|
|
{{if .Self.Vault}}
|
|
<h3 class="font-display text-lg font-bold mt-6 mb-3">Vault</h3>
|
|
<ul class="space-y-1.5 text-sm max-h-52 overflow-y-auto pr-1">
|
|
{{range .Self.Vault}}
|
|
<li class="flex items-baseline justify-between gap-3">
|
|
<span class="flex-1">{{.Name}}{{if .Temper}} <span class="text-theme-adventure">+{{.Temper}}</span>{{end}}</span>
|
|
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Value}} · {{.Value}}g{{end}}</span>
|
|
</li>
|
|
{{end}}
|
|
</ul>
|
|
{{end}}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{{end}}
|
|
</article>
|
|
|
|
<script>
|
|
// The sheet is state, like the board: keep an open tab honest without a reload.
|
|
// Public detail only (HP, room, supplies) — the private panels stay as rendered.
|
|
(function () {
|
|
var root = document.getElementById('who');
|
|
if (!root) return;
|
|
var token = root.getAttribute('data-token');
|
|
|
|
function setBar() {
|
|
var el = document.getElementById('who-hp-bar');
|
|
var hp = document.getElementById('who-hp');
|
|
if (!el || !hp) return;
|
|
var m = hp.textContent.match(/(-?\d+)\s*\/\s*(\d+)/);
|
|
if (!m) return;
|
|
var cur = parseInt(m[1], 10), max = parseInt(m[2], 10);
|
|
var pct = max > 0 ? Math.max(0, Math.min(100, Math.round(cur / max * 100))) : 0;
|
|
el.style.width = pct + '%';
|
|
}
|
|
setBar();
|
|
|
|
function txt(id, v) { var el = document.getElementById(id); if (el && v != null) el.textContent = v; }
|
|
|
|
function refresh() {
|
|
fetch('/api/adventure/who/' + encodeURIComponent(token), { headers: { 'Accept': 'application/json' } })
|
|
.then(function (r) { return r.ok ? r.json() : null; })
|
|
.then(function (data) {
|
|
if (!data) return;
|
|
if (!data.live) {
|
|
txt('who-where', 'Back in town');
|
|
return;
|
|
}
|
|
var d = data.detail || {};
|
|
var mark = data.mark || {};
|
|
if (data.has_detail) {
|
|
var temp = d.temp_hp ? ' +' + d.temp_hp : '';
|
|
txt('who-hp', d.hp_current + ' / ' + d.hp_max + temp);
|
|
txt('who-ac', d.armor_class);
|
|
txt('who-room', d.room);
|
|
txt('who-supplies', d.supplies);
|
|
txt('who-threat', d.threat_level);
|
|
setBar();
|
|
}
|
|
if (mark.OnRun) txt('who-where', mark.Where);
|
|
})
|
|
.catch(function () { /* transient — next tick will do */ });
|
|
}
|
|
setInterval(refresh, 60000);
|
|
})();
|
|
</script>
|
|
{{end}}
|