Files
Pete/internal/web/templates/who.html
prosolis 16711e13e6 adventure: a click-through page for every adventurer on the board
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.
2026-07-14 22:22:52 -07:00

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}}