adventure: let an owner equip and unequip from their own page
The one adventure ask that carries intent back to the game box, built the mischief way: no new network route, Pete records the equip/unequip and gogobee polls it and files a verdict. The who page grows Equip / Take off buttons on the owner's own worn and backpack panels, and a pending-changes strip that shows 'queued' until the verdict lands, never claiming a change it can't see. The item handle is the inventory row id, sent only on wearable magic items, so a non-zero id is also what gates the button. gogobee resolves the owner by localpart, never a name.
This commit is contained in:
@@ -20,6 +20,12 @@
|
||||
{{end}}
|
||||
{{if .Desc}}<p class="text-xs text-[color:var(--ink)]/55 mt-1 leading-snug">{{.Desc}}</p>{{end}}
|
||||
{{if .Effect}}<p class="text-xs text-theme-adventure/80 mt-0.5">{{.Effect}}</p>{{end}}
|
||||
{{if .EquipAction}}
|
||||
<div class="mt-1.5">
|
||||
<button type="button" class="equip-btn text-[11px] rounded-full border border-theme-adventure/40 text-theme-adventure hover:bg-theme-adventure/10 px-2.5 py-0.5 font-semibold transition-colors"
|
||||
data-action="{{.EquipAction}}" data-item-id="{{.ID}}" data-slot="{{.Slot}}" data-item-name="{{.Name}}">{{if eq .EquipAction "equip"}}Equip{{else}}Take off{{end}}</button>
|
||||
</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
|
||||
@@ -274,7 +280,7 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||||
<div id="gear-panel" data-token="{{.Mark.Token}}" class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete">
|
||||
{{if .Worn}}
|
||||
<h2 class="font-display text-xl font-bold mb-1">Worn</h2>
|
||||
<p class="text-xs text-[color:var(--ink)]/45 mb-3">{{.BondsUsed}} of 3 bonds in use</p>
|
||||
@@ -299,6 +305,14 @@
|
||||
{{range .VaultRows}}{{template "itemrow" .}}{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
|
||||
<!-- Changes you've asked for. The queue is honest: an equip lands on the
|
||||
game box's next poll, so a fresh order reads "queued", never "done".
|
||||
JS fills this from /api/equip/orders. -->
|
||||
<div id="equip-orders" class="mt-6 hidden">
|
||||
<h3 class="font-display text-lg font-bold mb-2">Pending changes</h3>
|
||||
<ul id="equip-orders-list" class="space-y-1.5 text-xs"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -363,5 +377,102 @@
|
||||
}
|
||||
timer = setInterval(refresh, 60000);
|
||||
})();
|
||||
|
||||
// The equip queue, owner side. Clicking Equip / Take off records the intent; the
|
||||
// item actually moves on the game box's next poll. So the UI never claims a change
|
||||
// landed — it shows "queued" and lets the order's own status be the truth.
|
||||
(function () {
|
||||
var panel = document.getElementById('gear-panel');
|
||||
if (!panel) return; // only the owner gets this panel at all
|
||||
var token = panel.getAttribute('data-token');
|
||||
var box = document.getElementById('equip-orders');
|
||||
var list = document.getElementById('equip-orders-list');
|
||||
|
||||
// How each terminal status reads to the owner. gogobee only rejects when the
|
||||
// item slipped out from under the order or can't be worn — a bond-cap "inert"
|
||||
// is still an applied change, and its detail line says so.
|
||||
var STATUS = {
|
||||
pending: 'queued',
|
||||
applied: 'done',
|
||||
rejected_not_owned: "couldn't — that item had already moved",
|
||||
rejected_not_worn: "couldn't — that slot was already empty",
|
||||
rejected_not_equippable: "couldn't — that item can't be worn"
|
||||
};
|
||||
|
||||
var pollTimer = null;
|
||||
|
||||
function render(orders) {
|
||||
list.innerHTML = '';
|
||||
if (!orders || !orders.length) { box.classList.add('hidden'); return; }
|
||||
box.classList.remove('hidden');
|
||||
var anyPending = false;
|
||||
orders.forEach(function (o) {
|
||||
if (o.status === 'pending') anyPending = true;
|
||||
var li = document.createElement('li');
|
||||
li.className = 'flex items-baseline justify-between gap-3';
|
||||
var verb = o.action === 'equip' ? 'Equip' : 'Take off';
|
||||
var left = document.createElement('span');
|
||||
left.className = 'flex-1';
|
||||
left.textContent = verb + ' ' + (o.item_name || o.slot || 'item');
|
||||
var right = document.createElement('span');
|
||||
right.className = 'shrink-0 ' + (o.status === 'pending'
|
||||
? 'text-[color:var(--ink)]/45'
|
||||
: (o.status === 'applied' ? 'text-theme-adventure font-semibold' : 'text-[color:var(--warn)]'));
|
||||
right.textContent = o.detail || STATUS[o.status] || o.status;
|
||||
li.appendChild(left); li.appendChild(right);
|
||||
list.appendChild(li);
|
||||
});
|
||||
// While anything is still queued, keep refreshing so the verdict lands without
|
||||
// a reload; stop once everything is terminal.
|
||||
if (anyPending && !pollTimer) {
|
||||
pollTimer = setInterval(loadOrders, 15000);
|
||||
} else if (!anyPending && pollTimer) {
|
||||
clearInterval(pollTimer); pollTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function loadOrders() {
|
||||
fetch('/api/equip/orders', { headers: { 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.ok ? r.json() : null; })
|
||||
.then(function (o) { if (o) render(o); })
|
||||
.catch(function () { /* transient — a later tick will do */ });
|
||||
}
|
||||
|
||||
panel.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.equip-btn');
|
||||
if (!btn || btn.disabled) return;
|
||||
btn.disabled = true;
|
||||
btn.classList.add('opacity-50');
|
||||
btn.textContent = 'queuing…';
|
||||
fetch('/api/equip/order', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
token: token,
|
||||
action: btn.getAttribute('data-action'),
|
||||
item_id: parseInt(btn.getAttribute('data-item-id') || '0', 10),
|
||||
slot: btn.getAttribute('data-slot') || ''
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
||||
.then(function (res) {
|
||||
if (!res.ok) {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('opacity-50');
|
||||
btn.textContent = (res.body && res.body.error) || 'try again';
|
||||
return;
|
||||
}
|
||||
btn.textContent = 'queued';
|
||||
loadOrders();
|
||||
})
|
||||
.catch(function () {
|
||||
btn.disabled = false;
|
||||
btn.classList.remove('opacity-50');
|
||||
btn.textContent = 'try again';
|
||||
});
|
||||
});
|
||||
|
||||
loadOrders();
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user