From 6e48c72d95580660d205ffa7a38b19b4c127386c Mon Sep 17 00:00:00 2001 From: rope Date: Tue, 27 Oct 2026 20:01:00 +0000 Subject: [PATCH] Queue: +Q from anything, After Q / Skip Q Co-Authored-By: Claude Opus 5.5 --- app.py | 4 +++- game.py | 23 +++++++++++++++++++++-- static/app.js | 38 ++++++++++++++++++++++++++++++-------- static/styles.css | 2 ++ tests/test_game.py | 41 +++++++++++++++++++++++++++++++++++++---- 5 files changed, 93 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index 7f00984..ce0a9ff 100644 --- a/app.py +++ b/app.py @@ -364,7 +364,7 @@ class Handler(BaseHTTPRequestHandler): once = body.get("once") or 0 if not isinstance(once, int) or not 0 <= once <= game.MAKE_MAX: return f"1 to {game.MAKE_MAX:,}" - return game.start_action(world, player, str(body.get("id")), now, once=int(once)) + return game.start_action(world, player, str(body.get("id")), now, once=int(once), skip=body.get("skip") is True) if path == "/api/queue": qty = body.get("qty") if not isinstance(qty, int) or not 1 <= qty <= game.MAKE_MAX: @@ -384,6 +384,8 @@ class Handler(BaseHTTPRequestHandler): if path == "/api/queue/clear": player["queue"] = [] return None + if path == "/api/queue/then": + return game.set_then(player, None) if path == "/api/queue/skip": return game.skip_make(world, player, now) if path == "/api/join": diff --git a/game.py b/game.py index fbfbacc..2b71f75 100644 --- a/game.py +++ b/game.py @@ -3225,6 +3225,19 @@ def queue_set(player: dict, index: int, qty: int) -> str | None: return None +def set_then(player: dict, action_id: str | None) -> str | None: + """What to go back to once the make and the queue are done: an action, or None to go idle.""" + current = player["action"] + if not current or "left" not in current: + return "Not making anything" + current.pop("resume_plot2", None) + if action_id: + current["resume"] = action_id + else: + current.pop("resume", None) + 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"] @@ -3951,8 +3964,10 @@ MAKE_MAX = 1000 # most you can ask for in one "Make X" QUEUE_MAX = 5 # makes lined up after the current one -def start_action(world: dict, player: dict, action_id: str, now: float, once: int = 0) -> str | None: - """Start an action. `once` does it that many times, then goes back to what you were doing.""" +def start_action(world: dict, player: dict, action_id: str, now: float, once: int = 0, skip: bool = False) -> str | None: + """Start an action. `once` does it that many times, then goes back to what you were doing. + While a make is running, a plain start is what comes after it and the queue, not instead, + unless `skip`: then the make and the queue go and it starts now.""" action = ACTION_BY_ID.get(action_id) if not action: return "No such action" @@ -3961,6 +3976,9 @@ def start_action(world: dict, player: dict, action_id: str, now: float, once: in return reason entry = {"id": action_id, "start": now} current = player["action"] + if not once and not skip and current and "left" in current: + set_then(player, action_id) + return None if once: back = current and (current.get("resume") or current["id"]) entry["left"] = int(once) @@ -6244,6 +6262,7 @@ PATCH_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", "Updated crafting queue to be more intuitive"), ("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 bfda724..6c0c1fe 100644 --- a/static/app.js +++ b/static/app.js @@ -806,7 +806,7 @@ function queuePanel() { ${now ? `
${now.icon}${esc(now.name)}${fmt(me.action.left)} left
` : ""} ${rows ? `
    ${rows}
` : `

Empty. +Q on a recipe adds to it.

`} -
Then ${back ? `back to ${esc(back.name.toLowerCase())}` : "idle"} +
After queue: ${back ? `${esc(back.name)}${now ? `` : ""}` : "idle"} ${q.length ? `` : ""}
`; } @@ -825,6 +825,15 @@ function queueRow() { }).join(`→`)}`; } +// Start, or while a make runs: After Q sets what comes once it and the queue are done, +// and on that one, Skip Q drops them and starts it now. +function startButton(a, size, why) { + const making = S.me.action?.left, after = making && S.me.action.resume === a.id; + const [label, title] = !making ? ["Start", ""] : after ? ["Skip Q", "Skip queue: drop the make and queue, start this now"] + : ["After Q", "After queue: do this once the make and queue are done"]; + return ``; +} + // 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); @@ -834,7 +843,7 @@ function trackedBar() { return `
${inputChips(a)} - ${go ? `` : ""} + ${go ? startButton(a, "sm") : ""}
`; } @@ -1715,8 +1724,13 @@ 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; + // Queue it behind the make that's running (materials only have to be there when its turn comes), + // or with nothing being made, start one: whatever you were doing is what comes after. + const making = Boolean(me.action?.left); + const canQueue = !active && me.queue_why[a.id] === null; + const queueOff = making ? me.queue.length >= C.queue_max : Boolean(why); + const queueTitle = making ? (queueOff ? `Queue full (${C.queue_max})` : "Queue: make this many after the current make") + : why || "Make this many, then queue more behind it"; 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; @@ -1786,11 +1800,11 @@ function actionCard(a) { ${second ? `` : active ? `` : `${!a.enemy && Object.keys(a.outputs).length - ? `
${canQueue - ? `` : ""}
` + ? `
${canQueue + ? `` : ""}
` : ``} ${canPlot ? `` : ""} -
`} + ${startButton(a, "", why)}`} `; } @@ -2876,7 +2890,15 @@ document.addEventListener("click", (e) => { animateFight(serverNow()); // the outline moves now; the server hears about it in the background return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim }); } - if (d.start) return act("/api/action", { id: d.start }); + if (d.start) { + // While a make runs, Start sets what comes after it and the queue; Skip Q starts it now. + if ("skip" in d) return act("/api/action", { id: d.start, skip: true }); + const making = S.me?.action?.left; + return act("/api/action", { id: d.start }).then(() => { + if (making && S.me.action?.resume === d.start) toast(`After queue: ${C.actions.find((x) => x.id === d.start).name}`); + }); + } + if ("queueThen" in d) return act("/api/queue/then", {}); 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) }); diff --git a/static/styles.css b/static/styles.css index d9dadc1..2cd70b9 100644 --- a/static/styles.css +++ b/static/styles.css @@ -868,3 +868,5 @@ button.swatch { width: 36px; height: 36px; } .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; } +.qp-then { display: inline-flex; align-items: center; gap: 4px; } +.qp-then .x { background: none; border: 0; color: var(--muted); cursor: pointer; font-size: 11px; padding: 0 2px; } diff --git a/tests/test_game.py b/tests/test_game.py index 7ba1b11..0b4cab0 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -145,12 +145,45 @@ class ActionTest(unittest.TestCase): self.assertTrue(game.queue_add(world, p, high, 1, T0).startswith("Needs level")) self.assertIsNone(game.queue_remove(p, 0)) self.assertEqual(len(p["queue"]), game.QUEUE_MAX - 1) - game.start_action(world, p, "mine_stone", T0) - self.assertEqual(p["queue"], []) - game.start_action(world, p, "smith_copper_helmet", T0, once=1) - game.queue_add(world, p, "smith_copper_legs", 1, T0) game.drop_out(world, p, T0) self.assertEqual(p["queue"], []) + game.start_action(world, p, "cut_logs", T0) + game.queue_add(world, p, "smith_copper_legs", 1, T0) + game.start_action(world, p, "mine_stone", T0) # no make running: starts now + self.assertEqual((p["action"]["id"], p["queue"]), ("mine_stone", [])) + + def test_start_during_a_make_sets_what_comes_after(self): + world, p = self._smith(20) + p["bank"]["logs"] = 5 + game.start_action(world, p, "cut_logs", T0) + self.assertIsNone(game.queue_add(world, p, "smith_copper_helmet", 1, T0)) # on an endless action: starts it + self.assertEqual((p["action"]["id"], p["action"]["resume"]), ("smith_copper_helmet", "cut_logs")) + game.queue_add(world, p, "smith_copper_legs", 1, T0) + self.assertIsNone(game.start_action(world, p, "mine_stone", T0)) + self.assertEqual((p["action"]["id"], p["action"]["resume"]), ("smith_copper_helmet", "mine_stone")) + self.assertEqual(len(p["queue"]), 1) # the queue stays + summary = game.process_player(world, p, T0 + 500, random.Random(1)) + self.assertEqual((p["bank"].get("copper_helmet"), p["bank"].get("copper_legs")), (1, 1)) + self.assertEqual((p["action"]["id"], summary["resumed"]), ("mine_stone", "mine_stone")) + + def test_skip_queue_starts_now(self): + world, p = self._smith(20) + game.start_action(world, p, "smith_copper_helmet", T0, once=2) + game.queue_add(world, p, "smith_copper_legs", 1, T0) + game.start_action(world, p, "cut_logs", T0) + self.assertIsNone(game.start_action(world, p, "cut_logs", T0 + 5, skip=True)) + self.assertEqual((p["action"], p["queue"]), ({"id": "cut_logs", "start": T0 + 5}, [])) + + def test_then_idle(self): + world, p = self._smith(20) + game.start_action(world, p, "smith_copper_helmet", T0, once=1) + self.assertEqual(p["action"]["resume"], "mine_stone") + self.assertIsNone(game.set_then(p, "cut_logs")) + self.assertEqual(p["action"]["resume"], "cut_logs") + self.assertIsNone(game.set_then(p, None)) + game.process_player(world, p, T0 + 500, random.Random(1)) + self.assertIsNone(p["action"]) + self.assertEqual(game.set_then(p, None), "Not making anything") def test_managing_the_queue(self): world, p = self._smith(20)