diff --git a/game.py b/game.py index f8b8fbe..5ad2019 100644 --- a/game.py +++ b/game.py @@ -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."), diff --git a/static/app.js b/static/app.js index d7c885b..919706c 100644 --- a/static/app.js +++ b/static/app.js @@ -478,6 +478,56 @@ function costChip(icon, name, need, have) { return `${icon} ${fmt(have)} / ${fmt(need)}`; } +// 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 ? `🏘️ ${fmt(n.qty)}` : ""; +} + // --- 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) => ``).join(""); + const needed = (g) => actions.some((a) => a.group === g && !isLocked(a) && Object.keys(a.outputs).some(cityNeed)); + const bar = groups.map((g) => ``).join(""); // Ungrouped actions (the building site) stay above the tabs. const loose = actions.filter((a) => !a.group); if (loose.length) html += `