Crafting queue

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-26 22:51:56 +00:00
parent 912f94f775
commit 40e5274c4e
5 changed files with 207 additions and 17 deletions

View file

@ -189,6 +189,7 @@ function showSummary(sum) {
if (sold.length) toast(`📦 Bank full: sold ${sold.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} for ${fmt(sum.sold_gold)} gold`);
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
if (sum.skipped?.length) toast(`⏭️ Skipped: ${skippedNames(sum)}`, "bad");
for (const id of sum.potion_out || []) toast(`🧪 Your ${item(id).name.toLowerCase()} ran out`, "bad");
for (const id of Object.keys(sum.potions_drunk || {})) toast(`🧪 Drank another ${item(id).name.toLowerCase()}`);
if (sum.prayer_out) toast("🙏 Out of prayer points", "bad");
@ -196,6 +197,11 @@ function showSummary(sum) {
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
}
// Queued makes passed over for want of materials.
function skippedNames(sum) {
return sum.skipped.map((id) => C.actions.find((x) => x.id === id).name).join(", ");
}
function awayModal(sum) {
const items = Object.entries(sum.items).map(([id, q]) => `<li>${item(id).icon} ${fmt(q)} ${esc(item(id).name)}</li>`).join("");
const xp = Object.entries(sum.xp).map(([id, q]) => `<li>${skillOf(id).icon} ${fmt(q)} ${esc(skillOf(id).name)} XP</li>`).join("");
@ -214,6 +220,7 @@ function awayModal(sum) {
${levels ? `<ul>${levels}</ul>` : ""}
${sold ? `<p class="muted small">Bank full, sold for ${fmt(sum.sold_gold)} gold:</p><ul>${sold}</ul>` : ""}
${spilled ? `<p class="muted small">Bank full, sent to the stockpile:</p><ul>${spilled}</ul>` : ""}
${sum.skipped?.length ? `<p class="tag bad">⏭️ Skipped: ${esc(skippedNames(sum))}</p>` : ""}
${sum.stopped ? `<p class="tag bad">Stopped: ${esc(sum.stopped)}</p>` : ""}
<p><button class="btn primary" data-close>Back to work</button></p>`);
}
@ -761,7 +768,7 @@ 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><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>${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>`;
} else if (S.me) {
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
}
@ -777,6 +784,20 @@ function topbar() {
return `<button class="menu-btn" data-menu aria-label="Menu">☰</button><h1>${esc(title)}</h1>${chips}${S.me ? trackedBar() : ""}`;
}
function queueTip() {
return `Queued: ${S.me.queue.map((q) => `${C.actions.find((x) => x.id === q.id).name} ×${fmt(q.n)}`).join(" → ")}`;
}
// Makes lined up after this one: tap ✕ to drop one.
function queueRow() {
const q = S.me.queue;
if (!q.length) return "";
return `<div class="queue small">⏳ ${q.map((e, i) => {
const a = C.actions.find((x) => x.id === e.id);
return `<span class="item-chip" title="${esc(a.name)}">${a.icon} ×${fmt(e.n)}<button class="x" data-unqueue="${i}" title="Remove">✕</button></span>`;
}).join(`<span class="arrow">→</span>`)}</div>`;
}
// The recipe you're after, under the top bar on every page: have / need for each input.
function trackedBar() {
const me = S.me, a = me.tracked && C.actions.find((x) => x.id === me.tracked);
@ -1656,6 +1677,9 @@ function actionCard(a) {
&& me.action && C.actions.find((x) => x.id === me.action.id)?.skill === "farming" && !me.action.left;
const why = me.blocked[a.id];
const locked = isLocked(a);
// Queue it behind the make that's running: materials only have to be there when its turn comes.
const canQueue = me.action?.left && !active && me.queue_why[a.id] === null;
const queued = me.queue.map((q, i) => q.id === a.id ? i + 1 : 0).filter(Boolean);
const out = Object.keys(a.outputs)[0];
const icon = a.icon;
const time = me.times[a.id];
@ -1717,13 +1741,15 @@ function actionCard(a) {
${me.action?.plot2 && (active || second) ? `<div class="small muted">Plot ${active ? 1 : 2}${growingNow ? " · growing now" : " · next"}</div>` : ""}
${growingNow && !a.enemy ? `<div class="bar thick"><span data-progress></span></div>` : ""}
${active && a.enemy ? `<div><span class="tag gold">⚔️ Fighting${me.action.with?.length ? ` with ${me.action.with.map((uid) => esc(playerName(uid))).join(", ")}` : ""}</span></div>` : ""}
${active && me.action.left ? `<div class="small muted">${me.action.left === 1 ? "Just one" : `${fmt(me.action.left)} more`}${me.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</div>` : ""}
${active && me.action.left ? `<div class="small muted">${me.action.left === 1 ? "Just one" : `${fmt(me.action.left)} more`}${me.queue.length ? `, then ${me.queue.length} queued` : ""}${me.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</div>${queueRow()}` : ""}
${queued.length ? `<div><span class="tag" title="${esc(queueTip())}">⏳ Queued #${queued.join(", #")}</span></div>` : ""}
<div class="foot">
<span class="why">${!active && !second && why ? esc(why) : ""}</span>
${second ? `<button class="btn" data-plot="">Clear plot 2</button>`
: active ? `<button class="btn danger" data-stop>Stop</button>`
: `<span class="btns">${!a.enemy && Object.keys(a.outputs).length
? `<form data-form="make" data-id="${a.id}" class="make" title="Make this many, then resume"><button class="btn" ${why ? "disabled" : ""}>Make</button><input type="number" min="1" max="${C.make_max}" value="1" data-keep="make-${a.id}" ${why ? "disabled" : ""}></form>`
? `<form data-form="make" data-id="${a.id}" class="make" title="Make this many, then resume"><button class="btn" ${why ? "disabled" : ""}>Make</button><input type="number" min="1" max="${C.make_max}" value="1" data-keep="make-${a.id}" ${why && !canQueue ? "disabled" : ""}>${canQueue
? `<button class="btn q" data-q ${me.queue.length >= C.queue_max ? `disabled title="Queue full (${C.queue_max})"` : `title="Queue: make this many after the current make"`}>+Q</button>` : ""}</form>`
: `<button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Once, then resume">${a.enemy ? "Fight once" : "Once"}</button>`}
${canPlot ? `<button class="btn" data-plot="${a.id}" ${why ? "disabled" : ""} title="Grow in plot 2">Plot 2</button>` : ""}
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
@ -2814,6 +2840,7 @@ document.addEventListener("click", (e) => {
if (d.start) return act("/api/action", { id: d.start });
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 (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 };
@ -2979,6 +3006,7 @@ document.addEventListener("submit", async (e) => {
const n = Math.floor(Number(value));
if (!(n >= 1 && n <= C.make_max)) return toast(`1 to ${fmt(C.make_max)}`, "bad");
field.value = "1";
if (e.submitter?.hasAttribute("data-q")) return act("/api/queue", { id: form.dataset.id, qty: n });
return act("/api/action", { id: form.dataset.id, once: n });
}
if (form.dataset.form === "qty") {

View file

@ -711,6 +711,8 @@ button.sheet-skill:hover { background: var(--panel-2); }
.make .btn { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.make input { width: 60px; padding-inline: 8px; appearance: textfield; border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px; }
.make.sm input { width: 70px; padding-block: 3px; font-size: 13px; }
.make .btn.q { border-top-left-radius: 0; border-bottom-left-radius: 0; margin-left: -1px; }
.make:has(.q) input { border-top-right-radius: 0; border-bottom-right-radius: 0; }
.make input::-webkit-inner-spin-button, .make input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.reward-input { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.reward-input input { width: 110px; }
@ -848,3 +850,5 @@ button.swatch { width: 36px; height: 36px; }
.ach.held { border: 2px dashed var(--gold); }
.ach.on { border-color: var(--text); }
.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; }