From 9daa64801711b68c3dbf68d77730e351f8ad53da Mon Sep 17 00:00:00 2001 From: rope Date: Wed, 28 Oct 2026 14:39:21 +0000 Subject: [PATCH] Queue: tend all until full Co-Authored-By: Claude Opus 5.5 --- game.py | 20 ++++++++++++++------ static/app.js | 18 +++++++++++++----- tests/test_game.py | 17 ++++++++++++++++- 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/game.py b/game.py index cac025a..066fabd 100644 --- a/game.py +++ b/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)."), diff --git a/static/app.js b/static/app.js index 460a8f0..880b986 100644 --- a/static/app.js +++ b/static/app.js @@ -800,7 +800,8 @@ function queuePanel() { const a = recipe(e.id), why = me.blocked[e.id]; return `
  • ${i + 1}${a.icon} ${esc(a.name)}${why ? `${esc(why)}` : ""} - + ${fills(a) ? `full` + : ``} ${i || !now ? `` : ``} @@ -808,7 +809,7 @@ function queuePanel() { }).join(""); return `
    โณ Queue${q.length}
    - ${now ? `
    ${now.icon}${esc(now.name)}${fmt(me.action.left)} left + ${now ? `
    ${now.icon}${esc(now.name)}${fills(now) ? "until full" : `${fmt(me.action.left)} left`}
    ` : ""} ${rows ? `
      ${rows}
    ` : `

    Empty. +Q on a recipe or gathering adds to it.

    `} @@ -817,8 +818,12 @@ function queuePanel() {
    `; } +// 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 `
    โณ ${q.map((e, i) => { const a = C.actions.find((x) => x.id === e.id); - return `${a.icon} ร—${fmt(e.n)}`; + return `${a.icon} ${queueCount(a, e.n)}`; }).join(`โ†’`)}
    `; } @@ -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() { ${glyph("stopwatch", "#98a1b4")} ${dur(fk.minutes * 60 / speed)} ยท ${perLog(pick)} XP ${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)} ${tendingAll ? `` - : ``}
    + : ``} +
    ${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}
    Kindling
    ${C.actions.filter((a) => a.skill === "firekeeping" && a.group === "Kindling").map(actionCard).join("")}
    `; @@ -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) }); diff --git a/tests/test_game.py b/tests/test_game.py index 93d686f..9f915a5 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -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")