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 = ""; let chips = "";
if (S.me?.action) { if (S.me?.action) {
const action = C.actions.find((a) => a.id === (S.me.action.growing || S.me.action.id)); 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) { } else if (S.me) {
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`; chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
} }
@ -421,7 +421,7 @@ function skillPage(id) {
</div> </div>
${fast ? `<span class="tag gold">${cls.icon} ${cls.name} bonus</span>` : ""} ${fast ? `<span class="tag gold">${cls.icon} ${cls.name} bonus</span>` : ""}
</div></div>`; </div></div>`;
if (id === "combat") html += fightPanel() + combatPanel(); if (id === "combat") html += combatFrame();
if (id === "prayer") html += prayerPanel(); if (id === "prayer") html += prayerPanel();
if (id === "construction") html += S.city.construction ? siteBanner() if (id === "construction") html += S.city.construction ? siteBanner()
: `<div class="panel muted">Nothing under construction.</div>`; : `<div class="panel muted">Nothing under construction.</div>`;
@ -550,21 +550,45 @@ function variantRows(e, o) {
}).join("")}</div>`; }).join("")}</div>`;
} }
// The fight in progress, replayed from the server's swing-by-swing record. // Everything about fighting in one frame. Left: you, your prayers (switch them mid-fight) and your
function fightPanel() { // gear (swap it mid-fight). Right: what you're fighting, what it attacks with, what it's weak to and
const f = S.me.action?.fight; // resists (your weapon's type outlined), and the fight as it plays out.
if (!f) return ""; function combatFrame() {
const side = (who, name, foe) => `<div class="fighter"> const me = S.me, a = me.action, f = a?.fight;
<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> const encounter = f && C.actions.find((x) => x.id === a.id);
<div class="bar hp ${who}"><span data-f="${who}-bar"></span></div> 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> <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>`; </div>`;
return `<div class="panel fight" data-fight="${S.me.action.start}"> let foe = `<div class="foe-idle muted">Not fighting.</div>`;
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy, f.foe)}</div> if (f) {
<ul class="fight-log" data-f="log"></ul> 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>`; </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) { function fightLine(e, enemy) {
const [, who, kind, amount, , , food] = e; const [, who, kind, amount, , , food] = e;
const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : ""; 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. // Power, armour and ward, and the armour's weight.
function gearStats(it) { function gearStats(it) {
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`]; 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) => { 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; }); $("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") e.target.hidden = true; });

View file

@ -235,7 +235,7 @@ select { background: var(--bg); border: 1px solid var(--line); border-radius: 8p
.fund { display: flex; flex-direction: column; gap: 6px; } .fund { display: flex; flex-direction: column; gap: 6px; }
.fight .fighters { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; align-items: center; } .fight .fighters { display: grid; grid-template-columns: 1fr auto 1fr; gap: 16px; align-items: center; }
.fight .vs { font-size: 22px; } .fight .vs { font-size: 22px; }
.fight.waiting .fighter:last-child { opacity: .45; } .fight.waiting .foe-side .foe-head, .fight.waiting .traits { opacity: .45; }
.fighter { display: flex; flex-direction: column; gap: 6px; min-width: 0; } .fighter { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.fighter-name { display: flex; justify-content: space-between; gap: 8px; } .fighter-name { display: flex; justify-content: space-between; gap: 8px; }
.fighter-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .fighter-name b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@ -510,3 +510,32 @@ button.sheet-skill:hover { background: var(--panel-2); }
.swap-qty { position: absolute; top: 1px; left: 3px; font-size: 10px; font-weight: 700; color: var(--muted); } .swap-qty { position: absolute; top: 1px; left: 3px; font-size: 10px; font-weight: 700; color: var(--muted); }
.doll-slot.swap .swap-type .gi { width: 11px; height: 11px; } .doll-slot.swap .swap-type .gi { width: 11px; height: 11px; }
.doll-slot.swap .hands { top: 1px; right: 1px; font-size: 8px; padding: 0 2px; } .doll-slot.swap .hands { top: 1px; right: 1px; font-size: 8px; padding: 0 2px; }
/* The combat frame: you on the left, the enemy on the right */
.combat-frame { display: grid; grid-template-columns: 1fr 1fr; gap: 26px; }
.you-side, .foe-side { display: grid; gap: 14px; align-content: start; min-width: 0; }
.foe-head { display: grid; grid-template-columns: auto 1fr; gap: 12px; align-items: center; }
.foe-ico { width: 56px; height: 56px; border-radius: 12px; background: var(--panel-2); display: grid; place-items: center; }
.foe-ico .gi { width: 40px; height: 40px; }
.foe-idle { padding: 18px 0; }
.traits { display: grid; gap: 6px; }
.trait { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.trait > .muted { width: 84px; }
.pill { display: inline-flex; align-items: center; gap: 5px; font-size: 13px; padding: 2px 9px 2px 3px; border-radius: 99px;
background: var(--panel-2); border: 1px solid var(--line); }
.pill.vuln { border-color: rgba(95, 174, 90, .6); }
.pill.resist { border-color: rgba(226, 100, 92, .55); }
.pill.yours { box-shadow: 0 0 0 2px var(--gold); }
.combat-frame .fight-log { margin: 0; }
.prayer-bar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.prayer-bar .pp { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; margin-right: 4px; }
.pray-btn { width: 34px; height: 34px; border-radius: 9px; display: grid; place-items: center; padding: 0; cursor: pointer;
background: var(--panel-2); border: 1px solid var(--line); color: inherit; opacity: .55; }
.pray-btn .gi { width: 22px; height: 22px; }
.pray-btn.on { opacity: 1; border-color: var(--gold); box-shadow: 0 0 0 1px var(--gold); }
@media (max-width: 900px) {
.combat-frame { display: flex; flex-direction: column; gap: 16px; }
.you-side { display: contents; }
.you-side > .fighter { order: 1; } .foe-side { order: 2; } .prayer-bar { order: 3; } .you-side > .doll-wrap { order: 4; }
}
.bar.mini.foe > span { background: var(--bad); }