${nav("bank", "🏦", "Bank", `${Object.keys(me.bank).length}/${me.slots}`)}`;
let group = null;
for (const skill of C.skills) {
if (skill.class && skill.class !== me.cls) continue;
if (skill.group !== group) {
group = skill.group;
html += `
`;
if (id === "combat") html += combatPanel();
if (id === "reconnaissance") html += `
Every ${8} reconnaissance points on an upcoming event raises its intel by one level, on top of what the spymaster's guild provides. The council & threats page shows the progress.
Combat power ${me.power} = combat level + gear${me.cls === "warrior" ? ". As a warrior you count for ×1.5 in the garrison" : ""}. Equip swords and shields from the bank.
`;
}
function actionCard(a) {
const me = S.me;
const active = me.action?.id === a.id;
const why = me.blocked[a.id];
const locked = why && (why.startsWith("Needs") || why.startsWith("Only"));
const out = Object.keys(a.outputs)[0];
const icon = out ? item(out).icon : skillOf(a.skill).icon;
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("") + `→`;
}
io += Object.entries(a.outputs).map(([id, q]) => itemChip(id, q)).join("");
let extra = "";
if (a.skill === "combat") {
const chance = Math.min(0.97, Math.max(0.15, 0.5 + (me.power - a.difficulty) * 0.04));
extra = `
Anyone can start a building once the stockpile and treasury cover it. Until then, the stockpile is what raiders come for.
`;
return html;
}
// --- council --------------------------------------------------------------
function pips(level) {
return `${[1, 2, 3].map((i) => ``).join("")}`;
}
function council() {
const ev = S.upcoming, city = S.city, me = S.me;
let html = "";
if (!ev) {
html += `
All quiet
No news on the roads. The spymasters will report as soon as something stirs.
You're one of a handful of adventurers settling a young city at the edge of the kingdom.
Train your skills, supply the council, and keep the place standing when the bandits come.
Your adventurer keeps working while you're away, for up to a day.
${cards}
Every class can train every skill. Your class makes some things faster and gives you one thing only you can do.
`;
}
// --- animation ------------------------------------------------------------
function animate() {
const now = serverNow();
const a = S?.me?.action;
if (a) {
const pct = Math.min(1, (now - a.start) / a.duration);
for (const el of document.querySelectorAll("[data-progress]")) el.style.width = (pct * 100) + "%";
if (pct >= 1 && refreshedFor !== a.start) {
refreshedFor = a.start;
setTimeout(refresh, 150);
}
}
for (const el of document.querySelectorAll("[data-span]")) {
const [start, end] = el.dataset.span.split(",").map(Number);
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);
}
function loop() {
if (S) animate();
requestAnimationFrame(loop);
}
// --- events ---------------------------------------------------------------
document.addEventListener("click", (e) => {
const t = e.target.closest("button, [data-menu], [data-close]");
if (!t) return;
const d = t.dataset;
if (t.matches("[data-close]") || e.target === $("#modal")) { $("#modal").hidden = true; return; }
if ("menu" in d) { $("#app").classList.toggle("open"); return; }
if (d.nav) {
page = d.nav; remember("page", page); selected = null;
history.replaceState(null, "", "#" + page);
$("#app").classList.remove("open");
render(); window.scrollTo(0, 0); return;
}
if (d.start) return act("/api/action", { id: d.start });
if ("stop" in d) return act("/api/stop", {});
if ("select" in d) { selected = d.select || null; render(); return; }
const qty = d.qty === "all" ? "all" : Number(d.qty);
if (d.donate) return act("/api/donate", { item: d.donate, qty });
if (d.sell) return act("/api/sell", { item: d.sell, qty });
if (d.give) return act("/api/give", { item: d.give, qty, to: $("[data-keep=give-to]").value });
if (d.equip) return act("/api/equip", { item: d.equip });
if (d.unequip) return act("/api/equip", { slot: d.unequip });
if (d.build) return act("/api/build", { building: d.build });
if (d.garrison) return act("/api/garrison", { join: d.garrison === "1" });
if (d.perk) return act("/api/perk", { perk: d.perk });
if (d.overflow) return act("/api/settings", { overflow: d.overflow });
if (d.class) { pickedClass = d.class; render(); return; }
if (d.devAdvance) return act("/api/dev/advance", { hours: Number(d.devAdvance) });
if ("devEvent" in d) return act("/api/dev/event", {});
});
$("#modal").addEventListener("click", (e) => { if (e.target.id === "modal") e.target.hidden = true; });
document.addEventListener("submit", async (e) => {
const form = e.target.closest("[data-form]");
if (!form) return;
e.preventDefault();
const field = form.querySelector("[data-keep]");
const value = field.value.trim();
if (form.dataset.form === "register") {
if (!pickedClass) return toast("Pick a class first", "bad");
return act("/api/register", { name: value, cls: pickedClass });
}
if (form.dataset.form === "board") {
if (!value) return;
field.value = "";
return act("/api/board", { text: value });
}
if (form.dataset.form === "gold") {
field.value = "";
return act("/api/donate", { item: "gold", qty: Number(value) });
}
if (form.dataset.form === "dev-user" && value) {
document.cookie = `gh_user=${encodeURIComponent(value)}; path=/; SameSite=Lax`;
field.value = "";
return refresh();
}
});
document.addEventListener("visibilitychange", () => { if (!document.hidden) refresh(); });
(async function boot() {
try {
C = await api("/api/content");
await refresh();
} catch (err) {
$("#main").innerHTML = `