Work orders: fill a reagent slot after posting; no auto-reload while writing an order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 18:46:39 +01:00
parent 326437ddf9
commit 1488692463
4 changed files with 39 additions and 3 deletions

2
app.py
View file

@ -326,6 +326,8 @@ class Handler(BaseHTTPRequestHandler):
if path == "/api/order": if path == "/api/order":
return game.post_order(world, player, body.get("kind"), str(item), qty, body.get("provide"), return game.post_order(world, player, body.get("kind"), str(item), qty, body.get("provide"),
body.get("reward"), now) body.get("reward"), now)
if path == "/api/order/provide":
return game.provide_reagent(world, player, body.get("id"), str(item))
if path == "/api/order/cancel": if path == "/api/order/cancel":
return game.cancel_order(world, player, body.get("id"), now) return game.cancel_order(world, player, body.get("id"), now)
if path == "/api/order/work": if path == "/api/order/work":

16
game.py
View file

@ -2125,6 +2125,22 @@ def post_order(world: dict, player: dict, kind: str, item: str, qty, provide, re
return None return None
def provide_reagent(world: dict, player: dict, order_id, item: str) -> str | None:
"""Put a reagent into your own order after posting it, enough for what's still to be made."""
order = order_by_id(world, order_id)
if not order or order["by"] != player["id"] or order["status"] != "open" or order["kind"] != "craft":
return "No such order"
action = ACTION_BY_ID[order["action"]]
if item not in action["inputs"] or item in order["provided"]:
return "Already provided" if item in order["provided"] else "That isn't in the recipe"
need = action["inputs"][item] * _batches(action, order["item"], order["qty"] - order["delivered"])
if player["bank"].get(item, 0) < need:
return f"{ITEMS[item]['name']}: {player['bank'].get(item, 0)} / {need}"
bank_take(player, item, need)
order["provided"][item] = need
return None
def _refund(world: dict, order: dict, now: float) -> None: def _refund(world: dict, order: dict, now: float) -> None:
owner = world["players"].get(order["by"]) owner = world["players"].get(order["by"])
if not owner: if not owner:

View file

@ -90,7 +90,7 @@ async function refresh() {
function accept(state) { function accept(state) {
version ??= state.version; version ??= state.version;
if (state.version !== version && !document.querySelector("[data-keep]:focus") && $("#modal").hidden) { if (state.version !== version && !document.querySelector("[data-keep]:focus") && $("#modal").hidden && !draft) {
location.reload(); // a new build was deployed location.reload(); // a new build was deployed
return; return;
} }
@ -1197,8 +1197,12 @@ function orderSlots(o) {
<span class="swap-qty">${fmt(per * all)}</span><span class="tick">✓</span></span></div>`; <span class="swap-qty">${fmt(per * all)}</span><span class="tick">✓</span></span></div>`;
} }
const need = per * (o.status === "open" ? left : all), have = S.me.bank[id] || 0; const need = per * (o.status === "open" ? left : all), have = S.me.bank[id] || 0;
return `<div class="ro-slot open" title="${esc(`${name}: crafter brings`)}"><span class="doll-slot empty-req">${item(id).icon}</span> // On your own open order, an empty slot can still be filled from your bank.
<span class="ro-have ${o.by === S.uid ? "muted" : have < need ? "bad" : "good"}">${o.by === S.uid ? `×${fmt(need)}` : `${fmt(have)} / ${fmt(need)}`}</span></div>`; const fill = o.by === S.uid && o.status === "open";
return `<div class="ro-slot open">${fill
? `<button class="doll-slot empty-req" data-order-fill="${o.id}" data-item="${id}" ${have < need ? "disabled" : ""} title="${esc(`${name}: ${fmt(have)} / ${fmt(need)}`)}">${item(id).icon}</button>`
: `<span class="doll-slot empty-req" title="${esc(`${name}: crafter brings`)}">${item(id).icon}</span>`}
<span class="ro-have ${o.status !== "open" ? "muted" : have < need ? "bad" : "good"}">${o.status !== "open" ? `×${fmt(need)}` : `${fmt(have)} / ${fmt(need)}`}</span></div>`;
}).join(""); }).join("");
} }
@ -1614,6 +1618,7 @@ document.addEventListener("click", (e) => {
if (d.orderDrop) return act("/api/order/drop", { id: Number(d.orderDrop) }); if (d.orderDrop) return act("/api/order/drop", { id: Number(d.orderDrop) });
if (d.orderDeliver) return act("/api/order/deliver", { id: Number(d.orderDeliver) }); if (d.orderDeliver) return act("/api/order/deliver", { id: Number(d.orderDeliver) });
if (d.orderCancel) return act("/api/order/cancel", { id: Number(d.orderCancel) }); if (d.orderCancel) return act("/api/order/cancel", { id: Number(d.orderCancel) });
if (d.orderFill) return act("/api/order/provide", { id: Number(d.orderFill), item: d.item });
if (d.doll) { if (d.doll) {
dollPick = dollPick === d.doll ? null : d.doll; dollPick = dollPick === d.doll ? null : d.doll;
return render(); return render();

View file

@ -1580,3 +1580,16 @@ class WorkOrderTest(unittest.TestCase):
game.expire_orders(self.world, T0 + 4 * game.DAY) game.expire_orders(self.world, T0 + 4 * game.DAY)
self.assertEqual(self.mike["gold"], 200) self.assertEqual(self.mike["gold"], 200)
self.assertEqual(self.mike["bank"]["iron_bar"], 3) self.assertEqual(self.mike["bank"]["iron_bar"], 3)
def test_requester_can_fill_a_slot_after_posting(self):
self.mike["gold"] = 200
self.mike["bank"] = {"iron_bar": 3, "clay_core": 3}
game.post_order(self.world, self.mike, "craft", "iron_warhammer", 1, [], 140, T0)
order = self.world["orders"][0]
self.assertIsNone(game.provide_reagent(self.world, self.mike, order["id"], "iron_bar"))
self.assertEqual(order["provided"], {"iron_bar": 3})
self.fred["bank"] = {"oak_plank": 1, "clay_core": 3}
self.assertIsNone(game.work_order(self.world, self.fred, order["id"], T0))
game.process_player(self.world, self.fred, T0 + 600, random.Random(1))
self.assertEqual(self.mike["bank"].get("iron_warhammer"), 1)
self.assertEqual(self.fred["bank"], {}) # used his own plank and cores, never needed bars