Firekeeping: shared guild fires
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
c64e28fc38
commit
54ae3ae641
6 changed files with 657 additions and 34 deletions
227
static/app.js
227
static/app.js
|
|
@ -149,7 +149,7 @@ function toast(text, kind = "") {
|
|||
}
|
||||
|
||||
function showSummary(sum) {
|
||||
if (!sum.done && !sum.stopped) return;
|
||||
if (!sum.done && !sum.stopped && !sum.levels.length) return;
|
||||
if (sum.done >= SUMMARY_MODAL_AT) return awayModal(sum);
|
||||
const got = Object.entries(sum.items).map(([id, q]) => `+${fmt(q)} ${item(id).name}`);
|
||||
if (sum.gold) got.push(`+${fmt(sum.gold)} gold`);
|
||||
|
|
@ -310,7 +310,11 @@ function nav(id, icon, label, meta = "", extra = "", tip = "") {
|
|||
|
||||
function skillNav(skill, me, doing) {
|
||||
const busy = doing?.skill === skill.id ? `<span class="dot"></span>` : "";
|
||||
return nav("skill:" + skill.id, skill.icon, skill.name, `${me.levels[skill.id]}`, busy);
|
||||
const fire = fireOfSkill(skill.id), heat = fire ? fireNow(fire).heat : -1;
|
||||
const flame = heat >= 0 ? `<span class="fdot" style="--c:${flameTint(heat)}"></span>` : "";
|
||||
const low = skill.id === "firekeeping" && Object.keys(C.fires).some((id) => fireNow(id).low) ? `<span class="alert"></span>` : "";
|
||||
const tip = heat >= 0 ? `${C.fires[fire].name}: ${C.flames[heat][0]}` : low ? "A fire is low" : "";
|
||||
return nav("skill:" + skill.id, skill.icon, skill.name, `${me.levels[skill.id]}`, flame + busy + low, tip);
|
||||
}
|
||||
|
||||
function pinnedNav(me, doing) {
|
||||
|
|
@ -381,6 +385,12 @@ function topbar() {
|
|||
} else if (S.me) {
|
||||
chips += `<span class="chip muted">Idle${S.me.stopped ? ` · ${esc(S.me.stopped)}` : ""}</span>`;
|
||||
}
|
||||
const fires = S.me ? Object.keys(C.fires).map((id) => [id, fireNow(id)]) : [];
|
||||
if (fires.some(([, n]) => n.heat >= 0)) {
|
||||
const tip = fires.map(([id, n]) => `${C.fires[id].name}: ${n.heat >= 0 ? `${C.flames[n.heat][0]}, ${dur(n.left / n.speed)}` : "out"}`).join("\n");
|
||||
chips += `<button class="chip fire-pips" data-nav="skill:firekeeping" title="${esc(tip)}">${fires.map(([, n]) =>
|
||||
`<i class="pip ${n.heat >= 0 ? "lit" : ""} ${n.low ? "low" : ""}" style="--c:${flameTint(n.heat)}"></i>`).join("")}</button>`;
|
||||
}
|
||||
const w = S.happenings?.weather;
|
||||
if (w) chips += `<button class="chip" data-nav="town" title="${esc(weatherTip(w))}">${w.icon}</button>`;
|
||||
if (S.upcoming) chips += `<button class="chip ${["raid", "dragon"].includes(S.upcoming.kind) ? "threat" : ""} ${S.upcoming.at_gates ? "alarm" : ""}" data-nav="council">${eventIcon(S.upcoming.kind)} ${S.upcoming.at_gates ? `<span class="label">${S.upcoming.kind === "dragon" ? "Overhead" : "At the gates"}</span> ` : ""}${eventWhen(S.upcoming, true)}</button>`;
|
||||
|
|
@ -391,11 +401,11 @@ function topbar() {
|
|||
function trackedBar() {
|
||||
const me = S.me, a = me.tracked && C.actions.find((x) => x.id === me.tracked);
|
||||
if (!a) return "";
|
||||
const ready = Object.entries(a.inputs).every(([id, q]) => (me.bank[id] || 0) >= q);
|
||||
const ready = Object.entries(me.fuelled?.[a.id] || a.inputs).every(([id, q]) => (me.bank[id] || 0) >= q);
|
||||
const go = !me.blocked[a.id] && me.action?.id !== a.id;
|
||||
return `<div class="tracked ${ready ? "ready" : ""}">
|
||||
<button class="tracked-name" data-nav="skill:${a.skill}" data-nav-group="${esc(a.group || "")}" title="${esc(a.name)}">${a.icon}<span class="label">${esc(a.name)}</span></button>
|
||||
<span class="io">${Object.entries(a.inputs).map(([id, q]) => itemChip(id, q, me.bank[id] || 0)).join("")}</span>
|
||||
<span class="io">${inputChips(a)}</span>
|
||||
${go ? `<button class="btn sm primary" data-start="${a.id}">Start</button>` : ""}
|
||||
<button class="pin on" data-track="${a.id}" title="Untrack">📌</button>
|
||||
</div>`;
|
||||
|
|
@ -430,6 +440,18 @@ function itemChip(id, qty, have) {
|
|||
return `<span class="item-chip ${short ? "short" : ""}" title="${esc([item(id).name, itemStats(id)].filter(Boolean).join(": "))}">${item(id).icon} ${have !== undefined ? `${fmt(have)} <span class="have">/ ${fmt(qty)}</span>` : fmt(qty)}</span>`;
|
||||
}
|
||||
|
||||
// A recipe's inputs as "have / need", with the fuel a lit fire pays struck through.
|
||||
function inputChips(a) {
|
||||
const me = S.me, ins = me.fuelled?.[a.id] || a.inputs;
|
||||
return Object.entries(a.inputs).map(([id, q]) => {
|
||||
const need = ins[id] || 0, have = me.bank[id] || 0;
|
||||
if (need === q) return itemChip(id, q, have);
|
||||
const tip = `title="${esc(`${item(id).name}: the ${C.fires[a.fire].name.toLowerCase()} pays ${q - need}`)}"`;
|
||||
return need ? `<span class="item-chip ${have < need ? "short" : ""}" ${tip}>${item(id).icon} ${fmt(have)} <span class="have">/ <s>${q}</s> ${need}</span></span>`
|
||||
: `<span class="item-chip paid" ${tip}>${item(id).icon} <s>${q}</s></span>`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
// Progress towards a building cost: "have / need".
|
||||
function costChip(icon, name, need, have) {
|
||||
return `<span class="item-chip ${have < need ? "short" : ""}" title="${esc(`${name}: ${have < need ? `${fmt(need - have)} more needed` : "done"}`)}">${icon} ${fmt(have)} <span class="have">/ ${fmt(need)}</span></span>`;
|
||||
|
|
@ -459,6 +481,8 @@ function skillPage(id) {
|
|||
: `<div class="panel muted">Nothing under construction.</div>`;
|
||||
if (id === "farming" && me.cls === "homesteader") html += plotsPanel();
|
||||
html += skillGear(id);
|
||||
if (id === "firekeeping") return html + firekeepingPage();
|
||||
html += fireBand(id);
|
||||
if (id === "reconnaissance") html += `<div class="panel small muted">Raises intel on the coming threat. Higher levels open as it nears.</div>`;
|
||||
html += skillHappenings(id);
|
||||
const wanted = (S.orders || []).filter((o) => o.skill === id && canTake(o));
|
||||
|
|
@ -539,6 +563,196 @@ function skillGear(skill) {
|
|||
return cards.length ? `<div class="gear-row">${cards.join("")}</div>` : "";
|
||||
}
|
||||
|
||||
// --- fires ----------------------------------------------------------------
|
||||
// The guild's shared fires. Between polls the client burns them down itself, hottest
|
||||
// fuel first, the same way the server does.
|
||||
|
||||
const ROMAN = ["I", "II", "III", "IV", "V", "VI"];
|
||||
// Core to edge of each flame colour, for the animated fires.
|
||||
const FLAME_RAMPS = [["#fff3cf", "#ffb54a", "#ff6a2a", "#8a210a"], ["#fffbe6", "#ffe680", "#f2a81f", "#7a4a06"],
|
||||
["#ffffff", "#f6f4ff", "#c9c4e0", "#4d4a66"], ["#eef8ff", "#8fd0ff", "#3485ea", "#10347a"],
|
||||
["#ecfff1", "#8af5ab", "#22b85a", "#0a4a25"], ["#f8efff", "#d6b0ff", "#9150f0", "#3a1474"]];
|
||||
const STILL = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
let firewood = Number(remember("firewood") ?? -1); // the log tier picked for tending
|
||||
const flames = {}; // fire -> its particles, kept across renders
|
||||
let fireTextAt = 0;
|
||||
|
||||
const fireOfSkill = (skill) => Object.keys(C.fires).find((id) => C.fires[id].skill === skill);
|
||||
const flameTint = (tier) => tier >= 0 ? C.flames[tier][1] : "#5d626b";
|
||||
const tendLevel = (tier) => C.actions.find((a) => a.id === `tend_forge_${C.firewood[tier]}`).level;
|
||||
|
||||
function fireNow(id) {
|
||||
const f = S.city.fires[id], fuel = f.fuel.map((c) => ({ ...c }));
|
||||
let burn = Math.max(0, serverNow() - f.at) * f.speed;
|
||||
while (burn > 0 && fuel.length) {
|
||||
const top = fuel.reduce((best, c) => (c.tier > best.tier ? c : best));
|
||||
const used = Math.min(burn, top.left);
|
||||
top.left -= used;
|
||||
burn -= used;
|
||||
if (top.left <= 1e-6) fuel.splice(fuel.indexOf(top), 1);
|
||||
}
|
||||
const heat = fuel.reduce((h, c) => Math.max(h, c.tier), -1), left = fuel.reduce((sum, c) => sum + c.left, 0);
|
||||
return { fuel, heat, left, low: heat >= 0 && left < C.fire_low, speed: f.speed };
|
||||
}
|
||||
|
||||
function pickFirewood() {
|
||||
const me = S.me, open = C.firewood.map((_, t) => t).filter((t) => me.levels.firekeeping >= tendLevel(t));
|
||||
if (open.includes(firewood)) return firewood;
|
||||
return [...open].reverse().find((t) => me.bank[C.firewood[t]]) ?? 0;
|
||||
}
|
||||
|
||||
function firekeepingPage() {
|
||||
const me = S.me, fk = me.firekeeping, pick = pickFirewood(), speed = Object.values(S.city.fires)[0].speed;
|
||||
const perLog = (tier) => +(C.tend_xp[tier] * fk.minutes).toFixed(1);
|
||||
const logs = C.firewood.map((log, tier) => {
|
||||
const level = tendLevel(tier), locked = me.levels.firekeeping < level;
|
||||
const tip = `${item(log).name} · ${C.flames[tier][0]} · ${locked ? `Needs level ${level}` : `${perLog(tier)} XP a log`}`;
|
||||
return `<button class="logbtn ${tier === pick ? "on" : ""}" style="--c:${flameTint(tier)}" data-firewood="${tier}" ${locked ? "disabled" : ""} title="${esc(tip)}"><span class="swatch"></span>${item(log).icon} ${fmt(me.bank[log] || 0)}</button>`;
|
||||
}).join("");
|
||||
return `<div class="panel fire-tools"><div class="logs">${logs}</div>
|
||||
<span class="tag" title="Fire and XP per log">${glyph("stopwatch", "#98a1b4")} ${dur(fk.minutes * 60 / speed)} · ${perLog(pick)} XP</span>
|
||||
<span class="tag" title="Most fuel you can bank in a fire">${glyph("campfire", "#98a1b4")} ${dur(fk.bank * 60 / speed)}</span></div>
|
||||
<div class="fire-grid">${Object.keys(C.fires).map((id) => fireCard(id, pick)).join("")}</div>`;
|
||||
}
|
||||
|
||||
function fireCard(id, pick) {
|
||||
const me = S.me, spec = C.fires[id], f = S.city.fires[id], n = fireNow(id), skill = skillOf(spec.skill);
|
||||
const mine = me.action?.id.startsWith(`tend_${id}_`);
|
||||
const start = `tend_${id}_${C.firewood[pick]}`, why = mine ? "" : me.blocked[start];
|
||||
const crafting = f.users.filter((u) => !u.action.startsWith("tend_"));
|
||||
const doing = (u) => C.actions.find((a) => a.id === u.action);
|
||||
const who = crafting.length
|
||||
? `<span title="${esc(crafting.map((u) => `${u.name}: ${doing(u).name}`).join("\n"))}">👤 ${crafting.length} ${crafting.map((u) => doing(u).icon).join("")}</span>`
|
||||
: `<span class="muted">👤 0</span>`;
|
||||
return `<div class="fire-card ${n.heat >= 0 ? "lit" : "out"} ${mine ? "active" : ""}" style="--c:${flameTint(n.heat)}">
|
||||
<div class="fire-stage"><canvas data-flame="${id}"></canvas><span class="fire-state" data-fire-state="${id}"></span></div>
|
||||
<div class="fire-body">
|
||||
<div class="fire-head">${glyph(spec.glyph, "#e7e9f0")}<b>${esc(spec.name)}</b><span class="muted small">${skill.icon} ${esc(skill.name)}</span></div>
|
||||
<div class="fuel" data-fuel="${id}"></div>
|
||||
<div class="fire-line"><span data-fire-left="${id}"></span><span data-fire-covers="${id}"></span></div>
|
||||
<div class="fire-line small">${who}<span title="Logs this fire has paid for">🪵 ${fmt(f.saved || 0)}</span></div>
|
||||
${mine ? `<div class="bar thick"><span data-progress></span></div>` : ""}
|
||||
<div class="foot"><span class="why">${esc(why || "")}</span>${mine
|
||||
? `<button class="btn danger" data-stop>Stop</button>`
|
||||
: `<button class="btn primary" data-start="${start}" ${why ? "disabled" : ""}>Tend</button>`}</div>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// The fire behind a crafting skill, at the top of its page.
|
||||
function fireBand(skill) {
|
||||
const id = fireOfSkill(skill);
|
||||
if (!id) return "";
|
||||
const n = fireNow(id), spec = C.fires[id], lit = n.heat >= 0;
|
||||
const pays = lit && `Pays the log for tier ${ROMAN[n.heat]} and below${id === "forge" ? ", and a coal" : ""}`;
|
||||
return `<div class="fire-band ${lit ? "lit" : ""}" style="--c:${flameTint(n.heat)}">
|
||||
${glyph(spec.glyph, lit ? flameTint(n.heat) : "#98a1b4")}
|
||||
<div class="grow"><b>${esc(spec.name)}</b> <span class="muted small">${lit ? `${C.flames[n.heat][0]} · <span data-fire-time="${id}"></span>` : "Out"}</span></div>
|
||||
${lit ? `<span class="tag flame" title="${esc(pays)}">🪵 ≤ ${ROMAN[n.heat]}</span>` : ""}
|
||||
<button class="btn sm" data-nav="skill:firekeeping">🔥 Tend</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function animateFires() {
|
||||
if (!S?.me || !S.city?.fires) return;
|
||||
const text = performance.now() - fireTextAt > 250;
|
||||
if (text) fireTextAt = performance.now();
|
||||
for (const id of Object.keys(C.fires)) {
|
||||
const canvas = document.querySelector(`[data-flame="${id}"]`);
|
||||
if (!canvas && !text) continue;
|
||||
const n = fireNow(id);
|
||||
if (canvas) drawFlame(canvas, id, n);
|
||||
if (!text) continue;
|
||||
const set = (sel, html) => { for (const el of document.querySelectorAll(sel)) if (el.dataset.v !== html) { el.innerHTML = html; el.dataset.v = html; } };
|
||||
const bank = Math.max(S.me.firekeeping.bank * 60, n.left);
|
||||
const tiers = C.flames.map((_, t) => n.fuel.filter((c) => c.tier === t).reduce((sum, c) => sum + c.left, 0));
|
||||
set(`[data-fuel="${id}"]`, tiers.map((left, t) => left ? `<i style="--c:${flameTint(t)};width:${(left / bank * 100).toFixed(2)}%"></i>` : "").join(""));
|
||||
set(`[data-fire-left="${id}"]`, `<b>${dur(n.left / n.speed)}</b> / ${dur(S.me.firekeeping.bank * 60 / n.speed)}`);
|
||||
set(`[data-fire-time="${id}"]`, dur(n.left / n.speed));
|
||||
set(`[data-fire-covers="${id}"]`, n.heat >= 0 ? `<span class="tag flame" style="--c:${flameTint(n.heat)}" title="Pays the log for tier ${ROMAN[n.heat]} and below">🪵 ≤ ${ROMAN[n.heat]}</span>` : "");
|
||||
set(`[data-fire-state="${id}"]`, n.heat < 0 ? "Out" : n.low ? "Low" : C.flames[n.heat][0]);
|
||||
for (const el of document.querySelectorAll(`[data-fire-state="${id}"]`)) el.className = `fire-state ${n.heat < 0 ? "out" : n.low ? "low" : ""}`;
|
||||
}
|
||||
}
|
||||
|
||||
function rampAt(ramp, t) {
|
||||
const s = t * (ramp.length - 1), i = Math.min(Math.floor(s), ramp.length - 2), f = s - i;
|
||||
const rgb = (hex) => [1, 3, 5].map((k) => parseInt(hex.slice(k, k + 2), 16));
|
||||
const a = rgb(ramp[i]), b = rgb(ramp[i + 1]);
|
||||
return `rgb(${a.map((v, k) => Math.round(v + (b[k] - v) * f)).join(",")})`;
|
||||
}
|
||||
|
||||
// A fire on a canvas: rising flame particles in its colour, taller with more fuel;
|
||||
// smoke when it's low, a grey bed of ash when it's out.
|
||||
function drawFlame(cv, id, n) {
|
||||
const st = flames[id] ||= { parts: [], at: performance.now() };
|
||||
const t = performance.now(), dt = Math.min(0.05, (t - st.at) / 1000);
|
||||
st.at = t;
|
||||
const r = cv.getBoundingClientRect(), d = Math.min(devicePixelRatio || 1, 2);
|
||||
if (!r.width) return;
|
||||
if (cv.width !== Math.round(r.width * d)) { cv.width = Math.round(r.width * d); cv.height = Math.round(r.height * d); }
|
||||
const ctx = cv.getContext("2d"), W = cv.width, H = cv.height, cx = W / 2, base = H - 22 * d;
|
||||
const lit = n.heat >= 0, ramp = FLAME_RAMPS[Math.max(0, n.heat)];
|
||||
const power = lit ? Math.min(1, 0.25 + n.left / 14400) : 0;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
if (lit && !STILL) {
|
||||
for (let i = Math.round(90 * power * dt) + 1; i > 0; i--) st.parts.push({ k: "f", x: cx + (Math.random() - 0.5) * 46 * d * (0.6 + power * 0.6), y: base,
|
||||
vx: (Math.random() - 0.5) * 18 * d, vy: -(40 + Math.random() * 60) * d * (0.55 + power), life: 0,
|
||||
max: 0.55 + Math.random() * 0.5 * (0.5 + power), r: (7 + Math.random() * 9) * d * (0.6 + power * 0.5) });
|
||||
if (Math.random() < dt * 6 * power) st.parts.push({ k: "e", x: cx + (Math.random() - 0.5) * 40 * d, y: base - 10 * d,
|
||||
vx: (Math.random() - 0.5) * 30 * d, vy: -(60 + Math.random() * 60) * d, life: 0, max: 1 + Math.random(), r: 1.4 * d });
|
||||
}
|
||||
if (!STILL && (!lit || n.low) && Math.random() < dt * (lit ? 5 : 3)) st.parts.push({ k: "s", x: cx + (Math.random() - 0.5) * 20 * d,
|
||||
y: base - (lit ? 40 * d : 0), vx: (Math.random() - 0.3) * 10 * d, vy: -(18 + Math.random() * 14) * d, life: 0, max: 2.5 + Math.random(), r: (6 + Math.random() * 5) * d });
|
||||
ctx.fillStyle = "#3a2a1e";
|
||||
ctx.save();
|
||||
ctx.translate(cx, base + 8 * d);
|
||||
for (const [angle, w] of [[-0.18, 70], [0.2, 64], [0, 50]]) {
|
||||
ctx.save(); ctx.rotate(angle); ctx.beginPath(); ctx.roundRect(-w / 2 * d, -5 * d, w * d, 10 * d, 5 * d); ctx.fill(); ctx.restore();
|
||||
}
|
||||
ctx.restore();
|
||||
if (lit) {
|
||||
const glow = ctx.createRadialGradient(cx, base + 4 * d, 2, cx, base + 4 * d, 60 * d);
|
||||
glow.addColorStop(0, ramp[1] + "aa");
|
||||
glow.addColorStop(1, ramp[3] + "00");
|
||||
ctx.fillStyle = glow;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
} else {
|
||||
ctx.fillStyle = "#4a4a50";
|
||||
ctx.beginPath(); ctx.ellipse(cx, base + 12 * d, 38 * d, 5 * d, 0, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
if (lit && STILL) {
|
||||
const h = 44 * d * (0.6 + power * 0.5), g = ctx.createRadialGradient(cx, base - 18 * d, 2, cx, base - 18 * d, h);
|
||||
ramp.forEach((c, i) => g.addColorStop(i / 3, c + (i === 3 ? "00" : "ff")));
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath(); ctx.ellipse(cx, base - 18 * d, 26 * d, h, 0, 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
st.parts = st.parts.filter((p) => (p.life += dt) < p.max);
|
||||
ctx.globalCompositeOperation = "lighter";
|
||||
for (const p of st.parts) {
|
||||
p.x += p.vx * dt;
|
||||
p.y += p.vy * dt;
|
||||
const k = p.life / p.max;
|
||||
if (p.k === "f") {
|
||||
p.vx += (cx - p.x) * 1.8 * dt;
|
||||
ctx.globalAlpha = (1 - k) * 0.55;
|
||||
ctx.fillStyle = rampAt(ramp, Math.min(1, k * 1.25));
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, p.r * (1 - k * 0.7), 0, Math.PI * 2); ctx.fill();
|
||||
} else if (p.k === "e") {
|
||||
ctx.globalAlpha = 1 - k;
|
||||
ctx.fillStyle = ramp[1];
|
||||
ctx.fillRect(p.x, p.y, p.r * 2, p.r * 2);
|
||||
}
|
||||
}
|
||||
ctx.globalCompositeOperation = "source-over";
|
||||
for (const p of st.parts) if (p.k === "s") {
|
||||
const k = p.life / p.max;
|
||||
ctx.globalAlpha = (1 - k) * 0.22;
|
||||
ctx.fillStyle = "#9aa0ae";
|
||||
ctx.beginPath(); ctx.arc(p.x, p.y, p.r * (1 + k * 2), 0, Math.PI * 2); ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
function prayerPanel() {
|
||||
const me = S.me, city = S.city, level = me.levels.prayer;
|
||||
const cost = C.prayers.filter((p) => me.prayers.includes(p.id)).reduce((sum, p) => sum + p.cost, 0);
|
||||
|
|
@ -773,8 +987,7 @@ function actionCard(a) {
|
|||
const time = me.times[a.id];
|
||||
let io = "";
|
||||
if (Object.keys(a.inputs).length) {
|
||||
io += Object.entries(a.inputs).map(([id, q]) => itemChip(id, q, me.bank[id] || 0)).join("")
|
||||
+ (Object.keys(a.outputs).length || a.finds ? `<span class="arrow">→</span>` : "");
|
||||
io += inputChips(a) + (Object.keys(a.outputs).length || a.finds ? `<span class="arrow">→</span>` : "");
|
||||
}
|
||||
io += Object.entries(a.outputs).map(([id, q]) => itemChip(id, q)).join("");
|
||||
const chance = ([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}${q > 1 ? ` ×${q}` : ""}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${Math.round(odds * 100)}%</span>`;
|
||||
|
|
@ -1665,6 +1878,7 @@ function animate() {
|
|||
el.style.width = Math.min(100, (now - start) / (end - start) * 100) + "%";
|
||||
}
|
||||
for (const el of document.querySelectorAll("[data-countdown]")) el.textContent = dur(Number(el.dataset.countdown) - now);
|
||||
animateFires();
|
||||
}
|
||||
|
||||
function loop() {
|
||||
|
|
@ -1693,6 +1907,7 @@ document.addEventListener("click", (e) => {
|
|||
setMenu(false);
|
||||
render(); window.scrollTo(0, 0); return;
|
||||
}
|
||||
if (d.firewood) { firewood = Number(d.firewood); remember("firewood", d.firewood); return render(); }
|
||||
if (d.start) return act("/api/action", { id: d.start });
|
||||
if (d.once) return act("/api/action", { id: d.once, once: true });
|
||||
if ("stop" in d) return act("/api/stop", {});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue