Queue: +Q from anything, After Q / Skip Q
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
54f703341e
commit
6e48c72d95
5 changed files with 93 additions and 15 deletions
4
app.py
4
app.py
|
|
@ -364,7 +364,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
once = body.get("once") or 0
|
once = body.get("once") or 0
|
||||||
if not isinstance(once, int) or not 0 <= once <= game.MAKE_MAX:
|
if not isinstance(once, int) or not 0 <= once <= game.MAKE_MAX:
|
||||||
return f"1 to {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":
|
if path == "/api/queue":
|
||||||
qty = body.get("qty")
|
qty = body.get("qty")
|
||||||
if not isinstance(qty, int) or not 1 <= qty <= game.MAKE_MAX:
|
if not isinstance(qty, int) or not 1 <= qty <= game.MAKE_MAX:
|
||||||
|
|
@ -384,6 +384,8 @@ class Handler(BaseHTTPRequestHandler):
|
||||||
if path == "/api/queue/clear":
|
if path == "/api/queue/clear":
|
||||||
player["queue"] = []
|
player["queue"] = []
|
||||||
return None
|
return None
|
||||||
|
if path == "/api/queue/then":
|
||||||
|
return game.set_then(player, None)
|
||||||
if path == "/api/queue/skip":
|
if path == "/api/queue/skip":
|
||||||
return game.skip_make(world, player, now)
|
return game.skip_make(world, player, now)
|
||||||
if path == "/api/join":
|
if path == "/api/join":
|
||||||
|
|
|
||||||
23
game.py
23
game.py
|
|
@ -3225,6 +3225,19 @@ def queue_set(player: dict, index: int, qty: int) -> str | None:
|
||||||
return 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:
|
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."""
|
"""Drop the make that's running: on to the next in the queue, or back to what you were doing."""
|
||||||
current = player["action"]
|
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
|
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:
|
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."""
|
"""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)
|
action = ACTION_BY_ID.get(action_id)
|
||||||
if not action:
|
if not action:
|
||||||
return "No such 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
|
return reason
|
||||||
entry = {"id": action_id, "start": now}
|
entry = {"id": action_id, "start": now}
|
||||||
current = player["action"]
|
current = player["action"]
|
||||||
|
if not once and not skip and current and "left" in current:
|
||||||
|
set_then(player, action_id)
|
||||||
|
return None
|
||||||
if once:
|
if once:
|
||||||
back = current and (current.get("resume") or current["id"])
|
back = current and (current.get("resume") or current["id"])
|
||||||
entry["left"] = int(once)
|
entry["left"] = int(once)
|
||||||
|
|
@ -6244,6 +6262,7 @@ PATCH_NOTES = [
|
||||||
("fix", "+Q button: rounded corners."),
|
("fix", "+Q button: rounded corners."),
|
||||||
("fix", "Make box: number field sits in front of +Q."),
|
("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", "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."),
|
("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 costs adjusted."),
|
||||||
("balance", "Prayer potions now relevant for new costs."),
|
("balance", "Prayer potions now relevant for new costs."),
|
||||||
|
|
|
||||||
|
|
@ -806,7 +806,7 @@ function queuePanel() {
|
||||||
${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>
|
${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>` : ""}
|
<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>`}
|
${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>
|
<div class="qp-foot"><span class="muted small qp-then">After queue: ${back ? `${esc(back.name)}${now ? `<button class="x" data-queue-then title="Idle after the queue">✕</button>` : ""}` : "idle"}</span>
|
||||||
${q.length ? `<button class="btn sm" data-queue-clear>Clear</button>` : ""}</div>
|
${q.length ? `<button class="btn sm" data-queue-clear>Clear</button>` : ""}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -825,6 +825,15 @@ function queueRow() {
|
||||||
}).join(`<span class="arrow">→</span>`)}</div>`;
|
}).join(`<span class="arrow">→</span>`)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 `<button class="btn ${size} primary" data-start="${a.id}" ${after ? "data-skip" : ""} ${why ? "disabled" : ""} ${title ? `title="${title}"` : ""}>${label}</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
// The recipe you're after, under the top bar on every page: have / need for each input.
|
// The recipe you're after, under the top bar on every page: have / need for each input.
|
||||||
function trackedBar() {
|
function trackedBar() {
|
||||||
const me = S.me, a = me.tracked && C.actions.find((x) => x.id === me.tracked);
|
const me = S.me, a = me.tracked && C.actions.find((x) => x.id === me.tracked);
|
||||||
|
|
@ -834,7 +843,7 @@ function trackedBar() {
|
||||||
return `<div class="tracked ${ready ? "ready" : ""}">
|
return `<div class="tracked ${ready ? "ready" : ""}">
|
||||||
<button class="tracked-name" data-nav="skill:${a.skill}" data-nav-group="${esc(a.group || "")}" title="${esc(a.name)}">${a.icon}<span class="label">${esc(a.name)}</span></button>
|
<button class="tracked-name" data-nav="skill:${a.skill}" data-nav-group="${esc(a.group || "")}" title="${esc(a.name)}">${a.icon}<span class="label">${esc(a.name)}</span></button>
|
||||||
<span class="io">${inputChips(a)}</span>
|
<span class="io">${inputChips(a)}</span>
|
||||||
${go ? `<button class="btn sm primary" data-start="${a.id}">Start</button>` : ""}
|
${go ? startButton(a, "sm") : ""}
|
||||||
<button class="pin on" data-track="${a.id}" title="Untrack">📌</button>
|
<button class="pin on" data-track="${a.id}" title="Untrack">📌</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -1715,8 +1724,13 @@ function actionCard(a) {
|
||||||
&& me.action && C.actions.find((x) => x.id === me.action.id)?.skill === "farming" && !me.action.left;
|
&& me.action && C.actions.find((x) => x.id === me.action.id)?.skill === "farming" && !me.action.left;
|
||||||
const why = me.blocked[a.id];
|
const why = me.blocked[a.id];
|
||||||
const locked = isLocked(a);
|
const locked = isLocked(a);
|
||||||
// Queue it behind the make that's running: materials only have to be there when its turn comes.
|
// 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;
|
// 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 queued = me.queue.map((q, i) => q.id === a.id ? i + 1 : 0).filter(Boolean);
|
||||||
const out = Object.keys(a.outputs)[0];
|
const out = Object.keys(a.outputs)[0];
|
||||||
const icon = a.icon;
|
const icon = a.icon;
|
||||||
|
|
@ -1786,11 +1800,11 @@ function actionCard(a) {
|
||||||
${second ? `<button class="btn" data-plot="">Clear plot 2</button>`
|
${second ? `<button class="btn" data-plot="">Clear plot 2</button>`
|
||||||
: active ? `<button class="btn danger" data-stop>Stop</button>`
|
: active ? `<button class="btn danger" data-stop>Stop</button>`
|
||||||
: `<span class="btns">${!a.enemy && Object.keys(a.outputs).length
|
: `<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 && !canQueue ? "disabled" : ""}>${canQueue
|
? `<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 || queueOff) ? "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 q" data-q ${queueOff ? "disabled" : ""} title="${esc(queueTitle)}">+Q</button>` : ""}</form>`
|
||||||
: `<button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Once, then resume">${a.enemy ? "Fight once" : "Once"}</button>`}
|
: `<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>` : ""}
|
${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>`}
|
${startButton(a, "", why)}</span>`}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
@ -2876,7 +2890,15 @@ document.addEventListener("click", (e) => {
|
||||||
animateFight(serverNow()); // the outline moves now; the server hears about it in the background
|
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 });
|
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 (d.once) return act("/api/action", { id: d.once, once: 1 });
|
||||||
if ("stop" in d) return act("/api/stop", {});
|
if ("stop" in d) return act("/api/stop", {});
|
||||||
if (d.unqueue) return act("/api/queue/remove", { index: Number(d.unqueue) });
|
if (d.unqueue) return act("/api/queue/remove", { index: Number(d.unqueue) });
|
||||||
|
|
|
||||||
|
|
@ -868,3 +868,5 @@ button.swatch { width: 36px; height: 36px; }
|
||||||
.chip.on { border-color: var(--gold); }
|
.chip.on { border-color: var(--gold); }
|
||||||
.qp-name .why { color: var(--warn); }
|
.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-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; }
|
||||||
|
|
|
||||||
|
|
@ -145,12 +145,45 @@ class ActionTest(unittest.TestCase):
|
||||||
self.assertTrue(game.queue_add(world, p, high, 1, T0).startswith("Needs level"))
|
self.assertTrue(game.queue_add(world, p, high, 1, T0).startswith("Needs level"))
|
||||||
self.assertIsNone(game.queue_remove(p, 0))
|
self.assertIsNone(game.queue_remove(p, 0))
|
||||||
self.assertEqual(len(p["queue"]), game.QUEUE_MAX - 1)
|
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)
|
game.drop_out(world, p, T0)
|
||||||
self.assertEqual(p["queue"], [])
|
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):
|
def test_managing_the_queue(self):
|
||||||
world, p = self._smith(20)
|
world, p = self._smith(20)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue