DPS meter under the combat log

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-25 20:30:15 +00:00
parent 1bf3f3b62f
commit 4f2d113abd
3 changed files with 48 additions and 1 deletions

View file

@ -943,7 +943,8 @@ function combatFrame() {
foe = `<div class="foe-head"><span class="foe-ico">${encounter?.icon || ""}</span><b>${esc(f.enemy)}</b></div>
<div class="pack">${f.pack.map((v, i) => unit(`e${i}`, `${esc(v.name)}${v.archetype === "elite" && !v.boss ? ` <span class="muted">· rare</span>` : ""}`, true,
` <span class="traits-inline">${traits(v)}</span>`)).join("")}</div>
<ul class="fight-log" data-f="log"></ul>`;
<ul class="fight-log" data-f="log"></ul>
<div class="dps" data-f="dps"></div>`;
}
return `<div class="panel fight combat-frame" ${f ? `data-fight="${a.start}"` : ""}>
<div class="you-side">${you}${abilityBar(me)}${prayerBar(me)}${paperDoll(me, true)}</div>
@ -1071,6 +1072,28 @@ function fightState(f, t) {
return { st, shown };
}
// The damage meter: what each fighter has dealt so far, and per second since they joined (game seconds,
// so dev speed doesn't inflate it). Best first, bars scaled to the top DPS.
const DAMAGE = new Set(["hit", "crit", "tick"]);
let lastMeter = null; // the fight just finished, shown dimmed while resting for the next one
function dpsMeter(f, t) {
const rows = f.members.map((m) => {
let dmg = 0, heal = 0;
for (const e of f.events) {
if (e[0] > t) break;
if (e[1] !== m.id) continue;
if (DAMAGE.has(e[3])) dmg += e[4] || 0;
else if (e[3] === "heal" && e[6] !== "potion") heal += e[4] || 0;
}
const secs = Math.max(1, Math.min(t, f.end) - (m.at + f.rest) / f.pace) * f.pace;
return { m, dmg, heal, dps: dmg / secs };
}).sort((x, y) => y.dmg - x.dmg);
const top = Math.max(...rows.map((r) => r.dps)) || 1;
return rows.map(({ m, dmg, heal, dps }) => `<li class="${m.id === S.uid ? "mine" : ""}"
title="${esc(`${fmt(dmg)} damage${heal ? ` · ${fmt(heal)} healing` : ""}`)}"><span class="dps-fill" style="width:${dps / top * 100}%"></span>
<b>${esc(playerName(m.id))}</b><span>${dps < 10 ? dps.toFixed(1) : fmt(Math.round(dps))} <span class="muted">· ${fmt(dmg)}</span></span></li>`).join("");
}
function animateFight(now) {
const a = S.me?.action, f = a?.fight;
if (!f) return;
@ -1166,6 +1189,20 @@ function animateFight(now) {
: lines.join("") || `<li class="muted">Squaring up…</li>`;
}
}
// Refreshed on every swing and once a second, since DPS falls between hits.
const meter = panel.querySelector('[data-f="dps"]');
if (meter) {
const stale = resting && lastMeter?.start !== a.start ? lastMeter : null;
const key = resting ? `rest:${stale?.start}` : `${a.start}:${shown}:${Math.floor(Math.min(t, f.end))}`;
if (meter.dataset.key !== key) {
meter.dataset.key = key;
if (!resting) lastMeter = { start: a.start, rows: dpsMeter(f, t) };
const rows = resting ? stale?.rows : lastMeter.rows;
meter.classList.toggle("stale", resting);
meter.innerHTML = rows ? `<div class="dps-head small muted"><span>${resting ? "Last fight" : "Damage"}</span><span title="Damage per second · total">DPS · total</span></div>
<ul>${rows}</ul>` : "";
}
}
}
// A piece of jewellery's bonus, e.g. "+8% accuracy".

View file

@ -545,6 +545,15 @@ button.sheet-skill:hover { background: var(--panel-2); }
.pill.yours { box-shadow: 0 0 0 2px var(--gold); }
/* Fixed at 7 lines (newest first) so the frame doesn't grow and shrink with the log; older lines drop off the bottom */
.combat-frame .fight-log { margin: 0; line-height: 1.35; height: calc(7 * 1.35em + 12px); min-height: 0; overflow: hidden; }
/* Damage meter under the log: a row per fighter, the bar behind it scaled to the top DPS */
.dps { display: grid; gap: 6px; font-size: 13px; }
.dps.stale { opacity: .55; }
.dps-head { display: flex; justify-content: space-between; }
.dps ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 3px; }
.dps li { position: relative; display: flex; justify-content: space-between; gap: 8px; padding: 3px 8px; border-radius: 6px; background: var(--bg); overflow: hidden; }
.dps li > :not(.dps-fill) { position: relative; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dps-fill { position: absolute; inset: 0 auto 0 0; background: rgba(152, 161, 180, .18); transition: width .3s ease-out; }
.dps li.mine .dps-fill { background: rgba(224, 176, 74, .3); }
.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;