Character sheet: equipment grid on a warrior outline, slot details, compact tools
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a40b837541
commit
812813c455
4 changed files with 110 additions and 21 deletions
|
|
@ -11,6 +11,7 @@ let S = null; // latest state
|
|||
let skew = 0; // server clock minus client clock, seconds
|
||||
let page = decodeURIComponent(location.hash.slice(1)) || remember("page") || "town";
|
||||
let selected = null; // bank item
|
||||
let dollPick = null; // equipment slot shown on the character sheet
|
||||
let pickedClass = null; // registration
|
||||
const tabs = {}; // skill -> selected recipe tab
|
||||
let refreshing = false;
|
||||
|
|
@ -668,7 +669,7 @@ const GEAR_SLOTS = ["weapon", "shield", "helmet", "body", "legs"];
|
|||
|
||||
// A weapon's or armour piece's stats in a small grid. Weapons: damage type and power | 1H or 2H | swing
|
||||
// time; armour: armour | weight | ward. Below: what it needs to wear, and any bonus for you.
|
||||
function gearChips(it) {
|
||||
function gearChips(it, who = S.me) {
|
||||
const cell = (icon, text, tip, cls = "") => `<span class="gstat ${cls}" title="${esc(tip)}">${icon}${text === "" ? "" : `<span>${text}</span>`}</span>`;
|
||||
const hands = (two) => `<span class="hands">${two ? "2H" : "1H"}</span>`;
|
||||
let row = ["", "", ""];
|
||||
|
|
@ -689,10 +690,10 @@ function gearChips(it) {
|
|||
let req = "", bonus = "";
|
||||
if (it.req) {
|
||||
const [skill, level] = it.req;
|
||||
req = cell(skillOf(skill).icon, level, `Needs ${skillOf(skill).name.toLowerCase()} ${level}`, S.me.levels[skill] < level ? "short" : "");
|
||||
req = cell(skillOf(skill).icon, level, `Needs ${skillOf(skill).name.toLowerCase()} ${level}`, who.levels[skill] < level ? "short" : "");
|
||||
}
|
||||
if (it.style && C.class_style[S.me.cls] === it.style) {
|
||||
bonus = cell(C.classes[S.me.cls].icon, "+15%", "Your trade's weapon: +15% damage", "trade");
|
||||
if (it.style && C.class_style[who.cls] === it.style) {
|
||||
bonus = cell(C.classes[who.cls].icon, "+15%", "The trade's own weapon: +15% damage", "trade");
|
||||
}
|
||||
return `<div class="stat-grid">${row.join("")}${req}<span></span>${bonus || "<span></span>"}</div>`;
|
||||
}
|
||||
|
|
@ -1200,19 +1201,6 @@ function characterSheet(uid) {
|
|||
const days = Math.max(1, Math.ceil((serverNow() - p.joined) / 86400));
|
||||
const guard = Math.round(p.power);
|
||||
|
||||
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`
|
||||
: gearStats(it);
|
||||
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];
|
||||
|
|
@ -1255,16 +1243,75 @@ function characterSheet(uid) {
|
|||
<p class="small">${esc(cls.combat)}.</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>
|
||||
${paperDoll(p, mine)}
|
||||
<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 from the <button class="linkish" data-nav="bank">bank</button>.</p>` : ""}
|
||||
${toolSlots(p)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel"><h2>Skills</h2><div class="sheet-skills">${skillRows}</div></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// The character, dressed: a compact grid of slots on a warrior's outline, Melvor-style, with
|
||||
// room for slots the game doesn't have yet. A two-handed weapon fills the off hand too, faded.
|
||||
// Picking a slot shows what's in it beside the grid.
|
||||
const DOLL = [
|
||||
["cape", "Cape", "cape"], ["helmet", "Head", "visored-helm"], ["amulet", "Amulet", "gem-pendant"],
|
||||
["weapon", "Main hand", "pointy-sword"], ["body", "Body", "breastplate"], ["shield", "Off hand", "round-shield"],
|
||||
["gloves", "Gloves", "gauntlet"], ["legs", "Legs", "leg-armor"], ["ring", "Ring", "ring"],
|
||||
["ammo", "Ammo", "quiver"], ["boots", "Boots", "leather-boot"], ["trinket", "Trinket", "crystal-ball"],
|
||||
];
|
||||
|
||||
function dollSlot(p, slot, label, outline, picked) {
|
||||
const known = slot in p.equip, two = slot === "shield" && p.equip.weapon && item(p.equip.weapon).two_handed;
|
||||
const id = two ? p.equip.weapon : p.equip[slot];
|
||||
const tip = !known ? `${label}: not in the game yet` : id ? `${label}: ${item(id).name}` : `${label}: empty`;
|
||||
return `<button class="doll-slot ${known ? "" : "future"} ${id ? "" : "empty"} ${two ? "ghost" : ""} ${picked ? "on" : ""}"
|
||||
${known ? `data-doll="${slot}"` : "disabled"} title="${esc(tip)}">
|
||||
${id ? item(id).icon : glyph(outline, "#98a1b4")}${two ? `<span class="hands">2H</span>` : ""}</button>`;
|
||||
}
|
||||
|
||||
function paperDoll(p, mine) {
|
||||
const worn = DOLL.filter(([slot]) => p.equip[slot]).map(([slot]) => slot);
|
||||
const pick = (dollPick && slot2(dollPick, p)) || (worn.includes("weapon") ? "weapon" : worn[0]) || "weapon";
|
||||
const label = DOLL.find(([slot]) => slot === pick)[1];
|
||||
const two = pick === "shield" && p.equip.weapon && item(p.equip.weapon).two_handed;
|
||||
const id = two ? p.equip.weapon : p.equip[pick], it = id && item(id);
|
||||
const detail = it ? `<div class="worn-head"><span class="ico">${it.icon}</span><span><b>${esc(it.name)}</b>
|
||||
<span class="muted small">${label}${two ? " (two-handed weapon)" : ""}</span></span>
|
||||
${mine && !two ? `<button class="btn sm" data-unequip="${pick}" title="Back to the bank">✕</button>` : ""}</div>
|
||||
${gearChips(it, p)}`
|
||||
: `<div class="muted small">${label}: nothing worn.${mine ? ` Equip from the <button class="linkish" data-nav="bank">bank</button>.` : ""}</div>`;
|
||||
const st = C.styles[(p.equip.weapon && item(p.equip.weapon).style) || "sword"], dt = C.damage_types[st.damage];
|
||||
return `<div class="doll-wrap">
|
||||
<div class="doll"><span class="doll-figure">${glyph("swordman", "#e6e8ee")}</span>
|
||||
${DOLL.map(([slot, name, outline]) => dollSlot(p, slot, name, outline, slot === pick)).join("")}</div>
|
||||
<div class="doll-side">
|
||||
<div class="doll-detail">${detail}</div>
|
||||
<div class="doll-stats">
|
||||
<span>${glyph(dt.glyph, dt.tint)} ${dt.name}, a swing every ${st.interval}s</span>
|
||||
<span class="muted">Power</span><b>${p.power}</b>
|
||||
<span class="muted">${glyph("checked-shield", "#d4dae2")} Armour</span><b>${p.armour} <span class="muted small">-${Math.round(p.reduction * 100)}% physical</span></b>
|
||||
<span class="muted">${glyph("magic-shield", "#a77be8")} Ward</span><b>${p.ward} <span class="muted small">-${Math.round(p.ward_reduction * 100)}% magic</span></b>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// A picked slot only counts if it's one this character has.
|
||||
function slot2(slot, p) {
|
||||
return slot in p.equip ? slot : null;
|
||||
}
|
||||
|
||||
// Tools: one small slot per gathering skill.
|
||||
function toolSlots(p) {
|
||||
return `<div class="tool-slots">${C.skills.filter((s) => `tool_${s.id}` in p.equip).map((s) => {
|
||||
const id = p.equip[`tool_${s.id}`], it = id && item(id);
|
||||
return `<span class="doll-slot small ${id ? "" : "empty"}" title="${esc(it ? `${it.name}: ${Math.round(it.speed * 100)}% faster ${s.name.toLowerCase()}` : `${s.name}: no tool`)}">
|
||||
${it ? it.icon : s.icon}</span>`;
|
||||
}).join("")}</div>`;
|
||||
}
|
||||
|
||||
// --- registration ---------------------------------------------------------
|
||||
|
||||
function classCards() {
|
||||
|
|
@ -1354,6 +1401,10 @@ document.addEventListener("click", (e) => {
|
|||
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 (d.doll) {
|
||||
dollPick = d.doll;
|
||||
return render();
|
||||
}
|
||||
if ("select" in d) {
|
||||
selected = d.select || null;
|
||||
render();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue