Achievements page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-25 20:01:33 +00:00
parent f6951b0f12
commit a05530b802
5 changed files with 152 additions and 4 deletions

View file

@ -13,6 +13,7 @@ let skew = 0; // server clock minus client clock, seconds
let page = decodeURIComponent(location.hash.slice(1)) || remember("page") || "town";
let selected = null; // bank item
let dollPick = null; // equipment slot shown on the character sheet
let achPick = null; // achievement chip spelled out: "section.card.entry"
let orderTab = "take"; // work orders: for you, all open, yours
let draft = null; // a work order being written
let seenOrders = null; // order ids already known, so only new ones toast
@ -374,7 +375,8 @@ function sidebar() {
${nav("notes", "📰", "Patch notes", "", notesUnread() ? `<span class="badge">new</span>` : "", notesUnread() ? `What's new: ${C.patch_notes[0].title}` : "")}
<div class="navhead">You</div>
${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`, "", `Combat level ${me.levels.combat}`)}
${nav("bank", "🏦", "Bank", `${me.slots_used}/${me.slots}`, "", `${me.slots_used} of ${me.slots} bank slots used`)}`;
${nav("bank", "🏦", "Bank", `${me.slots_used}/${me.slots}`, "", `${me.slots_used} of ${me.slots} bank slots used`)}
${achievementsNav(me)}`;
let group = null;
for (const skill of C.skills) {
if (skill.class && skill.class !== me.cls) continue;
@ -462,7 +464,7 @@ function pageHtml() {
}
if (page.startsWith("character:")) return characterSheet(page.slice(10));
if (page === "character") return characterSheet(S.uid);
return ({ town, council, orders, board, roster, notes, bank }[page] || town)();
return ({ town, council, orders, board, roster, notes, bank, achievements }[page] || town)();
}
// Recipe input as "have / need"; without `have`, just the quantity.
@ -2328,6 +2330,40 @@ function loop() {
requestAnimationFrame(loop);
}
// --- achievements ----------------------------------------------------------
// A record of what's left to unlock, read off what you already have. Tap a chip for what it is.
const tally = (entries) => [entries.filter((e) => e.done).length, entries.length];
function achievementsNav(me) {
const [have, need] = tally(me.achievements.flatMap((s) => s.cards.flatMap((c) => c.entries)));
return nav("achievements", "🏆", "Achievements", `${have}/${need}`, "", `${have} of ${need} unlocked`);
}
function achievements() {
const sections = S.me.achievements, all = sections.flatMap((s) => s.cards.flatMap((c) => c.entries));
const [have, need] = tally(all);
return `<div class="panel"><h2>Achievements <span class="ach-count">${have} / ${need}</span></h2>
<div class="bar gold thick"><span style="width:${have / need * 100}%"></span></div></div>`
+ sections.map((s, si) => {
const [sh, sn] = tally(s.cards.flatMap((c) => c.entries));
return `<h3 class="section-title">${esc(s.name)} <span class="ach-count">${sh} / ${sn}</span></h3>
<div class="grid">${s.cards.map((c, ci) => achCard(c, `${si}.${ci}`)).join("")}</div>`;
}).join("");
}
function achCard(card, key) {
const [have, need] = tally(card.entries);
const pick = achPick?.startsWith(key + ".") ? card.entries[Number(achPick.split(".")[2])] : null;
const icon = (e) => e.colour ? `<span class="swatch" style="background:${e.colour}"></span>` : glyph(e.glyph, e.tint);
const chips = card.entries.map((e, i) => `<button class="ach ${e.done ? "done" : ""} ${e.held ? "held" : ""} ${pick === e ? "on" : ""}" data-ach="${key}.${i}"
title="${esc(`${e.name}${e.tip ? ` · ${e.tip}` : ""}`)}" aria-label="${esc(e.name)}">${icon(e)}</button>`).join("");
return `<div class="panel ach-card ${have === need ? "full" : ""}"><h2>${esc(card.name)} <span class="ach-count">${have} / ${need}</span></h2>
<div class="bar gold"><span style="width:${have / need * 100}%"></span></div>
<div class="ach-chips">${chips}</div>
${pick ? `<p class="small ach-line">${pick.done ? "✓" : "🔒"} <b>${esc(pick.name)}</b>${pick.tip ? ` <span class="muted">· ${esc(pick.tip)}</span>` : ""}</p>` : ""}</div>`;
}
// --- events ---------------------------------------------------------------
document.addEventListener("click", (e) => {
@ -2626,3 +2662,7 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) refr
}, 500);
requestAnimationFrame(loop);
})();
if (d.ach) {
achPick = achPick === d.ach ? null : d.ach;
return render();
}

View file

@ -799,3 +799,17 @@ button.swatch { width: 36px; height: 36px; }
.swatch.on { border-color: var(--text); }
.look-preview { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.favour-option .bar { margin-top: 4px; }
/* --- achievements ----------------------------------------------------- */
.ach-count { color: var(--gold); font-variant-numeric: tabular-nums; margin-left: 6px; }
.ach-card.full { border-color: var(--gold); }
.ach-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
.ach { width: 40px; height: 40px; display: grid; place-items: center; padding: 0; border-radius: 8px;
background: var(--panel-2); border: 2px solid transparent; cursor: pointer; }
.ach .gi { width: 26px; height: 26px; }
.ach .swatch { width: 22px; height: 22px; border-radius: 4px; }
.ach:not(.done) > * { opacity: .25; filter: grayscale(1); }
.ach.done { border-color: color-mix(in srgb, var(--gold) 45%, transparent); }
.ach.held { border: 2px dashed var(--gold); }
.ach.on { border-color: var(--text); }
.ach-line { margin: 10px 0 0; }