Combat in one frame: you, prayers and gear left; the enemy, its traits and the log right. Fight chip in enemy red

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 15:30:05 +01:00
parent 2f2681ba17
commit 308c3a3c01
2 changed files with 67 additions and 33 deletions

View file

@ -360,7 +360,7 @@ function topbar() {
let chips = "";
if (S.me?.action) {
const action = C.actions.find((a) => a.id === (S.me.action.growing || S.me.action.id));
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span><span class="bar mini"><span data-progress></span></span></button>`;
chips += `<button class="chip act" data-nav="skill:${action.skill}">${skillOf(action.skill).icon} <span class="label">${esc(action.name)}</span><span class="bar mini ${S.me.action.fight ? "foe" : ""}"><span data-progress></span></span></button>`;
} else if (S.me) {
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
}
@ -421,7 +421,7 @@ function skillPage(id) {
</div>
${fast ? `<span class="tag gold">${cls.icon} ${cls.name} bonus</span>` : ""}
</div></div>`;
if (id === "combat") html += fightPanel() + combatPanel();
if (id === "combat") html += combatFrame();
if (id === "prayer") html += prayerPanel();
if (id === "construction") html += S.city.construction ? siteBanner()
: `<div class="panel muted">Nothing under construction.</div>`;
@ -550,21 +550,45 @@ function variantRows(e, o) {
}).join("")}</div>`;
}
// The fight in progress, replayed from the server's swing-by-swing record.
function fightPanel() {
const f = S.me.action?.fight;
if (!f) return "";
const side = (who, name, foe) => `<div class="fighter">
<div class="fighter-name"><b>${esc(name)}</b>${foe ? `<span class="v-res">${resistIcons(foe)}</span>` : ""}<span class="small muted" data-f="${who}-hp"></span></div>
<div class="bar hp ${who}"><span data-f="${who}-bar"></span></div>
<div class="bar swing"><span data-f="${who}-swing"></span></div>
// Everything about fighting in one frame. Left: you, your prayers (switch them mid-fight) and your
// gear (swap it mid-fight). Right: what you're fighting, what it attacks with, what it's weak to and
// resists (your weapon's type outlined), and the fight as it plays out.
function combatFrame() {
const me = S.me, a = me.action, f = a?.fight;
const encounter = f && C.actions.find((x) => x.id === a.id);
const bars = (who) => `<div class="bar hp ${who}"><span data-f="${who}-bar"${who === "you" ? ` data-live-hpbar style="width:${me.hp / me.max_hp * 100}%"` : ""}></span></div>
<div class="bar swing"><span data-f="${who}-swing"></span></div>`;
const you = `<div class="fighter">
<div class="fighter-name"><b>${esc(me.name)}</b><span class="small muted">${HEART} <span data-f="you-hp">${fmt(me.hp)} / ${me.max_hp}</span></span></div>
${bars("you")}
${me.edible.length ? "" : `<div class="small warn">No food in your bank.</div>`}
</div>`;
return `<div class="panel fight" data-fight="${S.me.action.start}">
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy, f.foe)}</div>
<ul class="fight-log" data-f="log"></ul>
let foe = `<div class="foe-idle muted">Not fighting.</div>`;
if (f) {
const mine = C.styles[(me.equip.weapon && item(me.equip.weapon).style) || "sword"].damage;
const pills = (label, kinds, how) => kinds?.length ? `<div class="trait"><span class="muted small">${label}</span>
${kinds.map((k) => `<span class="pill ${how} ${k === mine && how !== "attack" ? "yours" : ""}">${dmgIcon(k, how)}${C.damage_types[k].name}</span>`).join("")}</div>` : "";
foe = `<div class="foe-head"><span class="foe-ico">${encounter?.icon || ""}</span>
<div class="fighter"><div class="fighter-name"><b>${esc(f.enemy)}</b><span class="small muted" data-f="foe-hp"></span></div>${bars("foe")}</div></div>
${f.foe ? `<div class="traits">${pills("Attacks with", f.foe.attack, "attack")}${pills("Weak to", f.foe.vulnerable, "vuln")}${pills("Resists", f.foe.resists, "resist")}</div>` : ""}
<ul class="fight-log" data-f="log"></ul>`;
}
return `<div class="panel fight combat-frame" ${f ? `data-fight="${a.start}"` : ""}>
<div class="you-side">${you}${prayerBar(me)}${paperDoll(me, true)}</div>
<div class="foe-side">${foe}</div>
</div>`;
}
// Prayers you can switch on and off between swings, and the points left to pay for them.
function prayerBar(me) {
const level = me.levels.prayer;
const buttons = C.prayers.filter((p) => p.level <= level).map((p) => {
const on = me.prayers.includes(p.id);
return `<button class="pray-btn ${on ? "on" : ""}" data-pray="${p.id}" title="${esc(`${p.name}: ${p.text} · ${p.cost} pt${p.cost > 1 ? "s" : ""}/fight`)}">${p.icon}</button>`;
}).join("");
return `<div class="prayer-bar"><span class="pp ${me.prayers.length && !me.blessing.cost ? "warn" : ""}" title="Prayer points">${skillOf("prayer").icon} ${fmt(me.pp)}</span>${buttons}</div>`;
}
function fightLine(e, enemy) {
const [, who, kind, amount, , , food] = e;
const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : "";
@ -617,24 +641,6 @@ function animateFight(now) {
});
}
function combatPanel() {
const me = S.me;
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>${HEART} <span data-live-hp>${fmt(me.hp)}</span> / ${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 data-live-hpbar 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">${skillOf("prayer").icon} ${me.prayers.length ? `${me.prayers.map((id) => esc(C.prayers.find((p) => p.id === id).name)).join(" + ")} · ${fmt(me.pp)} points${me.blessing.cost ? "" : ` <b class="warn">(out of points)</b>`}` : `<span class="muted">No prayers active.</span>`}
<button class="linkish" data-nav="skill:prayer">Prayers</button></p>
<p class="small muted">Eats below half HP, retreats when out of food.
${me.edible.length ? "" : `<b class="warn">No food in your bank.</b>`}</p>
</div>
${equipmentPanel(me, true, false)}</div>`;
}
// Power, armour and ward, and the armour's weight.
function gearStats(it) {
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`];
@ -1439,7 +1445,6 @@ document.addEventListener("click", (e) => {
});
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; });