Combat HP, food and retreat; armour, leatherworking, fishing, tools

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-18 19:27:56 +01:00
parent 3716df7493
commit d2991ebf7a
6 changed files with 381 additions and 44 deletions

View file

@ -127,6 +127,8 @@ function showSummary(sum) {
const got = Object.entries(sum.items).map(([id, q]) => `+${fmt(q)} ${item(id).icon} ${item(id).name}`);
if (sum.gold) got.push(`+${fmt(sum.gold)} gold`);
if (got.length) toast(got.join(" · "), "good");
const eaten = Object.entries(sum.eaten || {});
if (eaten.length) toast(`Ate ${eaten.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")}`);
if (sum.lucky) toast(sum.lucky === 1 ? "✨ Double yield!" : `✨ ${sum.lucky} double yields!`, "level");
const spilled = Object.entries(sum.overflow);
if (spilled.length) toast(`📦 Bank full: ${spilled.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} went to the city`);
@ -142,6 +144,7 @@ function awayModal(sum) {
openModal(`
<h2>While you were away</h2>
<p class="muted">You completed ${fmt(sum.done)} actions${sum.wins || sum.losses ? ` (${sum.wins} fights won, ${sum.losses} lost)` : ""}.</p>
${Object.keys(sum.eaten || {}).length ? `<p class="muted small">Took ${fmt(sum.damage)} damage and ate ${Object.entries(sum.eaten).map(([id, q]) => `${fmt(q)} ${esc(item(id).name.toLowerCase())}`).join(", ")}.</p>` : ""}
${items ? `<ul>${items}</ul>` : ""}
${sum.gold ? `<p class="gold">+${fmt(sum.gold)} gold</p>` : ""}
${xp ? `<ul>${xp}</ul>` : ""}
@ -205,7 +208,7 @@ function sidebar() {
<div class="brand"><img src="/static/icon.svg" alt="">Guildhall <small>alpha</small></div>
<div class="me">
<div class="who">${esc(me.name)}</div>
<div class="cls">${cls.icon} ${cls.name}</div>
<div class="cls">${cls.icon} ${cls.name} · ❤️ ${fmt(me.hp)}/${me.max_hp}</div>
<div class="purse"><span class="gold" title="Gold">🪙 ${fmt(me.gold)}</span><span class="favour" title="Favour">✨ ${fmt(me.favour)}</span></div>
</div>
<div class="navhead">City</div>
@ -305,20 +308,52 @@ function skillPage(id) {
${fast ? `<span class="tag gold">${cls.icon} ${cls.name} bonus</span>` : ""}
</div></div>`;
if (id === "combat") html += combatPanel();
if (`tool_${id}` in me.equip) html += toolPanel(id);
if (id === "reconnaissance") html += `<div class="panel small muted">Every ${8} reconnaissance points on an upcoming event raises its intel by one level, on top of what the spymaster's guild provides. The council & threats page shows the progress.</div>`;
html += `<div class="grid">${C.actions.filter((a) => a.skill === id).map(actionCard).join("")}</div>`;
return html;
let group = null, cards = "";
for (const a of C.actions.filter((a) => a.skill === id)) {
if (a.group && a.group !== group) {
group = a.group;
cards += `<div class="section-title grid-head">${esc(group)}</div>`;
}
cards += actionCard(a);
}
return html + `<div class="grid">${cards}</div>`;
}
function toolPanel(skill) {
const tool = S.me.equip[`tool_${skill}`];
return `<div class="panel tool-panel">${tool
? `${item(tool).icon} <b>${esc(item(tool).name)}</b> <span class="muted">${Math.round(item(tool).speed * 100)}% faster</span>
<button class="btn sm" data-unequip="tool_${skill}">Unequip</button>`
: `<span class="muted">No tool. A smith can make you one that speeds this up.</span>`}</div>`;
}
function combatPanel() {
const me = S.me;
const stat = (id) => [item(id).power && `+${item(id).power} power`, item(id).armour && `+${item(id).armour} armour`].filter(Boolean).join(", ");
const slot = (name, label) => {
const id = me.equip[name];
return `<div class="item-chip">${id ? `${item(id).icon} ${esc(item(id).name)} (+${item(id).power}) <button class="btn sm" data-unequip="${name}">Unequip</button>` : `<span class="muted">No ${label}</span>`}</div>`;
return `<div class="gear-slot">${id
? `<span>${item(id).icon} ${esc(item(id).name)} <span class="muted small">${stat(id)}</span></span><button class="btn sm" data-unequip="${name}">Unequip</button>`
: `<span class="muted">No ${label}</span>`}</div>`;
};
return `<div class="panel"><h2>Equipment</h2>
<div class="btns">${slot("weapon", "weapon")} ${slot("shield", "shield")}</div>
<p class="small muted">Combat power ${me.power} = combat level + gear${me.cls === "warrior" ? ". As a warrior you count for ×1.5 in the garrison" : ""}. Equip swords and shields from the bank.</p></div>`;
const foods = me.edible.map((id) => `<option value="${id}" ${me.food === id ? "selected" : ""}>${esc(item(id).name)} (heals ${item(id).heal}, ${fmt(me.bank[id])} left)</option>`).join("");
const hpPct = me.hp / me.max_hp * 100;
return `<div class="cols">
<div class="panel"><h2>Health</h2>
<div class="hp-line"><b>❤️ ${fmt(me.hp)} / ${me.max_hp}</b><span class="muted small">recovers ${Math.round(C.regen_per_hour * 100)}% an hour</span></div>
<div class="bar thick hp"><span style="width:${hpPct}%"></span></div>
<div class="row" style="margin-top:12px"><label class="small muted">Food in fights
<select data-food-pick>${`<option value="">Cheapest first</option>` + foods}</select></label></div>
<p class="small muted">Below half health you eat from your bank automatically.
${me.edible.length ? "" : `<b class="warn">You have no food in your bank.</b>`}
Out of food and badly hurt, you retreat: the fight is lost, but nothing else is.</p>
</div>
<div class="panel"><h2>Equipment</h2>
<div class="gear">${slot("weapon", "weapon")}${slot("shield", "shield")}${slot("helmet", "helmet")}${slot("body", "body armour")}${slot("legs", "leg armour")}</div>
<p class="small muted">Power ${me.power} (level + weapon + shield + half your armour) · armour ${me.armour} takes ${Math.round(me.reduction * 100)}% off the damage you take${me.cls === "warrior" ? " · as a warrior you count ×1.5 in the garrison" : ""}. Equip gear from the bank.</p>
</div></div>`;
}
function actionCard(a) {
@ -337,7 +372,7 @@ function actionCard(a) {
let extra = "";
if (a.skill === "combat") {
const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04));
extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b>${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
extra = `<div class="small">Win chance <b>${Math.round(chance * 100)}%</b> · about <b>${me.fight_damage[a.id]}</b> damage a fight${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${Math.round(odds * 100)}% chance">${item(id).icon} ${q} <span class="have">${Math.round(odds * 100)}%</span></span>`).join("");
if (a.special === "sabotage") extra += `<div class="small muted">Each win weakens the coming raid by 3%, up to 30%.</div>`;
}
@ -397,7 +432,9 @@ function itemPanel(id) {
<div class="ico">${it.icon}</div>
<h3>${esc(it.name)}</h3>
<div class="muted small">You have ${fmt(have)} · worth ${price % 1 ? price.toFixed(1) : price} gold each${merchant ? " (merchant in town!)" : ""}</div>
${it.slot ? `<div class="row"><button class="btn gold" data-equip="${id}">Equip ${it.slot} (+${it.power} power)</button></div>` : ""}
${it.slot ? `<div class="row"><button class="btn gold" data-equip="${id}">${equipLabel(it)}</button></div>` : ""}
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} in fights.</div>
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first in fights</button>`}</div>` : ""}
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for two days in the city granary.</div>` : ""}
<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div></div>
${others.length ? `<div class="row"><div class="k">Give to <select data-keep="give-to">${others.map((p) => `<option value="${esc(p.id)}">${esc(p.name)}</option>`).join("")}</select></div><div class="btns">${qtyBtns("data-give")}</div></div>` : ""}
@ -406,6 +443,12 @@ function itemPanel(id) {
</div>${goldPanel()}`;
}
function equipLabel(it) {
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
return `Equip ${it.slot === "body" ? "body armour" : it.slot === "legs" ? "leg armour" : it.slot} (${stats})`;
}
function goldPanel() {
return `<div class="panel"><h2>Gold</h2>
<p class="gold">🪙 ${fmt(S.me.gold)}</p>
@ -658,12 +701,17 @@ document.addEventListener("click", (e) => {
if (d.fund) return act("/api/fund", { building: d.fund, item: d.item, source: d.source, qty: "all" });
if (d.garrison) return act("/api/garrison", { join: d.garrison === "1" });
if (d.perk) return act("/api/perk", { perk: d.perk });
if (d.food) return act("/api/food", { item: d.food });
if (d.overflow) return act("/api/settings", { overflow: d.overflow });
if (d.class) { pickedClass = d.class; render(); return; }
if (d.devAdvance) return act("/api/dev/advance", { hours: Number(d.devAdvance) });
if ("devEvent" in d) return act("/api/dev/event", {});
});
document.addEventListener("change", (e) => {
if (e.target.matches("[data-food-pick]")) act("/api/food", { item: e.target.value || null });
});
$("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") e.target.hidden = true; });
document.addEventListener("submit", async (e) => {

View file

@ -194,6 +194,14 @@ textarea { width: 100%; resize: vertical; min-height: 60px; }
.item-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 6px; background: var(--panel-2); }
.item-chip.short { color: var(--bad); }
.action .why { font-size: 13px; color: var(--warn); }
.grid-head { grid-column: 1 / -1; margin: 10px 0 -4px; }
.tool-panel { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.gear { display: flex; flex-direction: column; gap: 6px; }
.gear-slot { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 6px 10px; border-radius: 8px; background: var(--panel-2); }
.hp-line { display: flex; justify-content: space-between; align-items: baseline; gap: 8px; margin-bottom: 6px; }
.bar.hp > span { background: var(--bad); }
.warn { color: var(--warn); }
select { background: var(--bg); border: 1px solid var(--line); border-radius: 8px; padding: 5px 8px; margin-left: 6px; }
.action .foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: auto; }
/* --- bank ------------------------------------------------------------- */