City needs in profession menus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-23 22:13:32 +01:00
parent 2579be2475
commit 472609ac70
2 changed files with 62 additions and 3 deletions

10
game.py
View file

@ -3691,7 +3691,12 @@ def council_requests(world: dict, event: dict) -> list[dict]:
have = sum(stock.get(food, 0) * ITEMS[food]["food"] for food in FOOD_ORDER)
label = {"copper_sword": "Swords (any)", "iron_shield": "Shields (any)", "bread": "Food (any)"}[item] \
if item in ("copper_sword", "iron_shield", "bread") else ITEMS[item]["name"]
requests.append({"item": item, "label": label, "want": qty, "have": have})
request = {"item": item, "label": label, "want": qty, "have": have}
if item in ("copper_sword", "iron_shield"):
request["any"] = ITEMS[item]["slot"] # any item in this slot counts
if item == "bread":
request["any"] = "food" # counted in meals
requests.append(request)
return requests
@ -4747,6 +4752,9 @@ ORDER_RECIPES, ORDER_RAW = _order_sources()
# What changed, shown on the Patch notes page. Newest first; every shipped change adds a
# line here (new / balance / fix), and a new release a new entry on top.
PATCH_NOTES = [
{"date": "2026-10-23", "title": "City needs", "notes": [
("new", "City needs are now indicated in profession menus."),
]},
{"date": "2026-10-22", "title": "Bosses", "notes": [
("new", "Bosses and dragons: everyone in the fight gets their own loot."),
("balance", "Level requirements for area boss reduced to be area level +5."),

View file

@ -478,6 +478,56 @@ function costChip(icon, name, need, have) {
return `<span class="item-chip ${have < need ? "short" : ""}" title="${esc(`${name}: ${have < need ? `${fmt(need - have)} more needed` : "done"}`)}">${icon} ${fmt(have)} <span class="have">/ ${fmt(need)}</span></span>`;
}
// What the city still needs of each item from building funds, fittings and happenings, after the stockpile.
// Recomputed once per state.
let needsState = null, needsCache = {};
function fundNeeds() {
if (needsState === S) return needsCache;
const city = S.city, missing = {}, why = {};
const add = (id, short, line) => { missing[id] = (missing[id] || 0) + short; (why[id] ||= []).push(line); };
for (const [key, b] of Object.entries(city.buildings)) {
const spec = C.buildings[key];
if (b.status === "funding") for (const [id, need] of Object.entries(b.target)) {
const short = b.missing[id] || 0;
if (id !== "gold" && short) add(id, short, `${spec.name} · ${fmt(need - short)} / ${fmt(need)}`);
}
for (const f of C.fittings) {
if (f.building === key && b.level >= f.level && !b.fittings.includes(f.id)) add(f.id, 1, `${spec.name} · 0 / 1`);
}
}
for (const p of S.happenings.problems) for (const [id, need] of Object.entries(p.need || {})) {
const short = p.missing[id] || 0;
if (short) add(id, short, `${p.name} · ${fmt(need - short)} / ${fmt(need)}`);
}
needsCache = {};
for (const [id, short] of Object.entries(missing)) {
const stock = city.stockpile[id] || 0;
if (short > stock) needsCache[id] = { qty: short - stock, why: stock ? [...why[id], `Stockpile ${fmt(stock)}`] : why[id] };
}
needsState = S;
return needsCache;
}
// How many of an item the city still needs, with a have / need line per reason, or null.
function cityNeed(id) {
const it = item(id), fund = fundNeeds()[id];
let qty = fund?.qty || 0;
const why = [...(fund?.why || [])];
for (const r of S.upcoming?.requests || []) {
const short = r.want - r.have;
const counts = r.any === "food" ? it.food : r.any ? it.slot === r.any : id === r.item;
if (short <= 0 || !counts) continue;
qty += r.any === "food" ? Math.ceil(short / it.food) : short;
why.push(`Council: ${r.label} · ${fmt(r.have)} / ${fmt(r.want)}`);
}
return qty ? { qty, why } : null;
}
function needTag(id) {
const n = cityNeed(id);
return n ? `<span class="tag gold" title="${esc(`City needs ${fmt(n.qty)} ${item(id).name}\n${n.why.join("\n")}`)}">🏘️ ${fmt(n.qty)}</span>` : "";
}
// --- skill pages ----------------------------------------------------------
function skillPage(id) {
@ -523,7 +573,8 @@ function skillPage(id) {
// Tier ladders open on the highest tier you can work; categories open on the first.
const ladder = ["smithing", "leatherworking", "tailoring", "herblore", "combat"].includes(id);
if (!groups.includes(current)) current = (ladder && [...groups].reverse().find(open)) || groups[0];
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}</button>`).join("");
const needed = (g) => actions.some((a) => a.group === g && !isLocked(a) && Object.keys(a.outputs).some(cityNeed));
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}${needed(g) ? ` <span title="City needs">🏘️</span>` : ""}</button>`).join("");
// Ungrouped actions (the building site) stay above the tabs.
const loose = actions.filter((a) => !a.group);
if (loose.length) html += `<div class="grid" style="margin-bottom:14px">${loose.map(actionCard).join("")}</div>`;
@ -1077,7 +1128,7 @@ function actionCard(a) {
return `
<div class="action ${active || second ? "active" : ""} ${locked ? "locked" : ""}">
<div class="top"><div class="ico">${icon}</div><div>
<div class="name">${esc(a.name)}</div>
<div class="name">${esc(a.name)} ${Object.keys(a.outputs).map(needTag).join("")}</div>
<div class="sub">Level ${a.level} · ${dur(time)} · ${a.xp} XP</div>
</div>${Object.keys(a.inputs).length ? `<button class="pin ${me.tracked === a.id ? "on" : ""}" data-track="${a.id}" title="${me.tracked === a.id ? "Untrack" : "Track"}">📌</button>` : ""}</div>
${io ? `<div class="io">${io}</div>` : ""}