adventure: ask 7 — full equipment management from the web
Extends equip-from-the-web (ask 5, magic-only) to all five standard gear slots. Owners get an Equipment panel on their own who page with: - Take off for worn masterwork/arena pieces (round-trippable to pack) - Upgrade to the next shop tier (spends euros, confirm-gated) - Repair a damaged slot (spends euros, confirm-gated) The public Gear panel is hidden for the owner since this supersedes it. Wire: equip_orders gains a tier column; new actions upgrade/repair; new verdicts rejected_downgrade / rejected_insufficient_funds / rejected_max_tier. PlayerDetail carries Slots (EquipSlotView x5) + Balance for the confirm dialogs. handleEquipOrder resolves take-off/upgrade/repair from pd.Slots server-side and rejects a client-forged tier (409), same as ask 5 trusts only Pete's own record. Verified: full suite green, headless render of the panel + confirm dialog in both day and night phases. gogobee ships the poll-apply half separately; Pete deploys first so its ingest accepts the new verdicts before gogobee emits them.
This commit is contained in:
+120
-10
@@ -117,6 +117,9 @@
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{/* Public gear list. Hidden for the owner: their own "Equipment" panel below
|
||||
supersedes it with live tiers, conditions, and management controls. */}}
|
||||
{{if not .HasSelf}}
|
||||
<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}}
|
||||
@@ -133,6 +136,7 @@
|
||||
<p class="text-sm text-[color:var(--ink)]/50">Traveling light — nothing equipped.</p>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
{{if .MapView}}
|
||||
@@ -290,6 +294,46 @@
|
||||
<span class="text-xs text-[color:var(--ink)]/45">only you can see the panels below</span>
|
||||
</div>
|
||||
|
||||
{{/* Equipment: the 5 standard slots, owner-only management. Take off round-trips
|
||||
a masterwork/arena piece to the pack (no money); Upgrade buys the next tier
|
||||
and Repair mends condition, both spending euros behind a confirm that shows
|
||||
the cost and the resulting balance. Every price and tier here is gogobee's,
|
||||
resolved server-side — the buttons only carry what was pushed. */}}
|
||||
{{if .Self.Slots}}
|
||||
<div id="equip-panel" data-token="{{.Mark.Token}}" data-balance="{{printf "%.2f" .Self.Balance}}"
|
||||
class="rounded-3xl bg-[color:var(--card)] border-2 border-[color:var(--ink)]/10 p-6 shadow-pete mb-6">
|
||||
<div class="flex items-baseline justify-between mb-4">
|
||||
<h2 class="font-display text-xl font-bold">Equipment</h2>
|
||||
<span class="text-xs text-[color:var(--ink)]/50">Balance <span class="font-semibold text-[color:var(--ink)]/70">€{{printf "%.2f" .Self.Balance}}</span></span>
|
||||
</div>
|
||||
<ul class="grid gap-3 sm:grid-cols-2">
|
||||
{{range .Self.Slots}}
|
||||
<li class="slot-card rounded-2xl bg-[color:var(--ink)]/5 p-4">
|
||||
<div class="flex items-baseline justify-between gap-2">
|
||||
<span class="text-[10px] uppercase tracking-wider text-[color:var(--ink)]/45">{{.Slot}}</span>
|
||||
<span class="text-xs text-[color:var(--ink)]/50 shrink-0">T{{.Tier}}{{if .Condition}} · {{.Condition}}%{{end}}</span>
|
||||
</div>
|
||||
<div class="font-semibold mt-0.5 flex items-center flex-wrap gap-1.5">
|
||||
<span>{{.Name}}</span>
|
||||
{{if .Masterwork}}<span class="text-theme-adventure" title="masterwork">★</span>{{end}}
|
||||
{{if .ArenaTier}}<span class="text-[11px] rounded-full bg-theme-adventure/20 px-2 py-0.5 text-theme-adventure font-semibold">arena T{{.ArenaTier}}</span>{{end}}
|
||||
</div>
|
||||
{{if or .CanTakeOff .NextTier .RepairCost}}
|
||||
<div class="mt-2 flex flex-wrap gap-1.5">
|
||||
{{if .CanTakeOff}}<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="unequip" data-slot="{{.Slot}}" data-item-name="{{.Name}}">Take off</button>{{end}}
|
||||
{{if .NextTier}}<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="upgrade" data-slot="{{.Slot}}" data-tier="{{.NextTier}}" data-cost="{{printf "%.2f" .NextPrice}}" data-item-name="{{.NextName}}">Upgrade to {{.NextName}} · €{{printf "%.0f" .NextPrice}}</button>{{end}}
|
||||
{{if .RepairCost}}<button type="button" class="equip-btn text-[11px] rounded-full border border-[color:var(--warn)]/40 text-[color:var(--warn)] hover:bg-amber-400/10 px-2.5 py-0.5 font-semibold transition-colors"
|
||||
data-action="repair" data-slot="{{.Slot}}" data-cost="{{.RepairCost}}" data-item-name="{{.Name}}">Repair · €{{.RepairCost}}</button>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<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>
|
||||
@@ -417,12 +461,22 @@
|
||||
// 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');
|
||||
// Two owner panels can carry equip controls: the magic Worn/Backpack panel
|
||||
// (#gear-panel) and the standard-slot management panel (#equip-panel). Either
|
||||
// may be absent, so wire whichever exist and share one handler.
|
||||
var gearPanel = document.getElementById('gear-panel');
|
||||
var equipPanel = document.getElementById('equip-panel');
|
||||
var panels = [gearPanel, equipPanel].filter(Boolean);
|
||||
if (!panels.length) return; // only the owner gets these panels at all
|
||||
var token = (equipPanel || gearPanel).getAttribute('data-token');
|
||||
var balance = equipPanel ? parseFloat(equipPanel.getAttribute('data-balance') || '0') : 0;
|
||||
var box = document.getElementById('equip-orders');
|
||||
var list = document.getElementById('equip-orders-list');
|
||||
|
||||
function euroFmt(n) {
|
||||
return (Math.round(n * 100) / 100).toLocaleString('en-US', { maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -431,9 +485,14 @@
|
||||
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"
|
||||
rejected_not_equippable: "couldn't — that item can't be worn",
|
||||
rejected_downgrade: "couldn't — that wouldn't be an upgrade",
|
||||
rejected_insufficient_funds: "couldn't — not enough euros",
|
||||
rejected_max_tier: "couldn't — already at the top tier"
|
||||
};
|
||||
|
||||
var VERB = { equip: 'Equip', unequip: 'Take off', upgrade: 'Upgrade', repair: 'Repair' };
|
||||
|
||||
var pollTimer = null;
|
||||
|
||||
function render(orders) {
|
||||
@@ -445,7 +504,7 @@
|
||||
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 verb = VERB[o.action] || o.action;
|
||||
var left = document.createElement('span');
|
||||
left.className = 'flex-1';
|
||||
left.textContent = verb + ' ' + (o.item_name || o.slot || 'item');
|
||||
@@ -473,9 +532,9 @@
|
||||
.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;
|
||||
// placeOrder records the intent. equip / unequip / take-off go straight here;
|
||||
// upgrade / repair pass through askConfirm first (they spend euros).
|
||||
function placeOrder(btn) {
|
||||
btn.disabled = true;
|
||||
btn.classList.add('opacity-50');
|
||||
btn.textContent = 'queuing…';
|
||||
@@ -486,7 +545,8 @@
|
||||
token: token,
|
||||
action: btn.getAttribute('data-action'),
|
||||
item_id: parseInt(btn.getAttribute('data-item-id') || '0', 10),
|
||||
slot: btn.getAttribute('data-slot') || ''
|
||||
slot: btn.getAttribute('data-slot') || '',
|
||||
tier: parseInt(btn.getAttribute('data-tier') || '0', 10)
|
||||
})
|
||||
})
|
||||
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
||||
@@ -505,7 +565,57 @@
|
||||
btn.classList.remove('opacity-50');
|
||||
btn.textContent = 'try again';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// askConfirm is the money gate: an upgrade or repair debits euros on the game
|
||||
// box, so show the cost and the resulting balance before anything is queued.
|
||||
// Placing it here in the DOM (rather than a native dialog) keeps the page's look
|
||||
// and never blocks the browser event loop.
|
||||
function askConfirm(btn) {
|
||||
var card = btn.closest('.slot-card') || btn.parentElement;
|
||||
var existing = card.querySelector('.equip-confirm');
|
||||
if (existing) existing.remove();
|
||||
var cost = parseFloat(btn.getAttribute('data-cost') || '0');
|
||||
var action = btn.getAttribute('data-action');
|
||||
var name = btn.getAttribute('data-item-name') || 'this slot';
|
||||
var head = action === 'upgrade' ? ('Upgrade to ' + name) : ('Repair ' + name);
|
||||
|
||||
var boxEl = document.createElement('div');
|
||||
boxEl.className = 'equip-confirm mt-2 rounded-xl bg-[color:var(--ink)]/5 p-2.5 text-xs';
|
||||
var p = document.createElement('p');
|
||||
p.className = 'text-[color:var(--ink)]/70';
|
||||
p.textContent = head + ' for €' + euroFmt(cost) + '? Balance €' + euroFmt(balance) + ' → €' + euroFmt(balance - cost) + '.';
|
||||
var row = document.createElement('div');
|
||||
row.className = 'mt-2 flex gap-1.5';
|
||||
var yes = document.createElement('button');
|
||||
yes.type = 'button';
|
||||
yes.className = 'rounded-full bg-theme-adventure text-white px-2.5 py-0.5 font-semibold';
|
||||
yes.textContent = 'Confirm · €' + euroFmt(cost);
|
||||
yes.addEventListener('click', function () { boxEl.remove(); placeOrder(btn); });
|
||||
var no = document.createElement('button');
|
||||
no.type = 'button';
|
||||
no.className = 'rounded-full border border-[color:var(--ink)]/20 text-[color:var(--ink)]/60 px-2.5 py-0.5';
|
||||
no.textContent = 'Cancel';
|
||||
no.addEventListener('click', function () { boxEl.remove(); });
|
||||
row.appendChild(yes);
|
||||
row.appendChild(no);
|
||||
boxEl.appendChild(p);
|
||||
boxEl.appendChild(row);
|
||||
card.appendChild(boxEl);
|
||||
}
|
||||
|
||||
function onClick(e) {
|
||||
var btn = e.target.closest('.equip-btn');
|
||||
if (!btn || btn.disabled) return;
|
||||
var action = btn.getAttribute('data-action');
|
||||
if (action === 'upgrade' || action === 'repair') {
|
||||
askConfirm(btn); // money moves — confirm cost + balance first
|
||||
return;
|
||||
}
|
||||
placeOrder(btn);
|
||||
}
|
||||
|
||||
panels.forEach(function (p) { p.addEventListener('click', onClick); });
|
||||
|
||||
loadOrders();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user