Firekeeping: tend all fires

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-27 20:11:40 +00:00
parent 6e48c72d95
commit fd85580466
3 changed files with 45 additions and 7 deletions

19
game.py
View file

@ -1318,6 +1318,10 @@ ACTIONS = [
*[_action(f"tend_{fire}_{log}", "firekeeping", f"Tend the {spec['name'].lower()}", TIER_LEVELS[tier], 30, 0, *[_action(f"tend_{fire}_{log}", "firekeeping", f"Tend the {spec['name'].lower()}", TIER_LEVELS[tier], 30, 0,
{log: 1}, special="tend", tend=fire, tier=tier, group=spec["name"]) {log: 1}, special="tend", tend=fire, tier=tier, group=spec["name"])
for fire, spec in FIRES.items() for tier, log in enumerate(FIREWOOD)], for fire, spec in FIRES.items() for tier, log in enumerate(FIREWOOD)],
# Tending all: each log goes on the lowest fire with room in your pile.
*[_action(f"tend_all_{log}", "firekeeping", "Tend all fires", TIER_LEVELS[tier], 30, 0,
{log: 1}, special="tend", tend=None, tier=tier, group="All fires")
for tier, log in enumerate(FIREWOOD)],
] ]
# --- dungeon loot -------------------------------------------------------------- # --- dungeon loot --------------------------------------------------------------
@ -2768,6 +2772,14 @@ def _room(fire: dict, player: dict, tier: int) -> float:
return -1 if short > cheaper + 1e-6 else max(0.0, short) return -1 if short > cheaper + 1e-6 else max(0.0, short)
def tend_target(world: dict, player: dict, action: dict) -> str | None:
"""The fire a tend feeds, or None if the log won't fit: its own fire, or when tending
all, the one with the least fuel left among those with room in your pile."""
fires, tier = world["city"]["fires"], action["tier"]
room = [key for key in ([action["tend"]] if action["tend"] else FIRES) if _room(fires[key], player, tier) >= 0]
return min(room, key=lambda key: sum(chunk["left"] for chunk in fires[key]["fuel"]), default=None)
def feed_fire(world: dict, player: dict, fire_id: str, tier: int) -> float: def feed_fire(world: dict, player: dict, fire_id: str, tier: int) -> float:
"""Put one log on your pile in a fire, pushing out your cheapest fuel if it's full. Returns the minutes added.""" """Put one log on your pile in a fire, pushing out your cheapest fuel if it's full. Returns the minutes added."""
fire, level = world["city"]["fires"][fire_id], skill_level(player, "firekeeping") fire, level = world["city"]["fires"][fire_id], skill_level(player, "firekeeping")
@ -2860,8 +2872,8 @@ def can_start(world: dict, player: dict, action: dict) -> str | None:
pooled = order["provided"].get(item, 0) if order else 0 pooled = order["provided"].get(item, 0) if order else 0
if pooled + player["bank"].get(item, 0) < qty: if pooled + player["bank"].get(item, 0) < qty:
return f"Out of {ITEMS[item]['name'].lower()}" return f"Out of {ITEMS[item]['name'].lower()}"
if action.get("special") == "tend" and _room(world["city"]["fires"][action["tend"]], player, action["tier"]) < 0: 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" 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": if action.get("special") == "sabotage":
raid = upcoming_raid(world) raid = upcoming_raid(world)
if not raid or intel_level(world, raid) < 3: if not raid or intel_level(world, raid) < 3:
@ -3018,7 +3030,7 @@ def _complete(world: dict, player: dict, action: dict, now: float, rng: random.R
xp = DRILL_XP * (gain + boost) * mods["xp"] xp = DRILL_XP * (gain + boost) * mods["xp"]
threat["drilled"] = min(drill_needed(threat), threat.get("drilled", 0) + gain + boost) threat["drilled"] = min(drill_needed(threat), threat.get("drilled", 0) + gain + boost)
if action.get("special") == "tend": if action.get("special") == "tend":
xp = TEND_XP[action["tier"]] * feed_fire(world, player, action["tend"], action["tier"]) * mods["xp"] xp = TEND_XP[action["tier"]] * feed_fire(world, player, tend_target(world, player, action), action["tier"]) * mods["xp"]
if warmed: if warmed:
_warm(world, player, action, xp, now) _warm(world, player, action, xp, now)
if action.get("special") == "labour": if action.get("special") == "labour":
@ -6258,6 +6270,7 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
# line here (new / balance / fix), and a new release a new entry on top. # line here (new / balance / fix), and a new release a new entry on top.
PATCH_NOTES = [ PATCH_NOTES = [
{"date": "2026-10-27", "title": "Prayer", "notes": [ {"date": "2026-10-27", "title": "Prayer", "notes": [
("new", "Firekeeping: Tend all fires. Each log goes on the lowest fire with room in your pile."),
("new", "Added resource dumps for Leatherworking: Guard Jerkins"), ("new", "Added resource dumps for Leatherworking: Guard Jerkins"),
("fix", "+Q button: rounded corners."), ("fix", "+Q button: rounded corners."),
("fix", "Make box: number field sits in front of +Q."), ("fix", "Make box: number field sits in front of +Q."),

View file

@ -1111,8 +1111,19 @@ function pickFirewood() {
return [...open].reverse().find((t) => me.bank[C.firewood[t]]) ?? 0; return [...open].reverse().find((t) => me.bank[C.firewood[t]]) ?? 0;
} }
// The fire tending all feeds next: the least fuel left among those with room in your pile.
function tendAllTarget() {
const action = S.me.action && C.actions.find((a) => a.id === S.me.action.id), fk = S.me.firekeeping;
if (!action?.id.startsWith("tend_all_")) return null;
const sum = (fuel) => fuel.reduce((total, c) => total + c.left, 0);
const room = Object.keys(C.fires).map((id) => [id, fireNow(id)])
.filter(([, n]) => sum(n.mine) + (fk.minutes - fk.bank) * 60 <= sum(n.mine.filter((c) => c.tier < action.tier)) + 1e-6);
return room.reduce((best, f) => (!best || f[1].left < best[1].left ? f : best), null)?.[0] ?? null;
}
function firekeepingPage() { function firekeepingPage() {
const me = S.me, fk = me.firekeeping, pick = pickFirewood(), speed = Object.values(S.city.fires)[0].speed; 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 perLog = (tier) => +(C.tend_xp[tier] * fk.minutes).toFixed(1); const perLog = (tier) => +(C.tend_xp[tier] * fk.minutes).toFixed(1);
const logs = C.firewood.map((log, tier) => { const logs = C.firewood.map((log, tier) => {
const level = tendLevel(tier), locked = me.levels.firekeeping < level; const level = tendLevel(tier), locked = me.levels.firekeeping < level;
@ -1121,13 +1132,15 @@ function firekeepingPage() {
}).join(""); }).join("");
return `<div class="panel fire-tools"><div class="logs">${logs}</div> return `<div class="panel fire-tools"><div class="logs">${logs}</div>
<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="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></div> <span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span>
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick)).join("")}</div>`; ${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>
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}</div>`;
} }
function fireCard(id, pick) { function fireCard(id, pick, target) {
const me = S.me, spec = C.fires[id], f = S.city.fires[id], n = fireNow(id), skill = skillOf(spec.skill); const me = S.me, spec = C.fires[id], f = S.city.fires[id], n = fireNow(id), skill = skillOf(spec.skill);
const mine = me.action?.id.startsWith(`tend_${id}_`); const mine = me.action?.id.startsWith(`tend_${id}_`) || target === id;
const start = `tend_${id}_${C.firewood[pick]}`, why = mine ? "" : me.blocked[start]; const start = `tend_${id}_${C.firewood[pick]}`, why = mine ? "" : me.blocked[start];
const crafting = f.users.filter((u) => !u.action.startsWith("tend_")); const crafting = f.users.filter((u) => !u.action.startsWith("tend_"));
const doing = (u) => C.actions.find((a) => a.id === u.action); const doing = (u) => C.actions.find((a) => a.id === u.action);

View file

@ -2785,6 +2785,18 @@ class FirekeepingTest(unittest.TestCase):
self.assertEqual(game.heat(self.world, "kitchen"), -1) self.assertEqual(game.heat(self.world, "kitchen"), -1)
self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 3 * game.TEND_XP[0] * game.fire_minutes(level)) self.assertAlmostEqual(self.kai["xp"]["firekeeping"], 3 * game.TEND_XP[0] * game.fire_minutes(level))
def test_tending_all_feeds_the_lowest_fire_with_room(self):
self.fires["forge"].update(fuel=[{"tier": 0, "left": 600, "by": "sam"}], at=T0)
self.fires["kitchen"].update(fuel=[{"tier": 0, "left": game.fire_bank(1) * 60, "by": "kai"}], at=T0) # kai's pile is full
self.fires["kiln"].update(fuel=[{"tier": 0, "left": 300, "by": "sam"}], at=T0)
self.kai["bank"] = {"logs": 1}
self.tend(self.kai, "all", "logs", T0, 35)
self.assertEqual([chunk["by"] for chunk in self.fires["still"]["fuel"]], ["kai"]) # out, so lowest
self.kai["bank"] = {"logs": 100}
for key in ("forge", "kiln", "still"):
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_better_logs_need_the_level(self): def test_better_logs_need_the_level(self):
self.kai["bank"] = {"maple_logs": 3} self.kai["bank"] = {"maple_logs": 3}
self.assertEqual(game.start_action(self.world, self.kai, "tend_forge_maple_logs", T0), "Needs level 45") self.assertEqual(game.start_action(self.world, self.kai, "tend_forge_maple_logs", T0), "Needs level 45")