Crafting queue menu

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-27 15:45:15 +00:00
parent 77fa8ad3e6
commit 4abf165857
5 changed files with 115 additions and 2 deletions

View file

@ -20,6 +20,7 @@ let seenOrders = null; // order ids already known, so only new ones toast
let deliveredSeen = {}; // how much of each order had arrived
let earnedSeen = {}; // gold each order had paid you for reagents
let pickedClass = null; // registration
let queueOpen = false; // the crafting queue panel under the top bar
const tabs = {}; // skill -> selected recipe tab
let refreshing = false;
let refreshedFor = null; // action (start and length) a completion refresh was sent for
@ -768,10 +769,13 @@ function topbar() {
let chips = "";
if (S.me?.action) {
const action = C.actions.find((a) => a.id === (S.me.action.growing || S.me.action.id));
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span>${S.me.queue.length ? `<span class="small muted" title="${esc(queueTip())}">+${S.me.queue.length}</span>` : ""}<span class="bar mini ${S.me.action.fight ? "foe" : ""}"><span data-progress></span></span></button>`;
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span><span class="bar mini ${S.me.action.fight ? "foe" : ""}"><span data-progress></span></span></button>`;
} else if (S.me) {
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
}
if (S.me?.action?.left || S.me?.queue.length) {
chips += `<button class="chip ${queueOpen ? "on" : ""}" data-queue-menu title="Crafting queue">⏳ ${S.me.queue.length}</button>`;
}
const fires = S.me ? Object.keys(C.fires).map((id) => [id, fireNow(id)]) : [];
if (fires.some(([, n]) => n.heat >= 0)) {
const tip = fires.map(([id, n]) => `${C.fires[id].name}: ${n.heat >= 0 ? `${C.flames[n.heat][0]}, ${dur(n.left / n.speed)}` : "out"}`).join("\n");
@ -781,7 +785,30 @@ function topbar() {
const w = S.happenings?.weather;
if (w) chips += `<button class="chip" data-nav="town" title="${esc(weatherTip(w))}">${w.icon}</button>`;
if (S.upcoming) chips += `<button class="chip ${["raid", "dragon"].includes(S.upcoming.kind) ? "threat" : ""} ${S.upcoming.at_gates ? "alarm" : ""}" data-nav="council">${eventIcon(S.upcoming.kind)} ${S.upcoming.at_gates ? `<span class="label">${S.upcoming.kind === "dragon" ? "Overhead" : "At the gates"}</span> ` : ""}${eventWhen(S.upcoming, true)}</button>`;
return `<button class="menu-btn" data-menu aria-label="Menu">☰</button><h1>${esc(title)}</h1>${chips}${S.me ? trackedBar() : ""}`;
return `<button class="menu-btn" data-menu aria-label="Menu">☰</button><h1>${esc(title)}</h1>${chips}${S.me ? trackedBar() : ""}${S.me && queueOpen ? queuePanel() : ""}`;
}
// The crafting queue, opened from the ⏳ chip: what's being made, what's next, and in what order.
function queuePanel() {
const me = S.me, q = me.queue, recipe = (id) => C.actions.find((x) => x.id === id);
const now = me.action?.left && recipe(me.action.id), back = me.action?.resume && recipe(me.action.resume);
const rows = q.map((e, i) => {
const a = recipe(e.id), why = me.blocked[e.id];
return `<li class="qp-row"><span class="qp-pos">${i + 1}</span>${a.icon}
<span class="qp-name">${esc(a.name)}${why ? `<span class="why small" title="Skipped if still short when its turn comes">${esc(why)}</span>` : ""}</span>
<input type="number" min="1" max="${C.make_max}" value="${e.n}" data-qset="${i}" data-keep="qset-${i}-${e.id}" title="How many">
<button class="btn sm" data-queue-move="${i}" data-to="${i - 1}" ${i ? "" : "disabled"} title="Up">▲</button>
<button class="btn sm" data-queue-move="${i}" data-to="${i + 1}" ${i < q.length - 1 ? "" : "disabled"} title="Down">▼</button>
<button class="btn sm danger" data-unqueue="${i}" title="Remove">✕</button></li>`;
}).join("");
return `<div class="queue-panel">
<div class="qp-head"><b>⏳ Crafting queue</b><span class="muted small">${q.length} / ${C.queue_max}</span><button class="x" data-queue-menu title="Close">✕</button></div>
${now ? `<div class="qp-row now">${now.icon}<span class="qp-name">${esc(now.name)}<span class="muted small">${fmt(me.action.left)} left</span></span>
<button class="btn sm" data-queue-skip title="Drop this make, on to the next">Skip</button></div>` : ""}
${rows ? `<ol class="qp-list">${rows}</ol>` : `<p class="muted small">Empty. +Q on a recipe adds to it.</p>`}
<div class="qp-foot"><span class="muted small">Then ${back ? `back to ${esc(back.name.toLowerCase())}` : "idle"}</span>
${q.length ? `<button class="btn sm" data-queue-clear>Clear</button>` : ""}</div>
</div>`;
}
function queueTip() {
@ -2806,6 +2833,7 @@ function loop() {
// --- events ---------------------------------------------------------------
document.addEventListener("click", (e) => {
if (queueOpen && !e.target.closest(".queue-panel, [data-queue-menu]")) { queueOpen = false; render(); }
const t = e.target.closest("button, [data-menu], [data-close], [data-aim]");
if (!t) return;
const d = t.dataset;
@ -2851,6 +2879,10 @@ document.addEventListener("click", (e) => {
if (d.once) return act("/api/action", { id: d.once, once: 1 });
if ("stop" in d) return act("/api/stop", {});
if (d.unqueue) return act("/api/queue/remove", { index: Number(d.unqueue) });
if ("queueMenu" in d) { queueOpen = !queueOpen; return render(); }
if (d.queueMove) return act("/api/queue/move", { index: Number(d.queueMove), to: Number(d.to) });
if ("queueSkip" in d) return act("/api/queue/skip", {});
if ("queueClear" in d) return act("/api/queue/clear", {});
if (d.join) { tagging = true; return act("/api/join", { id: d.join }); }
if ("orderNew" in d) {
draft = draft ? null : { kind: "craft", item: "", qty: 1, provide: [], reward: 0 };
@ -2963,6 +2995,11 @@ document.addEventListener("input", (e) => {
});
document.addEventListener("change", (e) => {
if (e.target.dataset.qset) {
const n = Math.floor(Number(e.target.value));
if (!(n >= 1 && n <= C.make_max)) return toast(`1 to ${fmt(C.make_max)}`, "bad");
return act("/api/queue/set", { index: Number(e.target.dataset.qset), qty: n });
}
if ("lookTitle" in e.target.dataset) return act("/api/look", { look: { ...S.me.look, title: e.target.value || null } });
const key = e.target.dataset.draft;
if (!key || !draft) return;
@ -2987,6 +3024,7 @@ $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") e.ta
// Number keys press your abilities, in the bar's order (the ones you know).
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && queueOpen) { queueOpen = false; return render(); }
if (e.target.matches("input, textarea, select") || e.ctrlKey || e.metaKey || e.altKey) return;
const n = Number(e.key);
if (!n) return;

View file

@ -852,3 +852,19 @@ button.swatch { width: 36px; height: 36px; }
.ach-line { margin: 10px 0 0; }
.queue { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 4px; }
.queue .x { background: none; border: 0; color: var(--muted); cursor: pointer; padding: 0 0 0 4px; font-size: 11px; }
#top .queue-panel { position: absolute; top: calc(100% + 6px); right: 24px; z-index: 6; width: min(400px, calc(100vw - 32px));
display: grid; gap: 8px; padding: 12px; background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
box-shadow: 0 12px 32px rgba(0, 0, 0, .45); }
.qp-head, .qp-row, .qp-foot { display: flex; align-items: center; gap: 8px; }
.qp-head b { flex: 1; }
.qp-head .x { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 14px; }
.qp-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.qp-row .gi { width: 1.4em; height: 1.4em; }
.qp-row.now { padding-bottom: 8px; border-bottom: 1px solid var(--line); }
.qp-pos { width: 1.2em; text-align: right; color: var(--muted); font-size: 12px; }
.qp-name { flex: 1; min-width: 0; display: grid; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.qp-row input { width: 56px; padding: 3px 6px; font-size: 13px; appearance: textfield; }
.qp-foot { justify-content: space-between; padding-top: 8px; border-top: 1px solid var(--line); }
.chip.on { border-color: var(--gold); }
.qp-name .why { color: var(--warn); }
.qp-row input::-webkit-inner-spin-button, .qp-row input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }