Queue: tend all until full
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e981e89e6c
commit
9daa648017
3 changed files with 43 additions and 12 deletions
20
game.py
20
game.py
|
|
@ -3329,7 +3329,8 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
|
|||
burn_fires(world, current["start"] + duration)
|
||||
reason = can_start(world, player, action)
|
||||
if reason:
|
||||
if next_queued(world, player, current["start"], summary, skipped=current) or \
|
||||
if fills(action) and "left" in current and finish_once(world, player, current["start"], reason, summary) or \
|
||||
next_queued(world, player, current["start"], summary, skipped=current) or \
|
||||
"order" in current and finish_once(world, player, current["start"], reason, summary):
|
||||
current = player["action"]
|
||||
continue
|
||||
|
|
@ -3346,7 +3347,7 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
|
|||
current["start"] += duration
|
||||
if current.get("plot2"):
|
||||
current["turn"] = 1 - current.get("turn", 0)
|
||||
if player["action"] is current and "left" in current:
|
||||
if player["action"] is current and "left" in current and not fills(action):
|
||||
current["left"] -= 1
|
||||
if current["left"] <= 0 and finish_once(world, player, current["start"], summary=summary):
|
||||
current = player["action"]
|
||||
|
|
@ -3354,7 +3355,8 @@ def process_player(world: dict, player: dict, now: float, rng: random.Random | N
|
|||
# Out of materials: stop now, not once the next bar has filled.
|
||||
reason = can_start(world, player, growing(player))
|
||||
if reason:
|
||||
if next_queued(world, player, current["start"], summary, skipped=current) or \
|
||||
if fills(action) and "left" in current and finish_once(world, player, current["start"], reason, summary) or \
|
||||
next_queued(world, player, current["start"], summary, skipped=current) or \
|
||||
"order" in current and finish_once(world, player, current["start"], reason, summary):
|
||||
current = player["action"]
|
||||
continue
|
||||
|
|
@ -3373,15 +3375,20 @@ def stop(player: dict, reason: str | None = None) -> None:
|
|||
|
||||
|
||||
def queueable(action: dict) -> bool:
|
||||
"""Crafting and gathering: makes or gathers something. Only these can be queued."""
|
||||
return bool(action["outputs"]) and not (
|
||||
"""Crafting, gathering and tending. Only these can be queued."""
|
||||
return fills(action) or bool(action["outputs"]) and not (
|
||||
action.get("enemy") or action.get("special") or action.get("together"))
|
||||
|
||||
|
||||
def fills(action: dict) -> bool:
|
||||
"""A queued tend has no count: it runs until your pile is full, then on with the queue."""
|
||||
return action.get("special") == "tend"
|
||||
|
||||
|
||||
def queue_reason(world: dict, player: dict, action: dict) -> str | None:
|
||||
"""Why it can't go in the queue. Materials are checked when its turn comes."""
|
||||
if not queueable(action):
|
||||
return "Only crafting and gathering can be queued"
|
||||
return "Only crafting, gathering and tending can be queued"
|
||||
if skill_level(player, action["skill"]) < action["level"]:
|
||||
return f"Needs level {action['level']}"
|
||||
if action.get("pattern") and action["pattern"] not in player.get("patterns", []):
|
||||
|
|
@ -6747,6 +6754,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
|
|||
# line here (new / balance / fix), and a new release a new entry on top.
|
||||
PATCH_NOTES = [
|
||||
{"date": "2026-10-28", "title": "Queue", "notes": [
|
||||
("new", "Firekeeping: +Q next to 🔥 Tend all queues tending until your piles are full, then the queue moves on."),
|
||||
("new", "City work orders: 🏘️ City when posting, or 🔨 on anything a building needs (goes straight into its fund). Anyone can craft them, several at once. Reagents earn their worth in favour, the maker the rest."),
|
||||
("new", "Work orders: no size cap (was 500 crafted / 2,000 materials)."),
|
||||
("new", "Work orders: no cap on open orders (was 5)."),
|
||||
|
|
|
|||
|
|
@ -800,7 +800,8 @@ function queuePanel() {
|
|||
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">
|
||||
${fills(a) ? `<span class="muted small" title="${esc(`${item(Object.keys(a.inputs)[0]).name} until your piles are full`)}">full</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">`}
|
||||
${i || !now ? `<button class="btn sm" data-queue-move="${i}" data-to="${i - 1}" ${i ? "" : "disabled"} title="Up">▲</button>`
|
||||
: `<button class="btn sm" data-queue-now="0" title="Swap with current">▲</button>`}
|
||||
<button class="btn sm" data-queue-move="${i}" data-to="${i + 1}" ${i < q.length - 1 ? "" : "disabled"} title="Down">▼</button>
|
||||
|
|
@ -808,7 +809,7 @@ function queuePanel() {
|
|||
}).join("");
|
||||
return `<div class="queue-panel">
|
||||
<div class="qp-head"><b>⏳ Queue</b><span class="muted small">${q.length}</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>
|
||||
${now ? `<div class="qp-row now">${now.icon}<span class="qp-name">${esc(now.name)}<span class="muted small">${fills(now) ? "until full" : `${fmt(me.action.left)} left`}</span></span>
|
||||
<button class="btn sm" data-queue-now="0" ${q.length ? "" : "disabled"} title="Swap with next">▼</button>
|
||||
<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 or gathering adds to it.</p>`}
|
||||
|
|
@ -817,8 +818,12 @@ function queuePanel() {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// A queued tend has no count: it runs until your piles are full.
|
||||
const fills = (a) => a.special === "tend";
|
||||
const queueCount = (a, n) => fills(a) ? "full" : `×${fmt(n)}`;
|
||||
|
||||
function queueTip() {
|
||||
return `Queued: ${S.me.queue.map((q) => `${C.actions.find((x) => x.id === q.id).name} ×${fmt(q.n)}`).join(" → ")}`;
|
||||
return `Queued: ${S.me.queue.map((q) => { const a = C.actions.find((x) => x.id === q.id); return `${a.name} ${queueCount(a, q.n)}`; }).join(" → ")}`;
|
||||
}
|
||||
|
||||
// Makes lined up after this one: tap ✕ to drop one.
|
||||
|
|
@ -827,7 +832,7 @@ function queueRow() {
|
|||
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>`;
|
||||
return `<span class="item-chip" title="${esc(a.name)}">${a.icon} ${queueCount(a, e.n)}<button class="x" data-unqueue="${i}" title="Remove">✕</button></span>`;
|
||||
}).join(`<span class="arrow">→</span>`)}</div>`;
|
||||
}
|
||||
|
||||
|
|
@ -1165,6 +1170,7 @@ function tendAllTarget() {
|
|||
function firekeepingPage() {
|
||||
const me = S.me, fk = me.firekeeping, pick = pickFirewood(), speed = Object.values(S.city.fires)[0].speed;
|
||||
const all = `tend_all_${C.firewood[pick]}`, tendingAll = me.action?.id.startsWith("tend_all_"), allWhy = me.blocked[all];
|
||||
const making = Boolean(me.action?.left), qWhy = me.queue_why[all] || (!making && allWhy);
|
||||
const perLog = (tier) => +(C.tend_xp[tier] * fk.minutes).toFixed(1);
|
||||
const logs = C.firewood.map((log, tier) => {
|
||||
const level = tendLevel(tier), locked = me.levels.firekeeping < level;
|
||||
|
|
@ -1175,7 +1181,8 @@ function firekeepingPage() {
|
|||
<span class="tag" title="Fire and XP per log">${glyph("stopwatch", "#98a1b4")} ${dur(fk.minutes * 60 / speed)} · ${perLog(pick)} XP</span>
|
||||
<span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span>
|
||||
${tendingAll ? `<button class="btn danger" data-stop>Stop</button>`
|
||||
: `<button class="btn primary" data-start="${all}" ${allWhy ? "disabled" : ""} title="${esc(allWhy || "Each log goes on the lowest fire with room in your pile")}">🔥 Tend all</button>`}</div>
|
||||
: `<button class="btn primary" data-start="${all}" ${allWhy ? "disabled" : ""} title="${esc(allWhy || "Each log goes on the lowest fire with room in your pile")}">🔥 Tend all</button>`}
|
||||
<button class="btn q" data-queue-add="${all}" ${qWhy ? "disabled" : ""} title="${esc(qWhy || (making ? "Queue: tend all until full after the current make" : "Tend all until full, then back to what you were doing"))}">+Q</button></div>
|
||||
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}</div>
|
||||
<div class="section-title">Kindling</div>
|
||||
<div class="grid">${C.actions.filter((a) => a.skill === "firekeeping" && a.group === "Kindling").map(actionCard).join("")}</div>`;
|
||||
|
|
@ -3079,6 +3086,7 @@ 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 (d.queueAdd) return act("/api/queue", { id: d.queueAdd, qty: 1 });
|
||||
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 (d.queueNow) return act("/api/queue/now", { index: Number(d.queueNow) });
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ class ActionTest(unittest.TestCase):
|
|||
|
||||
def test_queue_rules(self):
|
||||
world, p = self._smith(4)
|
||||
self.assertEqual(game.queue_add(world, p, "bury_bones", 1, T0), "Only crafting and gathering can be queued")
|
||||
self.assertEqual(game.queue_add(world, p, "bury_bones", 1, T0), "Only crafting, gathering and tending can be queued")
|
||||
self.assertIsNone(game.queue_add(world, p, "smith_copper_helmet", 1, T0))
|
||||
self.assertEqual(p["action"]["id"], "smith_copper_helmet") # nothing being made: it just starts
|
||||
self.assertEqual(p["action"]["resume"], "mine_stone")
|
||||
|
|
@ -3012,6 +3012,21 @@ class FirekeepingTest(unittest.TestCase):
|
|||
self.fires[key].update(fuel=[{"tier": 0, "left": game.fire_bank(1) * 60, "by": "kai"}], at=T0 + 35)
|
||||
self.assertEqual(game.start_action(self.world, self.kai, "tend_all_logs", T0 + 35), "Your piles are all full")
|
||||
|
||||
def test_queued_tending_runs_until_full_then_on_with_the_queue(self):
|
||||
self.kai["bank"] = {"logs": 500}
|
||||
game.start_action(self.world, self.kai, "mine_stone", T0)
|
||||
self.assertIsNone(game.queue_add(self.world, self.kai, "tend_all_logs", 1, T0)) # starts it, mining comes after
|
||||
self.assertIsNone(game.queue_add(self.world, self.kai, "split_logs", 2, T0))
|
||||
self.assertEqual(self.kai["action"]["left"], 1)
|
||||
game.process_player(self.world, self.kai, T0 + 600, random.Random(1))
|
||||
self.assertEqual(self.kai["action"]["id"], "tend_all_logs") # past one log: no count, still tending
|
||||
summary = game.process_player(self.world, self.kai, T0 + 4 * 3600, random.Random(1))
|
||||
self.assertEqual(summary["skipped"], [])
|
||||
self.assertEqual(self.kai["bank"]["kindling"], 2 * game.KINDLING_YIELD[0])
|
||||
self.assertEqual((self.kai["action"]["id"], summary["resumed"]), ("mine_stone", "mine_stone"))
|
||||
tended = 500 - 2 - self.kai["bank"]["logs"]
|
||||
self.assertGreater(tended, 4 * game.fire_bank(1) / game.fire_minutes(1)) # every pile filled
|
||||
|
||||
def test_better_logs_need_the_level(self):
|
||||
self.kai["bank"] = {"maple_logs": 3}
|
||||
self.assertEqual(game.start_action(self.world, self.kai, "tend_forge_maple_logs", T0), "Needs level 45")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue