Icons: game-icons.net glyphs tinted per tier, built into one sprite
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2eed9b7ec4
commit
5cc5b98d61
7 changed files with 314 additions and 18 deletions
|
|
@ -19,6 +19,17 @@ let version = null; // server build this page was loaded from
|
|||
let failures = 0; // polls in a row that didn't reach the server
|
||||
|
||||
const $ = (sel, root = document) => root.querySelector(sel);
|
||||
|
||||
// Icons are game-icons.net glyphs (CC BY 3.0) from one sprite, tinted per tier or material.
|
||||
const glyph = (name, tint) => `<svg class="gi" style="color:${tint}" aria-hidden="true"><use href="/static/icons.svg#${name}"></use></svg>`;
|
||||
const COIN = glyph("two-coins", "#e0b04a"), FAVOUR = glyph("sparkles", "#b98ce8"), HEART = glyph("hearts", "#e2645c");
|
||||
|
||||
function iconise(content) {
|
||||
const entries = [...Object.values(content.items), ...content.skills, ...Object.values(content.classes),
|
||||
...Object.values(content.buildings), ...content.prayers, ...content.actions];
|
||||
for (const entry of entries) if (entry.glyph) entry.icon = glyph(entry.glyph, entry.tint);
|
||||
return content;
|
||||
}
|
||||
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
||||
const fmt = (n) => Math.floor(n).toLocaleString("en-GB");
|
||||
const serverNow = () => Date.now() / 1000 + skew;
|
||||
|
|
@ -125,7 +136,7 @@ function toast(text, kind = "") {
|
|||
function showSummary(sum) {
|
||||
if (!sum.done && !sum.stopped) return;
|
||||
if (sum.done >= SUMMARY_MODAL_AT) return awayModal(sum);
|
||||
const got = Object.entries(sum.items).map(([id, q]) => `+${fmt(q)} ${item(id).icon} ${item(id).name}`);
|
||||
const got = Object.entries(sum.items).map(([id, q]) => `+${fmt(q)} ${item(id).name}`);
|
||||
if (sum.gold) got.push(`+${fmt(sum.gold)} gold`);
|
||||
if (got.length) toast(got.join(" · "), "good");
|
||||
const eaten = Object.entries(sum.eaten || {});
|
||||
|
|
@ -218,8 +229,8 @@ function sidebar() {
|
|||
<div class="brand"><img src="/static/icon.svg" alt="">Guildhall <small>alpha</small></div>
|
||||
<div class="me">
|
||||
<div class="who">${esc(me.name)}</div>
|
||||
<div class="cls">${cls.icon} ${cls.name} · ❤️ ${fmt(me.hp)}/${me.max_hp}</div>
|
||||
<div class="purse"><span class="gold" title="Gold">🪙 ${fmt(me.gold)}</span><span class="favour" title="Favour">✨ ${fmt(me.favour)}</span></div>
|
||||
<div class="cls">${cls.icon} ${cls.name} · ${HEART} ${fmt(me.hp)}/${me.max_hp}</div>
|
||||
<div class="purse"><span class="gold" title="Gold">${COIN} ${fmt(me.gold)}</span><span class="favour" title="Favour">${FAVOUR} ${fmt(me.favour)}</span></div>
|
||||
</div>
|
||||
<div class="navhead">City</div>
|
||||
${nav("town", "🏘️", "Town square", `${S.city.population}`)}
|
||||
|
|
@ -227,7 +238,7 @@ function sidebar() {
|
|||
${nav("board", "💬", "Town board")}
|
||||
${nav("roster", "👥", "Guild roster", `${S.players.length}`)}
|
||||
<div class="navhead">You</div>
|
||||
${nav("character", cls.icon, "Character", `⚔️ ${me.levels.combat}`)}
|
||||
${nav("character", cls.icon, "Character", `${skillOf("combat").icon} ${me.levels.combat}`)}
|
||||
${nav("bank", "🏦", "Bank", `${Object.keys(me.bank).length}/${me.slots}`)}`;
|
||||
let group = null;
|
||||
for (const skill of C.skills) {
|
||||
|
|
@ -239,7 +250,8 @@ function sidebar() {
|
|||
const busy = doing?.skill === skill.id ? `<span class="dot"></span>` : "";
|
||||
html += nav("skill:" + skill.id, skill.icon, skill.name, `${me.levels[skill.id]}`, busy);
|
||||
}
|
||||
return html + (S.dev ? devbar() : "");
|
||||
return html + (S.dev ? devbar() : "")
|
||||
+ `<p class="credit">Icons by Lorc, Delapouite and others at <a href="https://game-icons.net" target="_blank" rel="noopener">game-icons.net</a>, CC BY 3.0</p>`;
|
||||
}
|
||||
|
||||
function devbar() {
|
||||
|
|
@ -391,7 +403,7 @@ function prayerPanel() {
|
|||
const temple = city.buildings.temple.level;
|
||||
return `<div class="cols">
|
||||
<div class="panel"><h2>Prayers · ${me.prayers.length} / ${C.max_prayers} active</h2>
|
||||
<div class="prayer-points">🙏 <b>${fmt(me.pp)}</b> prayer points${cost ? ` · ${cost} per fight, enough for about ${fmt(me.pp / cost)} fights` : ""}</div>
|
||||
<div class="prayer-points">${skillOf("prayer").icon} <b>${fmt(me.pp)}</b> prayer points${cost ? ` · ${cost} per fight, enough for about ${fmt(me.pp / cost)} fights` : ""}</div>
|
||||
<div class="prayers">${cards}</div>
|
||||
<p class="small muted">Active prayers work in every fight while you have the points to pay for them, and each point spent trains Prayer. Bury bones for more points.</p>
|
||||
</div>
|
||||
|
|
@ -423,11 +435,11 @@ function combatPanel() {
|
|||
const hpPct = me.hp / me.max_hp * 100;
|
||||
return `<div class="cols">
|
||||
<div class="panel"><h2>Health</h2>
|
||||
<div class="hp-line"><b>❤️ ${fmt(me.hp)} / ${me.max_hp}</b><span class="muted small">recovers ${Math.round(C.regen_per_hour * 100)}% an hour</span></div>
|
||||
<div class="hp-line"><b>${HEART} ${fmt(me.hp)} / ${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 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">🙏 ${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">(not enough points: prayers are off)</b>`}` : `<span class="muted">No prayers active.</span>`}
|
||||
<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">(not enough points: prayers are off)</b>`}` : `<span class="muted">No prayers active.</span>`}
|
||||
<button class="linkish" data-nav="skill:prayer">Prayers</button></p>
|
||||
<p class="small muted">Below half health you eat from your bank automatically.
|
||||
${me.edible.length ? "" : `<b class="warn">You have no food in your bank.</b>`}
|
||||
|
|
@ -459,7 +471,7 @@ function actionCard(a) {
|
|||
const why = me.blocked[a.id];
|
||||
const locked = isLocked(a);
|
||||
const out = Object.keys(a.outputs)[0];
|
||||
const icon = out ? item(out).icon : skillOf(a.skill).icon;
|
||||
const icon = a.icon;
|
||||
const time = me.times[a.id];
|
||||
let io = "";
|
||||
if (Object.keys(a.inputs).length) {
|
||||
|
|
@ -477,7 +489,7 @@ function actionCard(a) {
|
|||
if (a.special === "recon") extra = `<div class="small muted">+${a.recon} recon on the next event</div>`;
|
||||
const temple = S.city.buildings.temple?.level || 0;
|
||||
if (a.special === "labour") extra = `<div class="small makes">🏗️ +${me.labour_per_action} labour per action</div>`;
|
||||
if (a.special === "bury") extra = `<div class="small makes">🙏 +${a.pp + temple} prayer points</div>`;
|
||||
if (a.special === "bury") extra = `<div class="small makes">${skillOf("prayer").icon} +${a.pp + temple} prayer points</div>`;
|
||||
if (a.special === "offer") extra = `<div class="small makes">⛪ +${+(a.devotion * (1 + 0.25 * temple)).toFixed(1)} devotion for the city's ward</div>`;
|
||||
const made = Object.keys(a.outputs).map(itemStats).filter(Boolean);
|
||||
if (made.length) extra += `<div class="small makes">${made.join(" · ")}</div>`;
|
||||
|
|
@ -576,7 +588,7 @@ function equipLabel(it) {
|
|||
|
||||
function goldPanel() {
|
||||
return `<div class="panel"><h2>Gold</h2>
|
||||
<p class="gold">🪙 ${fmt(S.me.gold)}</p>
|
||||
<p class="gold">${COIN} ${fmt(S.me.gold)}</p>
|
||||
<form data-form="gold" class="btns"><input type="number" min="1" data-keep="gold" placeholder="Amount" style="width:120px"><button class="btn gold">Donate to treasury</button></form>
|
||||
<p class="small muted">The treasury pays the gold part of new buildings. 1 favour per gold.</p></div>`;
|
||||
}
|
||||
|
|
@ -584,7 +596,7 @@ function goldPanel() {
|
|||
function favourPanel() {
|
||||
const me = S.me;
|
||||
return `<div class="panel"><h2>Favour with the council</h2>
|
||||
<p class="favour">✨ ${fmt(me.favour)} favour · ${fmt(me.donated)} given in total</p>
|
||||
<p class="favour">${FAVOUR} ${fmt(me.favour)} favour · ${fmt(me.donated)} given in total</p>
|
||||
<div class="btns"><button class="btn" data-perk="bank_slot" ${me.favour < me.perk_costs.bank_slot ? "disabled" : ""}>+1 bank slot (${fmt(me.perk_costs.bank_slot)} favour)</button></div>
|
||||
<p class="small muted">Favour can also hire builders, pay informants and rally the militia: see <button class="linkish" data-nav="council">Council & threats</button>.</p></div>`;
|
||||
}
|
||||
|
|
@ -633,7 +645,7 @@ function siteBanner() {
|
|||
<span class="muted small">${fmt(work.labour)} / ${fmt(work.needed)} labour</span>
|
||||
<span class="btns">
|
||||
${working ? `<span class="tag good">You're working on it</span>` : `<button class="btn sm primary" data-start="labour">Work on it</button>`}
|
||||
${hire.why ? "" : `<button class="btn sm gold" data-favour="builders" ${me.favour < hire.cost ? "disabled" : ""} title="Adds half the labour that's left">Hire builders ✨ ${fmt(hire.cost)}</button>`}
|
||||
${hire.why ? "" : `<button class="btn sm gold" data-favour="builders" ${me.favour < hire.cost ? "disabled" : ""} title="Adds half the labour that's left">Hire builders ${FAVOUR} ${fmt(hire.cost)}</button>`}
|
||||
</span></div>
|
||||
<div class="bar gold" style="margin-top:6px"><span style="width:${Math.min(100, work.labour / work.needed * 100)}%"></span></div>
|
||||
<div class="small muted" style="margin-top:6px">${builders.length ? `On site: ${builders.join(", ")}. ` : ""}The townsfolk add about ${fmt(city.townsfolk_labour)} labour an hour.${queued ? ` Next: ${queued}.` : ""}</div>
|
||||
|
|
@ -710,7 +722,7 @@ function buildingCard(key, b) {
|
|||
const gold = id === "gold";
|
||||
const mine = Math.min(missing, gold ? me.gold : me.bank[id] || 0);
|
||||
const stores = Math.min(missing, gold ? city.treasury : city.stockpile[id] || 0);
|
||||
const chip = costChip(gold ? "🪙" : item(id).icon, gold ? "Gold" : item(id).name, need, need - missing);
|
||||
const chip = costChip(gold ? COIN : item(id).icon, gold ? "Gold" : item(id).name, need, need - missing);
|
||||
return `<div class="fund-row">${chip}<span class="btns">
|
||||
${mine ? `<button class="btn sm gold" data-fund="${key}" data-item="${id}" data-source="bank" title="From your ${gold ? "purse" : "bank"}, earns favour">Give ${fmt(mine)}</button>` : ""}
|
||||
${stores ? `<button class="btn sm" data-fund="${key}" data-item="${id}" data-source="city" title="From the city's ${gold ? "treasury" : "stockpile"}">${gold ? "Treasury" : "Stockpile"} ${fmt(stores)}</button>` : ""}
|
||||
|
|
@ -782,10 +794,10 @@ function favourSpends() {
|
|||
<span class="ico">${icon}</span>
|
||||
<span class="grow"><b>${title}</b><span class="small muted">${text}</span></span>
|
||||
<span class="btns"><button class="btn sm gold" data-favour="${kind}" ${o.why || short ? "disabled" : ""}
|
||||
title="${esc(o.why || (short ? "Not enough favour" : ""))}">✨ ${fmt(o.cost)}</button></span>
|
||||
title="${esc(o.why || (short ? "Not enough favour" : ""))}">${FAVOUR} ${fmt(o.cost)}</button></span>
|
||||
</div>`;
|
||||
};
|
||||
return `<div class="panel"><h2>Spend favour on the city · you have ✨ ${fmt(me.favour)}</h2>
|
||||
return `<div class="panel"><h2>Spend favour on the city · you have ${FAVOUR} ${fmt(me.favour)}</h2>
|
||||
${option("builders", "🏗️", "Hire extra builders", f.builders.why ? esc(f.builders.why) + "." : `Adds ${fmt(f.builders.labour)} labour to the current building, half of what's left.`)}
|
||||
${option("informant", "🕵️", "Pay an informant", f.informant.why ? esc(f.informant.why) + "." : "One more level of intel on what's coming.")}
|
||||
${option("militia", "📯", "Rally the militia", f.militia.why ? esc(f.militia.why) + "." : `+10 defence for the coming raid${f.militia.rallied ? ` (+${f.militia.rallied} so far)` : ""}. Costs more each time.`)}
|
||||
|
|
@ -838,7 +850,7 @@ function roster() {
|
|||
return `<div class="panel person"><div class="top"><span class="ico">${cls.icon}</span><div>
|
||||
<b>${esc(p.name)}</b> <span class="muted small">${cls.name} · total level ${p.total}</span>
|
||||
<div class="small">${p.doing ? `${skillOf(p.skill).icon} ${esc(p.doing)}` : `<span class="muted">Idle</span>`}${p.garrison ? ` · <span class="tag warn">On guard</span>` : ""}</div>
|
||||
<div class="small favour">✨ ${fmt(p.donated)} given to the city</div></div></div>
|
||||
<div class="small favour">${FAVOUR} ${fmt(p.donated)} given to the city</div></div></div>
|
||||
<div class="levels">${levels}</div>
|
||||
<p><button class="btn sm" data-nav="character:${esc(p.id)}">Character sheet</button></p></div>`;
|
||||
}).join("")}</div>`;
|
||||
|
|
@ -1127,7 +1139,7 @@ document.addEventListener("visibilitychange", () => { if (!document.hidden) refr
|
|||
|
||||
(async function boot() {
|
||||
try {
|
||||
C = await api("/api/content");
|
||||
C = iconise(await api("/api/content"));
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
$("#main").innerHTML = `<p class="loading">Couldn't reach the city: ${esc(err.message)}</p>`;
|
||||
|
|
|
|||
98
static/icons.svg
Normal file
98
static/icons.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 145 KiB |
|
|
@ -397,3 +397,10 @@ button.sheet-skill:hover { background: var(--panel-2); }
|
|||
.skill-happenings { flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||
.msg-delete { background: none; border: 0; color: var(--muted); padding: 0 4px; margin-left: 4px; font-size: 12px; }
|
||||
.msg-delete:hover { color: var(--bad); }
|
||||
.gi { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; flex: none; }
|
||||
.item-chip .gi { width: 1.25em; height: 1.25em; vertical-align: -0.3em; }
|
||||
.slot .gi { width: 34px; height: 34px; }
|
||||
.action .ico .gi, .skill-head .big .gi, .sheet-head .big .gi { width: 70%; height: 70%; }
|
||||
.nav .ic .gi { width: 20px; height: 20px; vertical-align: middle; }
|
||||
.credit { margin: 14px 18px 0; font-size: 11.5px; color: var(--muted); }
|
||||
.credit a { color: var(--muted); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue