Crafting cards: one-row buttons, list all missing reagents

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-28 19:09:33 +00:00
parent a205d429bd
commit 377f534f6b
3 changed files with 20 additions and 9 deletions

10
game.py
View file

@ -3053,10 +3053,10 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
if order is not None or "order" in (player["action"] or {}) and player["action"]["id"] == action["id"]:
if not order or order["status"] != "open" or order["claimed"] != player["id"] and not order.get("city"):
return "Work order done" if order and order["status"] == "done" else "Work order closed"
for item, qty in fire_inputs(world, action).items():
pooled = order["provided"].get(item, 0) if order else 0
if pooled + player["bank"].get(item, 0) < qty:
return f"Out of {ITEMS[item]['name'].lower()}"
short = [ITEMS[item]["name"].lower() for item, qty in fire_inputs(world, action).items()
if (order["provided"].get(item, 0) if order else 0) + player["bank"].get(item, 0) < qty]
if short:
return f"Out of: {', '.join(short)}"
if action.get("special") == "tend" and not tend_target(world, player, action):
return f"Your pile in the {FIRES[action['tend']]['name'].lower()} is full" if action["tend"] else "Your piles are all full"
if action.get("special") == "sabotage":
@ -6753,6 +6753,8 @@ 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": [
("fix", "Crafting cards: buttons stay on one row on phones and narrow cards."),
("new", "\"Out of:\" lists every missing reagent."),
("new", "Updated guild roster layout"),
("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."),

View file

@ -225,7 +225,16 @@ span[data-goto]:not(.item-chip):hover, b[data-goto]:hover { text-decoration: und
.bar.hp > span { background: var(--bad); }
.warn { color: var(--warn); }
select { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; margin-left: 6px; }
.action .foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: auto; }
.action .foot { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 8px; margin-top: auto; }
/* The reason gets its own line when it and the buttons don't fit side by side; the buttons stay one row. */
.action .foot .why:empty { display: none; }
.action .foot > .btns { flex-wrap: nowrap; }
.action .foot > :last-child { margin-left: auto; }
.action { container-type: inline-size; }
@container (max-width: 270px) {
.action .foot .btn { padding-inline: 9px; }
.action .foot .make input { width: 48px; padding-inline: 6px; }
}
/* --- bank ------------------------------------------------------------- */

View file

@ -244,7 +244,7 @@ class ActionTest(unittest.TestCase):
self.assertEqual(p["bank"]["copper_bar"], 3)
self.assertNotIn("copper_ore", p["bank"])
self.assertIsNone(p["action"])
self.assertEqual(summary["stopped"], "Out of copper ore")
self.assertEqual(summary["stopped"], "Out of: copper ore")
def test_crafting_stops_as_the_last_one_finishes(self):
world = world_with(("ann", "commoner"))
@ -254,7 +254,7 @@ class ActionTest(unittest.TestCase):
duration = game.action_time(world, p, game.ACTION_BY_ID["smith_copper_bar"])
summary = game.process_player(world, p, T0 + duration + 0.1, random.Random(1))
self.assertIsNone(p["action"])
self.assertEqual(summary["stopped"], "Out of copper ore")
self.assertEqual(summary["stopped"], "Out of: copper ore")
def test_level_and_city_requirements(self):
world = world_with(("ann", "commoner"))
@ -2709,7 +2709,7 @@ class WorkOrderTest(unittest.TestCase):
self.dave["bank"] = {"iron_bar": 1}
game.provide_reagent(self.world, self.dave, order["id"], "iron_bar")
self.fred["bank"] = {"iron_bar": 1, "oak_plank": 1, "clay_core": 3}
self.assertEqual(game.work_order(self.world, self.fred, order["id"], T0), "Out of iron bar")
self.assertEqual(game.work_order(self.world, self.fred, order["id"], T0), "Out of: iron bar")
self.fred["bank"]["iron_bar"] = 2
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
@ -2850,7 +2850,7 @@ class JewelcraftingTest(unittest.TestCase):
game.start_action(self.world, self.ann, "prospect_garnet", T0)
summary = game.process_player(self.world, self.ann, T0 + 1000, Always())
self.assertEqual(self.ann["bank"], {"iron_ore": 2, "garnet": 1})
self.assertEqual(summary["stopped"], "Out of iron ore")
self.assertEqual(summary["stopped"], "Out of: iron ore")
self.assertGreater(self.ann["xp"]["jewelcrafting"], game.XP_TABLE[15])
def test_jewellery_bonuses(self):