diff --git a/app.py b/app.py
index 4b90e66..7f00984 100644
--- a/app.py
+++ b/app.py
@@ -373,6 +373,19 @@ class Handler(BaseHTTPRequestHandler):
if path == "/api/queue/remove":
index = body.get("index")
return game.queue_remove(player, index) if isinstance(index, int) else "Not in the queue"
+ if path == "/api/queue/move":
+ index, to = body.get("index"), body.get("to")
+ return game.queue_move(player, index, to) if isinstance(index, int) and isinstance(to, int) else "Not in the queue"
+ if path == "/api/queue/set":
+ index, qty = body.get("index"), body.get("qty")
+ if not isinstance(index, int) or not isinstance(qty, int):
+ return "Bad quantity"
+ return game.queue_set(player, index, qty)
+ if path == "/api/queue/clear":
+ player["queue"] = []
+ return None
+ if path == "/api/queue/skip":
+ return game.skip_make(world, player, now)
if path == "/api/join":
return game.join_fight(world, player, str(body.get("id")), now)
if path == "/api/press":
diff --git a/game.py b/game.py
index f80a63e..28bade8 100644
--- a/game.py
+++ b/game.py
@@ -3195,6 +3195,32 @@ def queue_remove(player: dict, index: int) -> str | None:
return None
+def queue_move(player: dict, index: int, to: int) -> str | None:
+ queue = player["queue"]
+ if not (0 <= index < len(queue) and 0 <= to < len(queue)):
+ return "Not in the queue"
+ queue.insert(to, queue.pop(index))
+ return None
+
+
+def queue_set(player: dict, index: int, qty: int) -> str | None:
+ if not 0 <= index < len(player["queue"]):
+ return "Not in the queue"
+ if not 1 <= qty <= MAKE_MAX:
+ return f"1 to {MAKE_MAX:,}"
+ player["queue"][index]["n"] = qty
+ return None
+
+
+def skip_make(world: dict, player: dict, now: float) -> str | None:
+ """Drop the make that's running: on to the next in the queue, or back to what you were doing."""
+ current = player["action"]
+ if not current or "left" not in current or "enemy" in ACTION_BY_ID[current["id"]]:
+ return "Not making anything"
+ finish_once(world, player, now)
+ return None
+
+
def next_queued(world: dict, player: dict, at: float, summary: dict | None, skipped: dict | None = None) -> bool:
"""Start the next make in the queue (True), skipping any you can't make right now.
`skipped` is a make that ran out of materials before its count was done."""
@@ -6196,6 +6222,7 @@ PATCH_NOTES = [
{"date": "2026-10-27", "title": "Prayer", "notes": [
("fix", "+Q button: rounded corners."),
("fix", "Make box: number field sits in front of +Q."),
+ ("new", "Crafting queue menu: tap ⏳ in the top bar to reorder, change counts, skip or clear."),
("new", "Bury on the spot: bones from combat turns into prayer points and 1/2 bury XP as they drop. Toggle in the Prayer menu."),
("balance", "Prayer costs adjusted."),
("balance", "Prayer potions now relevant for new costs."),
diff --git a/static/app.js b/static/app.js
index b43e1b7..f39a581 100644
--- a/static/app.js
+++ b/static/app.js
@@ -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 += ``;
+ chips += ``;
} else if (S.me) {
chips += `Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}`;
}
+ if (S.me?.action?.left || S.me?.queue.length) {
+ chips += ``;
+ }
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 += ``;
if (S.upcoming) chips += ``;
- return `
${esc(title)}
${chips}${S.me ? trackedBar() : ""}`;
+ return `
${esc(title)}
${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 `