Townsfolk mood: needs, kindling, clothes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
002e990dc4
commit
58a3e66454
6 changed files with 385 additions and 30 deletions
|
|
@ -934,12 +934,16 @@ 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 || it.kit) === r.any : id === r.item;
|
||||
if (short <= 0 || !counts) continue;
|
||||
qty += r.any === "food" ? Math.ceil(short / it.food) : short;
|
||||
why.push(`Raid readiness: ${r.label} · ${fmt(r.have)} / ${fmt(r.want)}`);
|
||||
const townsfolk = Object.values(S.city.happiness.needs).flatMap((n) => n.rows).filter((r) => !r.kinds);
|
||||
for (const [source, rows] of [["Raid readiness", S.upcoming?.requests || []], ["Townsfolk", townsfolk]]) {
|
||||
for (const r of rows) {
|
||||
const short = r.want - r.have;
|
||||
const counts = r.any === "food" ? it.food : r.any === "fuel" ? id === "kindling" || C.firewood.includes(id)
|
||||
: r.any ? (it.slot || it.kit) === r.any : id === r.item;
|
||||
if (short <= 0 || !counts) continue;
|
||||
qty += r.any === "food" ? Math.ceil(short / it.food) : short;
|
||||
why.push(`${source}: ${r.label} · ${fmt(r.have)} / ${fmt(r.want)}`);
|
||||
}
|
||||
}
|
||||
return qty ? { qty, why } : null;
|
||||
}
|
||||
|
|
@ -1140,7 +1144,9 @@ function firekeepingPage() {
|
|||
<span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span>
|
||||
${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>`;
|
||||
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick, tendAllTarget())).join("")}</div>
|
||||
<div class="section-title">Kindling</div>
|
||||
<div class="grid">${C.actions.filter((a) => a.skill === "firekeeping" && a.group === "Kindling").map(actionCard).join("")}</div>`;
|
||||
}
|
||||
|
||||
function fireCard(id, pick, target) {
|
||||
|
|
@ -1972,18 +1978,22 @@ function town() {
|
|||
const city = S.city;
|
||||
const growth = city.starving ? `<span class="bad">Hungry: ${city.growth} a day</span>`
|
||||
: city.growth > 0 ? `+${city.growth} a day`
|
||||
: city.growth < 0 && city.leaving ? `<span class="bad">Leaving: ${city.growth} a day</span>`
|
||||
: city.growth < 0 ? `<span class="bad">Overcrowded: ${city.growth} a day</span>`
|
||||
: city.halted ? `<span class="warn">Halted: mood</span>`
|
||||
: `<span class="warn">Full: build houses</span>`;
|
||||
const next = city.next_tier ? `${city.next_tier.name} at ${city.next_tier.pop} people` : "Largest tier";
|
||||
let html = `<div class="stats">
|
||||
<div class="stat"><div class="k">Settlement</div><div class="v">${city.tier_name}</div><div class="note">${next}</div></div>
|
||||
<div class="stat"><div class="k">Population</div><div class="v">${city.population} <small>/ ${city.housing} homes</small></div><div class="note">${growth}</div></div>
|
||||
<div class="stat"><div class="k">Food</div><div class="v ${city.food_days < 1 ? "bad" : ""}">${city.food_days ?? 0} <small>days</small></div><div class="note">${fmt(city.food)} meals in the granary</div></div>
|
||||
${moodStat()}
|
||||
<div class="stat"><div class="k">Treasury</div><div class="v gold">${fmt(city.treasury)}</div><div class="note">Day ${city.day}</div></div>
|
||||
<div class="stat"><div class="k">Defence</div><div class="v">${fmt(city.defence.total)}</div><div class="note">${city.repelled} raid${city.repelled === 1 ? "" : "s"} repelled</div></div>
|
||||
${weatherCard()}
|
||||
</div>`;
|
||||
html += happeningsPanel();
|
||||
html += moodPanel();
|
||||
html += caravanPanel();
|
||||
if (city.starving) html += `<div class="banner bad">🍞<div class="grow"><b>Granary empty.</b> People are leaving, defence is down. Donate food.</div></div>`;
|
||||
if (city.merchant_until > serverNow()) html += `<div class="banner">🐪<div class="grow"><b>Merchant in town.</b> Sell prices +50% for ${countdown(city.merchant_until)}.</div></div>`;
|
||||
|
|
@ -1999,6 +2009,35 @@ function town() {
|
|||
return html;
|
||||
}
|
||||
|
||||
// --- townsfolk ---------------------------------------------------------------
|
||||
const NEED_NAMES = { food: ["🍲", "Food"], warmth: ["🔥", "Firewood"], clothes: ["👕", "Clothes"] };
|
||||
const NEED_TIERS = { warmth: "Village", clothes: "Town" };
|
||||
const pct = (x) => `${Math.round(x * 100)}%`;
|
||||
|
||||
function moodStat() {
|
||||
const h = S.city.happiness;
|
||||
const trend = Math.abs(h.target - h.score) < .005 ? "Steady" : `${h.target > h.score ? "↗" : "↘"} ${h.heading}`;
|
||||
return `<div class="stat" title="${esc(`Mood ${pct(h.score)}, heading to ${pct(h.target)}`)}"><div class="k">Mood ${h.icon}</div>
|
||||
<div class="v ${h.score < .4 ? "bad" : ""}">${h.name}</div><div class="note">${trend}</div></div>`;
|
||||
}
|
||||
|
||||
function moodPanel() {
|
||||
const h = S.city.happiness, e = h.effects;
|
||||
const x = (v) => `×${+v.toFixed(2)}`;
|
||||
const needs = Object.entries(h.needs).map(([key, n]) => `<div class="need">
|
||||
<div class="need-head"><b>${NEED_NAMES[key][0]} ${NEED_NAMES[key][1]}</b><span class="muted small">${pct(n.score)}</span></div>
|
||||
${n.rows.map(reqRow).join("")}</div>`).join("");
|
||||
const locked = h.locked.map((key) => `<span class="tag" title="Needed from ${NEED_TIERS[key]}">🔒 ${NEED_NAMES[key][1]} · ${NEED_TIERS[key]}</span>`).join("");
|
||||
const mods = h.mods.map((m) => `<span class="tag ${m.value < 0 ? "bad" : "good"}">${esc(m.name)} ${m.value > 0 ? "+" : "−"}${pct(Math.abs(m.value))}</span>`).join("");
|
||||
const effects = [`🌱 Growth ${x(e.growth)}`, `🛡️ Guards ${x(e.militia)}`, `🔨 Labour ${x(e.work)}`, `${COIN} Taxes ${x(e.work)}`,
|
||||
...(e.leave ? [`<span class="bad">🚶 ${pct(e.leave)} leave a day</span>`] : [])];
|
||||
return `<div class="panel mood"><h2>${h.icon} Townsfolk · ${h.name}</h2>
|
||||
<div class="mood-bar" title="${esc(`Now ${pct(h.score)} · heading to ${pct(h.target)}`)}"><span style="width:${h.score * 100}%;--c:var(--${h.score < .4 ? "bad" : h.score < .6 ? "warn" : "good"})"></span><i style="left:${h.target * 100}%"></i></div>
|
||||
<div class="mood-effects small">${effects.join(" · ")}</div>
|
||||
<div class="needs">${needs}</div>
|
||||
${mods || locked ? `<div class="mood-tags">${mods}${locked}</div>` : ""}</div>`;
|
||||
}
|
||||
|
||||
function lookChip(id) {
|
||||
const c = C.cosmetics[id];
|
||||
if (c.kind === "colour") return `<span class="swatch" style="background:${c.value}"></span>`;
|
||||
|
|
@ -2285,16 +2324,19 @@ function favourSpends() {
|
|||
</div>`;
|
||||
}
|
||||
|
||||
// A have / need row with its bar, and a Give button when you hold the item.
|
||||
function reqRow(r) {
|
||||
const mine = !r.kinds && S.me.bank[r.item] || 0;
|
||||
return `<div class="req ${r.next ? "dim" : ""}"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
||||
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
||||
<div class="bar"><span style="width:${r.want ? Math.min(100, r.have / r.want * 100) : 100}%"></span></div></div>`;
|
||||
}
|
||||
|
||||
function raidPrep(ev) {
|
||||
const city = S.city, me = S.me, d = city.defence;
|
||||
const names = { mercenaries: "Hired sellswords", ward: "Divine ward (temple)", rallied: "Rallied militia (favour)", drilled: "Drilled militia", townsfolk: "Townsfolk with pitchforks", walls: "Walls", militia: "Paid guards (barracks)", weapons: "Weapons the guards hold", shields: "Shields the guards hold", jerkins: "Body armour the guards wear", salves: "Salves", arrows: "Arrows", garrison: "Garrison" };
|
||||
const full = d.posts * d.wage, owed = Math.max(0, full - d.wages);
|
||||
const reqs = (ev.requests || []).map((r) => {
|
||||
const mine = me.bank[r.item] || 0;
|
||||
return `<div class="req"><span>${item(r.item).icon}</span><span>${esc(r.label)} <span class="muted small">${fmt(r.have)} / ${fmt(r.want)}</span></span>
|
||||
${mine ? `<button class="btn sm gold" data-donate="${r.item}" data-qty="all">Give ${fmt(mine)}</button>` : "<span></span>"}
|
||||
<div class="bar"><span style="width:${Math.min(100, r.have / r.want * 100)}%"></span></div></div>`;
|
||||
}).join("");
|
||||
const reqs = (ev.requests || []).map(reqRow).join("");
|
||||
return `<div class="cols">
|
||||
<div class="panel"><h2>Raid readiness</h2>${reqs}
|
||||
<p class="small muted">Stockpile gear arms the defenders, best first. Fights use up arrows and break some gear.</p></div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue