Skill pages: potion and tool side by side with swaps; happenings as red and green banners with a fix button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-20 16:04:50 +01:00
parent 29ed3780da
commit 89fd9b3c40
3 changed files with 54 additions and 29 deletions

View file

@ -3562,7 +3562,7 @@ for _index, (_area, _level, _prefix, _races) in enumerate(AREAS):
ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index]) ACTION_GLYPHS[f"fight_{_slug(_race)}"] = (RACE_GLYPHS[_race], TIER_TINTS[_index])
UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield", "stopwatch", "swordman", "biceps", "cancel", "striking-diamonds", UI_GLYPHS = ["two-coins", "sparkles", "hearts", "prayer", "checked-shield", "magic-shield", "stopwatch", "swordman", "biceps", "cancel", "striking-diamonds",
"archery-target", "dodging", "shield-reflect", "archery-target", "dodging", "shield-reflect",
"cape", "gem-pendant", "gauntlet", "ring", "quiver", "leather-boot", "crystal-ball", "hot-meal", "visored-helm", "breastplate", "cape", "gem-pendant", "gauntlet", "ring", "quiver", "leather-boot", "crystal-ball", "hot-meal", "round-bottom-flask", "visored-helm", "breastplate",
"leg-armor", "pointy-sword", "round-shield", "leg-armor", "pointy-sword", "round-shield",
*(kind["glyph"] for kind in DAMAGE_TYPES.values())] *(kind["glyph"] for kind in DAMAGE_TYPES.values())]

View file

@ -426,8 +426,7 @@ function skillPage(id) {
if (id === "construction") html += S.city.construction ? siteBanner() if (id === "construction") html += S.city.construction ? siteBanner()
: `<div class="panel muted">Nothing under construction.</div>`; : `<div class="panel muted">Nothing under construction.</div>`;
if (id === "farming" && me.cls === "homesteader") html += plotsPanel(); if (id === "farming" && me.cls === "homesteader") html += plotsPanel();
html += potionPanel(id); html += skillGear(id);
if (`tool_${id}` in me.equip) html += toolPanel(id);
if (id === "reconnaissance") html += `<div class="panel small muted">Raises intel on the coming threat. Higher levels open as it nears.</div>`; 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); html += skillHappenings(id);
const live = new Set(S.happenings.problems.map((p) => p.kind)); const live = new Set(S.happenings.problems.map((p) => p.kind));
@ -473,19 +472,32 @@ function plotsPanel() {
} }
// The skill's active potion, or the ones in the bank that could be drunk for it. // The skill's active potion, or the ones in the bank that could be drunk for it.
function potionPanel(skill) { // A skill's potion and tool side by side, each a slot with what you could switch to beside it.
const me = S.me; function skillGear(skill) {
const me = S.me, cards = [];
const kinds = Object.entries(C.items).filter(([, it]) => it.potion?.skill === skill); const kinds = Object.entries(C.items).filter(([, it]) => it.potion?.skill === skill);
if (!kinds.length) return ""; if (kinds.length) {
const active = me.potions[skill]; const active = me.potions[skill], it = active && item(active.item);
const owned = kinds.filter(([id]) => me.bank[id]); const others = kinds.filter(([id]) => me.bank[id] && id !== active?.item).map(([id, p]) =>
const drink = owned.map(([id, it]) => `<button class="btn sm" data-potion="${id}" title="${esc(it.potion.text)}">${it.icon} ${esc(it.name)} (${fmt(me.bank[id])})</button>`).join(""); `<button class="doll-slot swap" data-potion="${id}" title="${esc(`${p.name}: ${p.potion.text}`)}">${p.icon}<span class="swap-qty">${fmt(me.bank[id])}</span></button>`).join("");
return `<div class="panel tool-panel">${active cards.push(`<div class="panel gear-card">
? `${item(active.item).icon} <b>${esc(item(active.item).name)}</b> <span class="muted">${esc(item(active.item).potion.text)} · ${fmt(active.charges)} uses left${me.bank[active.item] ? `, ${fmt(me.bank[active.item])} more in the bank` : ""}</span>` <span class="doll-slot ${it ? "" : "empty"}" title="${esc(it ? `${it.name}: ${it.potion.text}` : "No potion")}">${it ? it.icon : glyph("round-bottom-flask", "#98a1b4")}</span>
: `<span class="muted">No potion.</span>`} <div class="gear-text">${it ? `<b>${esc(it.name)}</b><span class="muted small">${esc(it.potion.text)} · ${fmt(active.charges)} uses</span>`
${!active && drink ? `<span class="btns">Drink: ${drink}</span>` : ""} : `<span class="muted">No potion</span>`}</div>
${!active && !drink ? `<span class="small muted">Brewed in Herblore.</span>` : ""} ${others ? `<div class="swap-grid">${others}</div>` : ""}</div>`);
</div>`; }
if (`tool_${skill}` in me.equip) {
const id = me.equip[`tool_${skill}`], it = id && item(id);
const others = Object.keys(me.bank).filter((x) => item(x).tool === skill).sort((a, b) => item(b).speed - item(a).speed).map((x) => {
const [s, level] = item(x).req || [], why = s && me.levels[s] < level ? `Needs ${skillOf(s).name.toLowerCase()} ${level}` : "";
return `<button class="doll-slot swap" data-equip="${x}" ${why ? "disabled" : ""} title="${esc([`${item(x).name}: ${Math.round(item(x).speed * 100)}% faster`, why].filter(Boolean).join(" · "))}">${item(x).icon}</button>`;
}).join("");
cards.push(`<div class="panel gear-card">
<span class="doll-slot ${it ? "" : "empty"}" title="${esc(it ? it.name : "No tool")}">${it ? it.icon : skillOf(skill).icon}</span>
<div class="gear-text">${it ? `<b>${esc(it.name)}</b><span class="muted small">${Math.round(it.speed * 100)}% faster</span>` : `<span class="muted">No tool</span>`}</div>
${others || it ? `<div class="swap-grid">${others}${it ? `<button class="doll-slot swap off" data-unequip="tool_${skill}" title="Take it off">${glyph("cancel", "#98a1b4")}</button>` : ""}</div>` : ""}</div>`);
}
return cards.length ? `<div class="gear-row">${cards.join("")}</div>` : "";
} }
function prayerPanel() { function prayerPanel() {
@ -515,14 +527,6 @@ function prayerPanel() {
</div></div>`; </div></div>`;
} }
function toolPanel(skill) {
const tool = S.me.equip[`tool_${skill}`];
return `<div class="panel tool-panel">${tool
? `${item(tool).icon} <b>${esc(item(tool).name)}</b> <span class="muted">${Math.round(item(tool).speed * 100)}% faster</span>
<button class="btn sm" data-unequip="tool_${skill}">Unequip</button>`
: `<span class="muted">No tool.</span>`}</div>`;
}
// A damage type's icon: what an enemy attacks with, or (tinted) what it's vulnerable to or resists. // A damage type's icon: what an enemy attacks with, or (tinted) what it's vulnerable to or resists.
function dmgIcon(type, how = "") { function dmgIcon(type, how = "") {
const t = C.damage_types[type], name = t.name.toLowerCase(); const t = C.damage_types[type], name = t.name.toLowerCase();
@ -975,17 +979,22 @@ function happeningsPanel() {
// What's speeding up, slowing down or blocking this skill right now. // What's speeding up, slowing down or blocking this skill right now.
function skillHappenings(skill) { function skillHappenings(skill) {
const h = S.happenings, lines = []; const h = S.happenings, banners = [];
const note = (icon, name, text) => lines.push(`<span>${icon} <b>${esc(name)}</b> <span class="muted">${esc(text)}</span></span>`);
const touches = (src) => ["speed", "yield", "xp"].some((k) => src[k] && skill in src[k]) || (src.block && skill in src.block); const touches = (src) => ["speed", "yield", "xp"].some((k) => src[k] && skill in src[k]) || (src.block && skill in src.block);
if (touches(h.weather)) note(h.weather.icon, h.weather.name, h.weather.text); const good = (src) => (src.speed?.[skill] ?? 1) < 1 || (src.yield?.[skill] ?? 1) > 1 || (src.xp?.[skill] ?? 1) > 1;
for (const e of h.effects) if (touches(e)) note(e.icon, e.name, e.text); const banner = (tone, icon, name, text, button = "") => banners.push(`<div class="banner ${tone} happening-banner">
<span class="banner-ico">${icon}</span><div class="grow"><b>${esc(name)}</b><div class="small">${esc(text)}</div></div>${button}</div>`);
if (touches(h.weather)) banner(good(h.weather) ? "good" : "bad", h.weather.icon, h.weather.name, h.weather.text);
for (const e of h.effects) if (touches(e)) banner(good(e) ? "good" : "bad", e.icon, e.name, e.text);
for (const p of h.problems) { for (const p of h.problems) {
if (!C.problems[p.kind] || !touches(C.problems[p.kind])) continue; if (!C.problems[p.kind] || !touches(C.problems[p.kind])) continue;
const fix = p.action && C.actions.find((x) => x.id === p.action); const fix = p.action && C.actions.find((x) => x.id === p.action);
note(p.icon, p.name, !fix ? p.text : fix.skill === skill ? "Clear it below." : `${skillOf(fix.skill).name} clears it (Town square).`); const button = fix ? (fix.skill === skill ? `<button class="btn primary" data-start="${fix.id}">${esc(fix.name)}</button>`
: `<button class="btn" data-nav="skill:${fix.skill}">${skillOf(fix.skill).icon} ${esc(fix.name)}</button>`)
: `<button class="btn" data-nav="town">Help on the Town square</button>`;
banner("bad", p.icon, p.name, p.text, button);
} }
return lines.length ? `<div class="panel tool-panel skill-happenings">${lines.join("")}</div>` : ""; return banners.join("");
} }
function buildingCard(key, b) { function buildingCard(key, b) {

View file

@ -555,3 +555,19 @@ button.sheet-skill:hover { background: var(--panel-2); }
.doll-slot.starving { border-color: rgba(226, 100, 92, .7); background: rgba(226, 100, 92, .12); } .doll-slot.starving { border-color: rgba(226, 100, 92, .7); background: rgba(226, 100, 92, .12); }
.doll-slot.starving .gi { opacity: .9; } .doll-slot.starving .gi { opacity: .9; }
@media (max-width: 520px) { .doll-col { justify-items: center; } } @media (max-width: 520px) { .doll-col { justify-items: center; } }
/* A skill's potion and tool, side by side */
.gear-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); gap: 14px; margin-bottom: 14px; }
.gear-row .panel { margin: 0; }
.gear-card { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 12px 14px; }
.gear-card > .doll-slot { cursor: default; flex: none; }
.gear-text { display: grid; gap: 2px; min-width: 0; flex: 1; }
.gear-card .swap-grid { margin-left: auto; }
/* Happenings that touch this skill */
.happening-banner { margin-bottom: 14px; align-items: center; }
.happening-banner .banner-ico { font-size: 30px; }
.happening-banner b { font-size: 15px; }
.banner.good { border-color: rgba(95, 174, 90, .55); background: rgba(95, 174, 90, .08); }
.happening-banner { flex-wrap: wrap; }
.happening-banner .grow { min-width: min(260px, 70%); }
.happening-banner > .btn { margin-left: auto; }