Combat rework: packs, abilities, rest, health potions, group bosses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-24 09:49:21 +01:00
parent 472609ac70
commit 57d11e30da
10 changed files with 1562 additions and 380 deletions

View file

@ -192,6 +192,7 @@ function awayModal(sum) {
<p class="muted">You completed ${fmt(sum.done)} actions${sum.wins || sum.losses ? ` (${sum.wins} fights won, ${sum.losses} lost)` : ""}.</p>
${Object.keys(sum.with || {}).length ? `<p class="muted small">⚔️ ${Object.entries(sum.with).map(([name, n]) => `${fmt(n)} with ${esc(name)}`).join(" · ")}</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>` : ""}
${Object.keys(sum.used || {}).length ? `<p class="muted small">Used ${Object.entries(sum.used).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>` : ""}
@ -888,38 +889,58 @@ function variantRows(e, o) {
}).join("")}</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.
// Everything about fighting in one frame. Left: you and whoever's with you, your abilities (on auto),
// your prayers (switch them mid-fight) and your gear (swap it mid-fight). Right: the pack, each with
// what it attacks with and how it takes each damage type (your weapon's 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 allies = f ? f.members.filter((m) => m.id !== S.uid) : [];
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")}
</div>${allies.map((m, i) => `<div class="fighter ally">
<div class="fighter-name"><b>${esc(m.name)}</b><span class="small muted">${HEART} <span data-f="ally${i}-hp"></span></span></div>
<div class="bar hp you"><span data-f="ally${i}-bar"></span></div><div class="bar swing"><span data-f="ally${i}-swing"></span></div>
</div>`).join("")}`;
let foe = `<div class="foe-idle muted">Not fighting.</div>`;
const encounter = a && C.actions.find((x) => x.id === a.id);
const waiting = a?.waiting;
const others = f ? f.members.filter((m) => m.id !== S.uid) : [];
const unit = (id, name, foe = false, extra = "") => `<div class="fighter ${foe ? "foe-unit" : id === S.uid ? "" : "ally"}" data-u="${esc(id)}">
<div class="fighter-name"><b>${name}${extra}</b><span class="small muted"><span data-u-status></span> ${foe ? "" : HEART} <span data-u-hp>${id === S.uid ? `${fmt(me.hp)} / ${me.max_hp}` : ""}</span></span></div>
<div class="bar hp ${foe ? "foe" : "you"}"><span data-u-bar${id === S.uid ? ` data-live-hpbar style="width:${me.hp / me.max_hp * 100}%"` : ""}></span></div>
<div class="bar swing"><span data-u-swing></span></div>
<div class="cast small" data-u-cast></div>
</div>`;
const you = unit(S.uid, esc(me.name)) + others.map((m) => unit(m.id, esc(m.name))).join("");
let foe = `<div class="foe-idle muted">${waiting ? `👥 Waiting for a group · 1 / ${C.group_size}` : "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>` : ""}
const mine = C.styles[me.fighter.style || "sword"]?.damage || me.fighter.damage;
const traits = (v) => `<span class="v-attack">${v.attack.map((k) => dmgIcon(k, "attack")).join("")}</span>
<span class="v-res">${v.vulnerable.map((k) => `<span class="${k === mine ? "yours" : ""}">${dmgIcon(k, "vuln")}</span>`).join("")}${v.resists.map((k) => `<span class="${k === mine ? "yours" : ""}">${dmgIcon(k, "resist")}</span>`).join("")}</span>`;
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" ? ` <span class="muted">· rare</span>` : ""}`, true,
` <span class="traits-inline">${traits(v)}</span>`)).join("")}</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="you-side">${you}${abilityBar(me)}${prayerBar(me)}${paperDoll(me, true)}</div>
<div class="foe-side">${foe}</div>
</div>`;
}
// Your abilities, from your weapon and your trade, on auto: each lights up as it's used and shows its
// cooldown. The weapon's ones still to come are shown dimmed with the level they're learned at.
function abilityBar(me) {
const known = me.fighter.abilities, style = me.fighter.style || "sword";
const shield = !!me.equip.shield;
const tile = (id, on) => {
const ab = C.abilities[id];
const why = on ? "" : ab.needs === "shield" && !shield && me.levels.combat >= ab.level ? "needs a shield" : `combat ${ab.level}`;
const tip = `${ab.name}: ${ab.tip}${ab.cd ? ` · ${ab.cd * C.auto_cd}s on auto` : ""}${why ? ` · ${why}` : ""}`;
return `<span class="ab ${on ? "" : "locked"}" data-ab="${id}" title="${esc(tip)}">${ab.icon}<span class="ab-cd" data-ab-cd></span>
${on ? "" : `<span class="ab-lv">${why === "needs a shield" ? "🛡️" : ab.level}</span>`}</span>`;
};
const kit = C.weapon_kits[style].map((id) => tile(id, known.includes(id)));
const trade = C.trade_ability[me.cls];
const potion = me.health_potion;
return `<div class="ability-bar">${kit.join("")}${trade ? `<span class="ab-gap"></span>${tile(trade, true)}` : ""}
<span class="ab-gap"></span><span class="ab potion ${potion ? "" : "locked"}" data-ab="potion"
title="${esc(potion ? `${item(potion).name}: +${item(potion).drink} HP, once a fight under ${Math.round(C.potion_below * 100)}% · ${fmt(me.bank[potion])} left` : "No health potion")}">❤️${potion ? `<span class="swap-qty">${fmt(me.bank[potion])}</span>` : ""}</span></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;
@ -930,72 +951,129 @@ function prayerBar(me) {
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>`;
}
// A line of the fight: yours as "You", everyone else's by name.
function fightLine(e, enemy) {
const [, who, kind, amount, , , food, target] = e;
const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : "";
const foe = esc(enemy);
const subject = who === "foe" ? target : who, mine = subject === S.uid;
const name = mine ? "You" : esc(playerName(subject)), s = mine ? "" : "s";
if (kind === "eat") return `<li class="${mine ? "good" : "muted"}">${name} eat${s} ${esc(item(food).name.toLowerCase())} (+${amount})</li>`;
if (kind === "wind") return `<li class="good">Second wind! ${mine ? "Back on your feet" : `${name} is back up`}</li>`;
if (kind === "block") return `<li class="${mine ? "good" : "muted"}">${name} block${s} ${foe}'s blow</li>`;
if (who !== "foe") return kind === "miss" ? `<li class="muted">${name} miss${mine ? "" : "es"}</li>` : `<li${mine ? "" : ` class="ally"`}>${name} ${kind === "crit" ? `<b>crit${s}</b>` : `hit${s}`} for <b>${amount}</b>${type}</li>`;
return kind === "miss" ? `<li class="muted">${foe} misses</li>` : `<li class="${mine ? "bad" : "muted"}">${foe} ${kind === "crit" ? "<b>crits</b>" : "hits"} ${mine ? "you" : name} for <b>${amount}</b>${type}</li>`;
const SWINGS = new Set(["hit", "crit", "miss", "block", "absorb"]);
// A line of the fight, terse: who, to whom, how much, with what.
function fightLine(e, f) {
const [, src, tgt, kind, n, , label] = e;
const name = (id) => id === S.uid ? "You" : id?.[0] === "e" ? esc(f.pack[+id.slice(1)].name) : esc(playerName(id));
const ab = C.abilities[label] || C.enemy_abilities[label];
const what = ab ? ` ${ab.icon} ${esc(ab.name)}` : C.damage_types[label] ? ` ${dmgIcon(label)}` : "";
const hurtsMe = tgt === S.uid && src?.[0] === "e";
const tone = hurtsMe ? "bad" : src === S.uid ? "" : "muted";
switch (kind) {
case "hit": case "crit": case "tick":
return `<li class="${tone}">${name(src)} → ${name(tgt)} <b>${n}</b>${kind === "crit" ? " crit" : ""}${what}</li>`;
case "miss": return `<li class="muted">${name(src)} → ${name(tgt)} miss</li>`;
case "block": return `<li class="${tgt === S.uid ? "good" : "muted"}">${name(tgt)} block${tgt === S.uid ? "" : "s"}${what}</li>`;
case "absorb": return `<li class="${tgt === S.uid ? "good" : "muted"}">🔮 ${name(tgt)} soak${tgt === S.uid ? "" : "s"} ${n}</li>`;
case "cast": return `<li class="${src?.[0] === "e" ? "warn" : tone}">${name(src)}${what}…</li>`;
case "use": return `<li class="${tone}">${name(src)}${what}${tgt && tgt !== src ? ` → ${name(tgt)}` : ""}</li>`;
case "stop": return `<li class="good">${name(src)} ✋ ${name(tgt)}${what}</li>`;
case "heal": return `<li class="good">${label === "potion" ? `${name(tgt)} ❤️ potion` : `${name(src)}${what} → ${name(tgt)}`} +${n}</li>`;
case "mend": return `<li class="muted">${name(tgt)} +${n}${what}</li>`;
case "cc": return `<li class="good">${what} ${name(tgt)} held</li>`;
case "free": return `<li class="muted">${name(tgt)} breaks free</li>`;
case "die": return `<li class="good">${name(tgt)} falls</li>`;
case "enrage": return `<li class="bad">😡 ${name(src)} enrages</li>`;
case "wind": return `<li class="good">🌬️ Second wind! ${name(src)}</li>`;
case "down": return `<li class="bad"><b>${src === S.uid ? "You're knocked out" : `${name(src)} is knocked out`}</b></li>`;
case "left": return `<li class="muted">${name(src)} left</li>`;
}
return "";
}
// Where everyone in the fight stands at `t` seconds into it: HP, last swing, cast under way, out or not,
// and when each of your abilities was last used.
function fightState(f, t) {
const rest = f.rest / f.pace, st = {};
for (const m of f.members) st[m.id] = { hp: m.hp_in, top: m.top, from: m.at / f.pace + rest, every: m.interval, swung: null, cast: null, out: null, cds: {} };
f.pack.forEach((foe, i) => { st[`e${i}`] = { hp: foe.hp, top: foe.hp, from: rest, every: foe.interval / f.pace, swung: null, cast: null, out: null, held: false, enraged: false }; });
let shown = -1;
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) {
const [time, src, tgt, kind, , hp, label] = f.events[++shown];
const who = st[src], them = st[tgt];
if (them && hp != null) them.hp = hp;
if (who && SWINGS.has(kind) && (label === "" || C.damage_types[label])) who.swung = time;
if (who && kind === "cast") {
const spec = src[0] === "e" ? C.enemy_abilities[label] : C.abilities[label];
who.cast = { label, from: time, until: time + spec.cast / f.pace };
}
const ab = C.abilities[label];
if (who?.cds && ab && (ab.cast ? kind === "cast" : kind === "use" || SWINGS.has(kind))) who.cds[label] = time;
if (who && kind === "heal" && label === "potion") who.drank = true;
if (them && (kind === "stop" || kind === "cc")) them.cast = null;
if (them && kind === "cc") them.held = true;
if (them && (kind === "free" || kind === "die")) them.held = false;
if (them && kind === "die") them.out = "dead";
if (who && (kind === "down" || kind === "left")) who.out = kind;
if (who && kind === "enrage") who.enraged = true;
}
return { st, shown };
}
function animateFight(now) {
const a = S.me?.action, f = a?.fight;
if (!f) return;
const t = now - a.start;
const t = now - a.start, rest = f.rest / f.pace;
const over = t >= f.end;
const resting = t < rest;
const { st, shown } = fightState(f, t);
const mine = f.members.find((m) => m.id === S.uid) || f.members[0];
// From the kill until the next enemy turns up (the next fight's start, once the server sends it),
// the panel is the same quiet countdown: no enemy, every swing timer empty.
const waiting = t < 0 || (over && f.result === "won" && mine.result === "won");
const respawnIn = t < 0 ? -t : f.end + (a.respawn || 1) - t;
let shown = -1;
while (shown + 1 < f.events.length && f.events[shown + 1][0] <= t) shown++;
const last = f.events[shown];
// Each fighter's HP is in their own swings and bites, and in the enemy's blows at them.
const about = (e, m) => e[1] === m.id || (e[1] === "foe" && e[7] === m.id);
const hpOf = (m) => { for (let i = shown; i >= 0; i--) if (about(f.events[i], m)) return f.events[i][4]; return m.start_hp; };
const swungAt = (id) => { for (let i = shown; i >= 0; i--) if (f.events[i][1] === id && f.events[i][2] !== "eat" && f.events[i][2] !== "wind") return f.events[i][0]; return null; };
const hp = hpOf(mine), foeHp = waiting ? 0 : last ? last[5] : f.enemy_hp;
let fell = Infinity; // beaten or out of food while the others fight on
if (mine.result === "lost" || mine.result === "fled") for (let i = f.events.length - 1; i >= 0; i--) if (about(f.events[i], mine)) { fell = f.events[i][0]; break; }
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${foeHp / f.enemy_hp * 100}%`;
const me = st[mine.id];
// Resting: everyone's HP climbs back to full while they eat.
if (resting) for (const m of f.members) st[m.id].hp = m.start_hp + (m.hp_in - m.start_hp) * Math.min(1, t / rest);
const enemies = f.pack.map((_, i) => st[`e${i}`]);
const left = enemies.reduce((sum, e) => sum + e.hp, 0), total = enemies.reduce((sum, e) => sum + e.top, 0);
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = `${(resting ? 1 : left / total) * 100}%`;
for (const el of document.querySelectorAll("[data-live-hp]")) el.textContent = fmt(me.hp);
for (const el of document.querySelectorAll("[data-live-hpbar]")) el.style.width = `${Math.max(0, me.hp / me.top * 100)}%`;
const panel = $("[data-fight]");
if (!panel) return;
panel.classList.toggle("waiting", waiting);
// Your HP everywhere follows the fight as it plays out.
for (const el of document.querySelectorAll("[data-live-hp]")) el.textContent = fmt(hp);
for (const el of document.querySelectorAll("[data-live-hpbar]")) el.style.width = `${Math.max(0, hp / mine.top * 100)}%`;
const set = (key, fn) => { const el = panel.querySelector(`[data-f="${key}"]`); if (el) fn(el); };
set("you-bar", (el) => { el.style.width = `${Math.max(0, hp / mine.top * 100)}%`; });
set("foe-bar", (el) => { el.style.width = `${Math.max(0, foeHp / f.enemy_hp * 100)}%`; });
set("you-hp", (el) => { el.textContent = `${fmt(hp)} / ${fmt(mine.top)}`; });
set("foe-hp", (el) => { el.textContent = waiting ? "" : `${fmt(foeHp)} / ${fmt(f.enemy_hp)}`; });
const swing = (key, swung, every, from = 0, gone = false) => set(`${key}-swing`, (el) => {
el.style.width = over || waiting || gone || t < from ? "0%" : `${Math.min(100, (t - (swung ?? from)) / every * 100)}%`;
});
swing("you", swungAt(mine.id), mine.interval, mine.at / f.pace, t >= fell);
swing("foe", swungAt("foe"), f.enemy_interval);
f.members.filter((m) => m.id !== S.uid).forEach((m, i) => {
const ally = hpOf(m), left = m.left != null && t >= m.left / f.pace;
set(`ally${i}-bar`, (el) => { el.style.width = `${Math.max(0, ally / m.top * 100)}%`; });
set(`ally${i}-hp`, (el) => { el.textContent = t < m.at / f.pace ? "joining…" : left ? "left" : `${fmt(ally)} / ${fmt(m.top)}`; });
swing(`ally${i}`, swungAt(m.id), m.interval, m.at / f.pace, left);
});
set("log", (el) => {
const key = `${a.start}:${shown}:${over}:${waiting ? Math.ceil(respawnIn * 10) : ""}`;
if (el.dataset.key === key) return;
el.dataset.key = key;
const lines = f.events.slice(Math.max(0, shown - 5), shown + 1).reverse().map((e) => fightLine(e, f.enemy));
if ((over && !waiting) || t >= fell) lines.unshift(`<li class="bad"><b>You fall back.</b></li>`);
el.innerHTML = waiting ? `<li class="good"><b>Victory!</b> ${respawnIn > 0.05 ? `Next enemy in ${respawnIn.toFixed(1)}s` : "Next enemy coming…"}</li>`
: lines.join("") || `<li class="muted">Squaring up…</li>`;
});
panel.classList.toggle("waiting", resting);
for (const el of panel.querySelectorAll("[data-u]")) {
const u = st[el.dataset.u];
if (!u) continue;
const gone = !!u.out || over;
el.classList.toggle("out", !!u.out);
el.querySelector("[data-u-bar]").style.width = `${Math.max(0, u.hp / u.top * 100)}%`;
el.querySelector("[data-u-hp]").textContent = u.out === "left" ? "left" : t < u.from && !resting ? "joining…" : `${fmt(u.hp)} / ${fmt(u.top)}`;
el.querySelector("[data-u-status]").textContent = u.out === "dead" ? "💀" : u.out === "down" ? "💫" : u.held ? "🌿" : u.enraged ? "😡" : "";
el.querySelector("[data-u-swing]").style.width = gone || t < u.from ? "0%" : `${Math.min(100, (t - (u.swung ?? u.from)) / u.every * 100)}%`;
const cast = !gone && u.cast && t < u.cast.until ? u.cast : null;
const spec = cast && (el.dataset.u[0] === "e" ? C.enemy_abilities : C.abilities)[cast.label];
const castEl = el.querySelector("[data-u-cast]");
const key = cast ? `${cast.label}:${cast.from}` : "";
if (castEl.dataset.key !== key) {
castEl.dataset.key = key;
castEl.innerHTML = cast ? `<span>${spec.icon} ${esc(spec.name)}</span><span class="bar"><span></span></span>` : "";
}
if (cast) castEl.querySelector(".bar > span").style.width = `${Math.min(100, (t - cast.from) / (cast.until - cast.from) * 100)}%`;
}
// Your abilities: the shade is the cooldown left, a flash when one's just been used.
for (const el of panel.querySelectorAll("[data-ab]")) {
const id = el.dataset.ab;
if (id === "potion") { el.classList.toggle("spent", !!me.drank); continue; }
const ab = C.abilities[id], used = me.cds?.[id];
const cd = (ab.cd || 0) * C.auto_cd / f.pace;
const leftCd = used == null || over ? 0 : Math.max(0, used + cd - t);
el.querySelector("[data-ab-cd]").style.height = `${cd ? leftCd / cd * 100 : 0}%`;
el.classList.toggle("fired", used != null && t - used < 0.6);
}
const log = panel.querySelector('[data-f="log"]');
if (log) {
const key = `${a.start}:${shown}:${over}:${resting ? Math.ceil(rest - t) : ""}`;
if (log.dataset.key !== key) {
log.dataset.key = key;
const eaten = Object.entries(mine.eaten || {});
const lines = f.events.slice(Math.max(0, shown - 6), shown + 1).reverse().map((e) => fightLine(e, f)).filter(Boolean);
if (over) lines.unshift(f.result === "won" ? `<li class="good"><b>Victory!</b> Next fight coming…</li>`
: `<li class="bad"><b>${f.result === "fled" ? "It dragged on: you fall back." : "Beaten."}</b></li>`);
log.innerHTML = resting ? `<li class="muted">Resting · ${Math.ceil(rest - t)}s</li>${eaten.length
? `<li class="muted">${HEART} ${eaten.map(([id, q]) => `${fmt(q)} ${esc(item(id).name.toLowerCase())}`).join(", ")}</li>` : ""}`
: lines.join("") || `<li class="muted">Squaring up…</li>`;
}
}
}
// A piece of jewellery's bonus, e.g. "+8% accuracy".
@ -1052,6 +1130,7 @@ function itemStats(id) {
const it = item(id);
const parts = [];
if (it.heal) parts.push(`❤️ Heals ${it.heal}`);
if (it.drink) parts.push(`❤️ +${it.drink} in a fight`);
if (it.food) parts.push(`feeds ${it.food} for 2 days`);
if (it.power) parts.push(`+${it.power} power`);
if (it.style) {
@ -1096,7 +1175,8 @@ function actionCard(a) {
let extra = "";
if (a.enemy) {
const o = me.fight_odds[a.id], e = a.enemy;
const own = a.personal_loot ? `<span class="tag good" title="Everyone in the fight gets their own loot">👥 Own loot</span>` : "";
const own = (a.together ? `<span class="tag warn" title="Fought by ${C.group_size} or more: alone, you wait for someone to join">👥 Group</span> ` : "")
+ (a.personal_loot ? `<span class="tag good" title="Everyone in the fight gets their own loot">🎁 Own loot</span>` : "");
extra = `${e.boss || own ? `<div>${e.boss ? `<span class="tag warn">Boss</span> ` : ""}${own}</div>` : ""}${variantRows(e, o)}${fightingHere(a, why)}`;
if (a.gold) io += `<span class="item-chip" title="Gold: ${a.gold[0]}–${a.gold[1]} a kill">${COIN} ${a.gold[0]}–${a.gold[1]}</span>`;
if (a.drops) io += a.drops.map(chance).join("");
@ -1228,8 +1308,9 @@ function itemPanel(id) {
${it.potion ? `<div class="row"><div class="small">${esc(it.potion.text)} for ${C.potion_charges} ${it.potion.skill === "combat" ? "fights" : "actions"}.</div>
<button class="btn gold" data-potion="${id}">Drink</button>
${S.me.potions[it.potion.skill] && S.me.potions[it.potion.skill].item !== id ? `<div class="small why">Replaces your ${esc(item(S.me.potions[it.potion.skill].item).name.toLowerCase())} (${S.me.potions[it.potion.skill].charges} uses left)</div>` : ""}</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.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} resting between fights.</div>
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first</button>`}</div>` : ""}
${it.drink ? `<div class="row small muted">+${it.drink} HP in a fight, drunk once a fight under ${Math.round(C.potion_below * 100)}% HP.</div>` : ""}
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for 2 days.</div>` : ""}
${C.city_uses[id].length
? `<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div>
@ -1919,7 +2000,7 @@ function slot2(slot, p) {
return slot in p.equip || (slot === "food" && "food" in p) ? slot : null;
}
// Food for fights: one you picked to eat first, or the cheapest first. Only your own sheet shows it.
// Food for resting between fights: one you picked to eat first, or the cheapest first. Only your own sheet shows it.
function foodSlot(p, picked) {
if (!("food" in p)) return `<span class="doll-slot future" title="Food"></span>`;
const id = p.food && p.bank[p.food] ? p.food : null;
@ -2002,7 +2083,7 @@ function animate() {
const now = serverNow();
const a = S?.me?.action;
animateFight(now);
if (a) {
if (a && !a.waiting) {
const pct = Math.min(1, (now - a.start) / a.duration);
// In a fight the bar is the enemy's health (set by animateFight), not a timer.
if (!a.fight) for (const el of document.querySelectorAll("[data-progress]")) el.style.width = (pct * 100) + "%";
@ -2010,7 +2091,7 @@ function animate() {
if (pct >= 1 && refreshedFor !== key) {
refreshedFor = key;
setTimeout(refresh, 150);
} else if (a.fight && now - a.start > a.duration + (a.respawn || 1) && now - retriedAt > 2) {
} else if (a.fight && now - a.start > a.duration + 1 && now - retriedAt > 2) {
retriedAt = now; // the next enemy hasn't turned up yet: ask again
refresh();
}