Work orders: show what a lit fire pays
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2bcda564b8
commit
517b560019
4 changed files with 51 additions and 11 deletions
14
game.py
14
game.py
|
|
@ -3525,11 +3525,14 @@ def post_order(world: dict, player: dict, kind: str, item: str, qty, provide, re
|
||||||
return "Not enough gold"
|
return "Not enough gold"
|
||||||
provided = {}
|
provided = {}
|
||||||
if kind == "craft":
|
if kind == "craft":
|
||||||
runs = _batches(action, item, qty)
|
# The pool holds what a run takes right now: a lit fire's share stays with the fire.
|
||||||
|
runs, inputs = _batches(action, item, qty), fire_inputs(world, action)
|
||||||
for input_item in provide or []:
|
for input_item in provide or []:
|
||||||
if input_item not in action["inputs"]:
|
if input_item not in action["inputs"]:
|
||||||
return "That isn't in the recipe"
|
return "That isn't in the recipe"
|
||||||
need = action["inputs"][input_item] * runs
|
if input_item not in inputs:
|
||||||
|
return f"The {FIRES[action['fire']]['name'].lower()} pays that"
|
||||||
|
need = inputs[input_item] * runs
|
||||||
if player["bank"].get(input_item, 0) < need:
|
if player["bank"].get(input_item, 0) < need:
|
||||||
return f"Not enough {ITEMS[input_item]['name'].lower()}"
|
return f"Not enough {ITEMS[input_item]['name'].lower()}"
|
||||||
provided[input_item] = need
|
provided[input_item] = need
|
||||||
|
|
@ -3559,7 +3562,10 @@ def provide_reagent(world: dict, player: dict, order_id, item: str) -> str | Non
|
||||||
action = ACTION_BY_ID[order["action"]]
|
action = ACTION_BY_ID[order["action"]]
|
||||||
if item not in action["inputs"]:
|
if item not in action["inputs"]:
|
||||||
return "That isn't in the recipe"
|
return "That isn't in the recipe"
|
||||||
need = action["inputs"][item] * _batches(action, order["item"], order["qty"] - order["delivered"])
|
inputs = fire_inputs(world, action)
|
||||||
|
if item not in inputs:
|
||||||
|
return f"The {FIRES[action['fire']]['name'].lower()} pays that"
|
||||||
|
need = inputs[item] * _batches(action, order["item"], order["qty"] - order["delivered"])
|
||||||
short = need - order["provided"].get(item, 0)
|
short = need - order["provided"].get(item, 0)
|
||||||
if short <= 0:
|
if short <= 0:
|
||||||
return "Already provided"
|
return "Already provided"
|
||||||
|
|
@ -5986,6 +5992,8 @@ PATCH_NOTES = [
|
||||||
"and Aimed Shot 25% faster."),
|
"and Aimed Shot 25% faster."),
|
||||||
("fix", "Work orders on phones: the item picker and reagent slots show names, and picking an item scrolls to "
|
("fix", "Work orders on phones: the item picker and reagent slots show names, and picking an item scrolls to "
|
||||||
"the amount, price and Post."),
|
"the amount, price and Post."),
|
||||||
|
("fix", "Work orders show what a lit fire pays: its log is struck through instead of asked for, and the forge "
|
||||||
|
"pays a coal. Orders never needed that log from the crafter, they just asked for it."),
|
||||||
]},
|
]},
|
||||||
{"date": "2026-10-24", "title": "A dungeon in every area", "notes": [
|
{"date": "2026-10-24", "title": "A dungeon in every area", "notes": [
|
||||||
("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren "
|
("new", "Six more dungeons for 2–4, seven pulls each with two mini-bosses and a final boss: Goblin Warren "
|
||||||
|
|
|
||||||
|
|
@ -1829,20 +1829,31 @@ function orderNews(state) {
|
||||||
earnedSeen = Object.fromEntries(orders.map((o) => [o.id, (o.earned || {})[uid] || 0]));
|
earnedSeen = Object.fromEntries(orders.map((o) => [o.id, (o.earned || {})[uid] || 0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// What one run of a recipe takes right now: a lit fire pays its log (the forge a coal too).
|
||||||
|
const runInputs = (a) => S.me.fuelled?.[a.id] || a.inputs;
|
||||||
|
const firePays = (a, id, qty) => `the ${C.fires[a.fire].name.toLowerCase()} pays ${fmt(qty)}`;
|
||||||
|
|
||||||
|
// A reagent the fire pays outright: struck through, nothing to put in.
|
||||||
|
function paidSlot(a, id, qty) {
|
||||||
|
return `<div class="ro-slot paid" title="${esc(`${item(id).name}: ${firePays(a, id, qty)}`)}"><span class="doll-slot empty-req">${item(id).icon}</span>
|
||||||
|
<span class="ro-have muted"><s>${fmt(qty)}</s></span><span class="ro-name">${esc(item(id).name)}</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
// Recipe slots on a crafting order. Anyone can put reagents in the pool; the crafter brings the rest.
|
// Recipe slots on a crafting order. Anyone can put reagents in the pool; the crafter brings the rest.
|
||||||
function orderSlots(o) {
|
function orderSlots(o) {
|
||||||
const a = C.actions.find((x) => x.id === o.action), out = a.outputs[o.item];
|
const a = C.actions.find((x) => x.id === o.action), out = a.outputs[o.item], ins = runInputs(a);
|
||||||
const left = Math.ceil((o.qty - o.delivered) / out), all = Math.ceil(o.qty / out);
|
const left = Math.ceil((o.qty - o.delivered) / out), all = Math.ceil(o.qty / out);
|
||||||
const worth = Object.entries(a.inputs).reduce((sum, [id, per]) => sum + item(id).value * per, 0);
|
const worth = Object.entries(ins).reduce((sum, [id, per]) => sum + item(id).value * per, 0);
|
||||||
return Object.entries(a.inputs).map(([id, per]) => {
|
return Object.entries(a.inputs).map(([id, per]) => {
|
||||||
const name = item(id).name, pool = o.provided[id] || 0, have = S.me.bank[id] || 0;
|
const name = item(id).name, pool = o.provided[id] || 0, have = S.me.bank[id] || 0;
|
||||||
if (o.status !== "open") {
|
if (o.status !== "open") {
|
||||||
return `<div class="ro-slot open"><span class="doll-slot empty-req" title="${esc(name)}">${item(id).icon}</span>
|
return `<div class="ro-slot open"><span class="doll-slot empty-req" title="${esc(name)}">${item(id).icon}</span>
|
||||||
<span class="ro-have muted">×${fmt(per * all)}</span><span class="ro-name">${esc(name)}</span></div>`;
|
<span class="ro-have muted">×${fmt(per * all)}</span><span class="ro-name">${esc(name)}</span></div>`;
|
||||||
}
|
}
|
||||||
const need = per * left;
|
const need = (ins[id] || 0) * left;
|
||||||
|
if (!need) return paidSlot(a, id, per * left);
|
||||||
const who = Object.entries(o.gave || {}).filter(([, items]) => items[id]).map(([uid, items]) => `${playerName(uid)} ${fmt(items[id])}`);
|
const who = Object.entries(o.gave || {}).filter(([, items]) => items[id]).map(([uid, items]) => `${playerName(uid)} ${fmt(items[id])}`);
|
||||||
const tip = [`${name}: ${fmt(pool)} / ${fmt(need)}`, ...who].join(" · ");
|
const tip = [`${name}: ${fmt(pool)} / ${fmt(need)}`, ...(need < per * left ? [firePays(a, id, per * left - need)] : []), ...who].join(" · ");
|
||||||
if (pool >= need) {
|
if (pool >= need) {
|
||||||
return `<div class="ro-slot filled" title="${esc(tip)}"><span class="doll-slot">${item(id).icon}
|
return `<div class="ro-slot filled" title="${esc(tip)}"><span class="doll-slot">${item(id).icon}
|
||||||
<span class="swap-qty">${fmt(need)}</span><span class="tick">✓</span></span><span class="ro-name">${esc(name)}</span></div>`;
|
<span class="swap-qty">${fmt(need)}</span><span class="tick">✓</span></span><span class="ro-name">${esc(name)}</span></div>`;
|
||||||
|
|
@ -1928,9 +1939,10 @@ function orderComposer() {
|
||||||
const smiths = S.players.filter((p) => p.id !== S.uid && p.levels[a.skill] >= a.level).length;
|
const smiths = S.players.filter((p) => p.id !== S.uid && p.levels[a.skill] >= a.level).length;
|
||||||
maker = `<div class="small muted" title="${esc(`${skillOf(a.skill).name} ${a.level} · ${smiths} can make it`)}">${skillOf(a.skill).icon} ${a.level} · 👥 ${smiths}</div>`;
|
maker = `<div class="small muted" title="${esc(`${skillOf(a.skill).name} ${a.level} · ${smiths} can make it`)}">${skillOf(a.skill).icon} ${a.level} · 👥 ${smiths}</div>`;
|
||||||
if (d.kind === "craft") {
|
if (d.kind === "craft") {
|
||||||
const runs = Math.ceil(d.qty / a.outputs[d.item]);
|
const runs = Math.ceil(d.qty / a.outputs[d.item]), ins = runInputs(a);
|
||||||
slots = Object.entries(a.inputs).map(([id, per]) => {
|
slots = Object.entries(a.inputs).map(([id, per]) => {
|
||||||
const need = per * runs, have = S.me.bank[id] || 0, on = d.provide.includes(id);
|
const need = (ins[id] || 0) * runs, have = S.me.bank[id] || 0, on = d.provide.includes(id);
|
||||||
|
if (!need) return paidSlot(a, id, per * runs);
|
||||||
return `<div class="ro-slot ${on ? "filled" : "open"}"><button class="doll-slot ${on ? "on" : "empty-req"}" data-provide="${id}"
|
return `<div class="ro-slot ${on ? "filled" : "open"}"><button class="doll-slot ${on ? "on" : "empty-req"}" data-provide="${id}"
|
||||||
${!on && have < need ? `disabled title="${esc(`${item(id).name}: ${fmt(have)} / ${fmt(need)}`)}"` : `title="${esc(item(id).name)}"`}>
|
${!on && have < need ? `disabled title="${esc(`${item(id).name}: ${fmt(have)} / ${fmt(need)}`)}"` : `title="${esc(item(id).name)}"`}>
|
||||||
${item(id).icon}${on ? `<span class="tick">✓</span>` : ""}</button>
|
${item(id).icon}${on ? `<span class="tick">✓</span>` : ""}</button>
|
||||||
|
|
@ -2362,7 +2374,10 @@ document.addEventListener("click", (e) => {
|
||||||
return render();
|
return render();
|
||||||
}
|
}
|
||||||
if ("orderPost" in d) {
|
if ("orderPost" in d) {
|
||||||
const body = { kind: draft.kind, item: draft.item, qty: draft.qty, provide: draft.provide, reward: draft.reward };
|
// A fire lit since picking may now pay a reagent that was ticked.
|
||||||
|
const a = draft.kind === "craft" && C.actions.find((x) => x.id === C.order_recipes[draft.item]);
|
||||||
|
const provide = a ? draft.provide.filter((id) => runInputs(a)[id]) : draft.provide;
|
||||||
|
const body = { kind: draft.kind, item: draft.item, qty: draft.qty, provide, reward: draft.reward };
|
||||||
return api("/api/order", body).then((state) => { draft = null; orderTab = "yours"; accept(state); }).catch((err) => toast(err.message, "bad"));
|
return api("/api/order", body).then((state) => { draft = null; orderTab = "yours"; accept(state); }).catch((err) => toast(err.message, "bad"));
|
||||||
}
|
}
|
||||||
if (d.otab) { orderTab = d.otab; return render(); }
|
if (d.otab) { orderTab = d.otab; return render(); }
|
||||||
|
|
@ -2460,7 +2475,7 @@ document.addEventListener("change", (e) => {
|
||||||
if (key === "qty" && draft.kind === "craft" && draft.item) {
|
if (key === "qty" && draft.kind === "craft" && draft.item) {
|
||||||
// Drop provided reagents the bank can no longer cover at the new quantity.
|
// Drop provided reagents the bank can no longer cover at the new quantity.
|
||||||
const a = C.actions.find((x) => x.id === C.order_recipes[draft.item]), runs = Math.ceil(draft.qty / a.outputs[draft.item]);
|
const a = C.actions.find((x) => x.id === C.order_recipes[draft.item]), runs = Math.ceil(draft.qty / a.outputs[draft.item]);
|
||||||
draft.provide = draft.provide.filter((id) => (S.me.bank[id] || 0) >= a.inputs[id] * runs);
|
draft.provide = draft.provide.filter((id) => (S.me.bank[id] || 0) >= (runInputs(a)[id] || 0) * runs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
render();
|
render();
|
||||||
|
|
|
||||||
|
|
@ -658,6 +658,7 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
||||||
.ro-slot .doll-slot .gi { width: 30px; height: 30px; }
|
.ro-slot .doll-slot .gi { width: 30px; height: 30px; }
|
||||||
.ro-slot.filled .doll-slot { border-color: rgba(95, 174, 90, .7); background: rgba(95, 174, 90, .1); }
|
.ro-slot.filled .doll-slot { border-color: rgba(95, 174, 90, .7); background: rgba(95, 174, 90, .1); }
|
||||||
.doll-slot.empty-req { border-style: dashed; background: transparent; }
|
.doll-slot.empty-req { border-style: dashed; background: transparent; }
|
||||||
|
.ro-slot.paid .doll-slot { cursor: default; opacity: .55; }
|
||||||
.doll-slot.empty-req .gi { opacity: .45; }
|
.doll-slot.empty-req .gi { opacity: .45; }
|
||||||
.tick { position: absolute; bottom: -5px; right: -5px; background: var(--good); color: var(--bg); border-radius: 50%; width: 15px; height: 15px;
|
.tick { position: absolute; bottom: -5px; right: -5px; background: var(--good); color: var(--bg); border-radius: 50%; width: 15px; height: 15px;
|
||||||
font-size: 10px; display: grid; place-items: center; font-weight: 900; }
|
font-size: 10px; display: grid; place-items: center; font-weight: 900; }
|
||||||
|
|
|
||||||
|
|
@ -2606,6 +2606,22 @@ class FirekeepingTest(unittest.TestCase):
|
||||||
self.fires["still"]["fuel"] = [{"tier": 0, "left": 600, "by": "kai"}]
|
self.fires["still"]["fuel"] = [{"tier": 0, "left": 600, "by": "kai"}]
|
||||||
self.assertEqual(game.fire_inputs(self.world, brew), {"guam_leaf": 1, "logs": 1})
|
self.assertEqual(game.fire_inputs(self.world, brew), {"guam_leaf": 1, "logs": 1})
|
||||||
|
|
||||||
|
def test_a_lit_fire_pays_for_work_orders_too(self):
|
||||||
|
self.fires["forge"].update(fuel=[{"tier": 0, "left": 60000, "by": "kai"}], at=T0)
|
||||||
|
self.world["players"]["mo"] = mo = game.new_player("mo", "Mo", "commoner", T0)
|
||||||
|
mo["gold"], mo["bank"] = 100, {"copper_ore": 5, "logs": 5}
|
||||||
|
self.assertEqual(game.post_order(self.world, mo, "craft", "copper_bar", 5, ["copper_ore", "logs"], 50, T0),
|
||||||
|
"The forge pays that")
|
||||||
|
self.assertIsNone(game.post_order(self.world, mo, "craft", "copper_bar", 5, ["copper_ore"], 50, T0))
|
||||||
|
order = self.world["orders"][0]
|
||||||
|
self.assertEqual(game.provide_reagent(self.world, mo, order["id"], "logs"), "The forge pays that")
|
||||||
|
self.sam["bank"] = {}
|
||||||
|
self.assertIsNone(game.work_order(self.world, self.sam, order["id"], T0))
|
||||||
|
game.process_player(self.world, self.sam, T0 + 600, random.Random(1))
|
||||||
|
self.assertEqual(order["status"], "done")
|
||||||
|
self.assertEqual(mo["bank"], {"copper_bar": 5, "logs": 5})
|
||||||
|
self.assertEqual(self.fires["forge"]["saved"], 5)
|
||||||
|
|
||||||
def test_warmth_goes_to_whoever_fed_the_fire(self):
|
def test_warmth_goes_to_whoever_fed_the_fire(self):
|
||||||
self.fires["kitchen"]["fuel"] = [{"tier": 0, "left": 6000, "by": "kai"}]
|
self.fires["kitchen"]["fuel"] = [{"tier": 0, "left": 6000, "by": "kai"}]
|
||||||
self.fires["kitchen"]["at"] = T0
|
self.fires["kitchen"]["at"] = T0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue