Character sheets; make one then go back to what you were doing
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d2991ebf7a
commit
34dc85284d
5 changed files with 167 additions and 9 deletions
|
|
@ -134,6 +134,7 @@ function showSummary(sum) {
|
|||
if (spilled.length) toast(`📦 Bank full: ${spilled.map(([id, q]) => `${fmt(q)} ${item(id).name.toLowerCase()}`).join(", ")} went to the city`);
|
||||
for (const [skill, level] of sum.levels) toast(`🎉 ${skillOf(skill).name} level ${level}!`, "level");
|
||||
if (sum.stopped) toast(`Stopped: ${sum.stopped}`, "bad");
|
||||
if (sum.resumed) toast(`Back to ${C.actions.find((x) => x.id === sum.resumed).name.toLowerCase()}`);
|
||||
}
|
||||
|
||||
function awayModal(sum) {
|
||||
|
|
@ -217,6 +218,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("bank", "🏦", "Bank", `${Object.keys(me.bank).length}/${me.slots}`)}`;
|
||||
let group = null;
|
||||
for (const skill of C.skills) {
|
||||
|
|
@ -242,11 +244,12 @@ function devbar() {
|
|||
</div></div>`;
|
||||
}
|
||||
|
||||
const TITLES = { town: "Town square", council: "Council & threats", board: "Town board", roster: "Guild roster", bank: "Bank" };
|
||||
const TITLES = { town: "Town square", council: "Council & threats", board: "Town board", roster: "Guild roster", bank: "Bank", character: "Character" };
|
||||
|
||||
function topbar() {
|
||||
let title = TITLES[page] || "Guildhall";
|
||||
if (page.startsWith("skill:")) title = skillOf(page.slice(6))?.name || title;
|
||||
if (page.startsWith("character:")) title = S.players.find((p) => p.id === page.slice(10))?.name || "Character";
|
||||
if (!S.me) title = "Welcome to Guildhall";
|
||||
let chips = "";
|
||||
if (S.me?.action) {
|
||||
|
|
@ -276,6 +279,8 @@ function pageHtml() {
|
|||
if (skill && (!skill.class || skill.class === S.me.cls)) return skillPage(id);
|
||||
page = "town";
|
||||
}
|
||||
if (page.startsWith("character:")) return characterSheet(page.slice(10));
|
||||
if (page === "character") return characterSheet(S.uid);
|
||||
return ({ town, council, board, roster, bank }[page] || town)();
|
||||
}
|
||||
|
||||
|
|
@ -386,9 +391,12 @@ function actionCard(a) {
|
|||
${io ? `<div class="io">${io}</div>` : ""}
|
||||
${extra}
|
||||
${active ? `<div class="bar thick"><span data-progress></span></div>` : ""}
|
||||
${active && me.action.left ? `<div class="small muted">Just one${me.action.resume ? `, then back to ${esc(C.actions.find((x) => x.id === me.action.resume).name.toLowerCase())}` : ""}</div>` : ""}
|
||||
<div class="foot">
|
||||
<span class="why">${!active && why ? esc(why) : ""}</span>
|
||||
${active ? `<button class="btn danger" data-stop>Stop</button>` : `<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button>`}
|
||||
${active ? `<button class="btn danger" data-stop>Stop</button>`
|
||||
: `<span class="btns"><button class="btn" data-once="${a.id}" ${why ? "disabled" : ""} title="Do this once, then go back to what you were doing">${a.skill === "combat" ? "Fight once" : Object.keys(a.outputs).length ? "Make one" : "Once"}</button>
|
||||
<button class="btn primary" data-start="${a.id}" ${why ? "disabled" : ""}>Start</button></span>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
|
@ -619,10 +627,86 @@ function roster() {
|
|||
<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="levels">${levels}</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>`;
|
||||
}
|
||||
|
||||
// --- character sheet -----------------------------------------------------
|
||||
|
||||
const SLOT_NAMES = { weapon: "Weapon", shield: "Shield", helmet: "Helmet", body: "Body", legs: "Legs" };
|
||||
|
||||
function characterSheet(uid) {
|
||||
const mine = uid === S.uid;
|
||||
const p = mine ? { ...S.players.find((x) => x.id === uid), ...S.me } : S.players.find((x) => x.id === uid);
|
||||
if (!p) return `<p class="muted">No such adventurer.</p>`;
|
||||
const cls = C.classes[p.cls];
|
||||
const skills = C.skills.filter((s) => !s.class || s.class === p.cls);
|
||||
const total = skills.reduce((sum, s) => sum + p.levels[s.id], 0);
|
||||
const days = Math.max(1, Math.ceil((serverNow() - p.joined) / 86400));
|
||||
const guard = Math.round(p.power * (p.cls === "warrior" ? 1.5 : 1));
|
||||
|
||||
const gearSlot = (slot, label, empty) => {
|
||||
const id = p.equip[slot];
|
||||
const it = id && item(id);
|
||||
const stat = !it ? "" : it.tool ? `${Math.round(it.speed * 100)}% faster`
|
||||
: [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
||||
return `<div class="sheet-slot ${id ? "" : "empty"}">
|
||||
<div class="k">${label}</div>
|
||||
${id ? `<div class="sheet-item"><span class="ico">${it.icon}</span><span><b>${esc(it.name)}</b><span class="muted small">${stat}</span></span>
|
||||
${mine ? `<button class="btn sm" data-unequip="${slot}" title="Back to the bank">✕</button>` : ""}</div>`
|
||||
: `<div class="muted small">${empty}</div>`}
|
||||
</div>`;
|
||||
};
|
||||
|
||||
const skillRows = skills.map((s) => {
|
||||
const level = p.levels[s.id], xp = p.xp[s.id] || 0;
|
||||
const floor = C.xp_table[level], ceil = C.xp_table[level + 1];
|
||||
const pct = ceil ? (xp - floor) / (ceil - floor) * 100 : 100;
|
||||
const tool = p.equip[`tool_${s.id}`];
|
||||
const marks = (cls.fast.includes(s.id) ? `<span title="${cls.name} bonus">${cls.icon}</span>` : "")
|
||||
+ (tool ? `<span title="${esc(item(tool).name)}">${item(tool).icon}</span>` : "");
|
||||
return `<${mine ? `button data-nav="skill:${s.id}"` : "div"} class="sheet-skill">
|
||||
<span class="ico">${s.icon}</span>
|
||||
<span class="grow"><span class="row1"><b>${s.name}</b><span class="marks">${marks}</span><span class="lv">${level}</span></span>
|
||||
<span class="bar xp"><span style="width:${pct}%"></span></span></span>
|
||||
</${mine ? "button" : "div"}>`;
|
||||
}).join("");
|
||||
|
||||
return `
|
||||
<div class="panel sheet-head">
|
||||
<div class="big">${cls.icon}</div>
|
||||
<div class="grow">
|
||||
<h2>${esc(p.name)}</h2>
|
||||
<div class="muted">${cls.name} · total level ${total} · ${days === 1 ? "first day" : `${days} days`} in the city</div>
|
||||
<div class="small" style="margin-top:6px">${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>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div class="stat"><div class="k">Health</div><div class="v">${fmt(p.hp)} <small>/ ${p.max_hp}</small></div><div class="bar hp" style="margin-top:6px"><span style="width:${p.hp / p.max_hp * 100}%"></span></div></div>
|
||||
<div class="stat"><div class="k">Combat power</div><div class="v">${p.power}</div><div class="note">${guard} in the garrison</div></div>
|
||||
<div class="stat"><div class="k">Armour</div><div class="v">${p.armour}</div><div class="note">${Math.round(p.reduction * 100)}% less damage</div></div>
|
||||
<div class="stat"><div class="k">Given to the city</div><div class="v favour">${fmt(p.donated)}</div><div class="note">${fmt(p.favour)} favour to spend</div></div>
|
||||
${mine ? `<div class="stat"><div class="k">Purse</div><div class="v gold">${fmt(p.gold)}</div><div class="note">${Object.keys(p.bank).length} / ${p.slots} bank slots</div></div>` : ""}
|
||||
</div>
|
||||
<div class="cols">
|
||||
<div>
|
||||
<div class="panel"><h2>Class</h2>
|
||||
<p><b>${cls.icon} ${cls.name}</b></p>
|
||||
<p class="small">${esc(cls.bonus)}.</p>
|
||||
<p class="small">${esc(cls.signature)}.</p>
|
||||
</div>
|
||||
<div class="panel"><h2>Equipment</h2>
|
||||
<div class="sheet-gear">${Object.entries(SLOT_NAMES).map(([slot, label]) => gearSlot(slot, label, "Nothing")).join("")}</div>
|
||||
<h2 style="margin-top:16px">Tools</h2>
|
||||
<div class="sheet-gear">${C.skills.filter((s) => `tool_${s.id}` in p.equip).map((s) => gearSlot(`tool_${s.id}`, s.name, "No tool")).join("")}</div>
|
||||
${mine ? `<p class="small muted">Equip gear and tools from the <button class="linkish" data-nav="bank">bank</button>.</p>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel"><h2>Skills</h2><div class="sheet-skills">${skillRows}</div></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// --- registration ---------------------------------------------------------
|
||||
|
||||
function register() {
|
||||
|
|
@ -684,6 +768,7 @@ document.addEventListener("click", (e) => {
|
|||
render(); window.scrollTo(0, 0); return;
|
||||
}
|
||||
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", {});
|
||||
if ("select" in d) {
|
||||
selected = d.select || null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue