Combat rework: damage types, races with variants, armour weights, tailoring
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a35bcd6512
commit
ba7556de4d
7 changed files with 715 additions and 254 deletions
|
|
@ -439,7 +439,7 @@ function skillPage(id) {
|
|||
const open = (g) => actions.some((a) => a.group === g && !isLocked(a));
|
||||
let current = tabs[id] || remember(`tab.${id}`);
|
||||
// Tier ladders open on the highest tier you can work; categories open on the first.
|
||||
const ladder = ["smithing", "leatherworking", "herblore", "combat"].includes(id);
|
||||
const ladder = ["smithing", "leatherworking", "tailoring", "herblore", "combat"].includes(id);
|
||||
if (!groups.includes(current)) current = (ladder && [...groups].reverse().find(open)) || groups[0];
|
||||
const bar = groups.map((g) => `<button class="tab ${g === current ? "on" : ""} ${open(g) ? "" : "dim"}" data-tab="${id}" data-group="${esc(g)}">${esc(g)}</button>`).join("");
|
||||
// Ungrouped actions (the building site) stay above the tabs.
|
||||
|
|
@ -522,40 +522,57 @@ function toolPanel(skill) {
|
|||
: `<span class="muted">No tool.</span>`}</div>`;
|
||||
}
|
||||
|
||||
// What a monster is weak to and shrugs off, and how your weapon fares against it.
|
||||
function weaknessLine(kind, odds) {
|
||||
const table = C.weakness[kind];
|
||||
if (!table) return "";
|
||||
const names = (pick) => Object.entries(table).filter(([, m]) => pick(m)).map(([s]) => `${C.styles[s].name}s`).join(", ");
|
||||
const weak = names((m) => m > 1), tough = names((m) => m < 1);
|
||||
const yours = odds.weakness > 1 ? ` · <b class="good">your ${C.styles[odds.style].name} hits it hard</b>`
|
||||
: odds.weakness < 1 ? ` · <b class="bad">your ${C.styles[odds.style].name} does half damage</b>` : "";
|
||||
return `<div class="small muted">${weak ? `Weak to ${weak}` : ""}${weak && tough ? " · " : ""}${tough ? `shrugs off ${tough}` : ""}${yours}</div>`;
|
||||
// A damage type's icon: what an enemy attacks with, or (tinted) what it's vulnerable to or resists.
|
||||
function dmgIcon(type, how = "") {
|
||||
const t = C.damage_types[type], name = t.name.toLowerCase();
|
||||
const tip = how === "vuln" ? `Vulnerable to ${name} (+${Math.round((C.vulnerable - 1) * 100)}%)`
|
||||
: how === "resist" ? `Resists ${name} (-${Math.round((1 - C.resistant) * 100)}%)`
|
||||
: how === "attack" ? `Attacks with ${name}` : `${t.name} damage`;
|
||||
return `<span class="dmg ${how}" title="${esc(tip)}">${glyph(t.glyph, t.tint)}</span>`;
|
||||
}
|
||||
|
||||
function resistIcons(v) {
|
||||
return v.vulnerable.map((k) => dmgIcon(k, "vuln")).join("") + v.resists.map((k) => dmgIcon(k, "resist")).join("");
|
||||
}
|
||||
|
||||
// Who turns up in an encounter: each variant, what it attacks with, and how it takes each damage type.
|
||||
// A variant your weapon is good against is green, one that resists it red.
|
||||
function variantRows(e, o) {
|
||||
return `<div class="variants">${e.variants.map((v, i) => {
|
||||
const fit = o.variants[i].affinity;
|
||||
return `<div class="variant ${fit > 1 ? "good" : fit < 1 ? "bad" : ""}">
|
||||
<span class="v-attack">${v.attack.map((k) => dmgIcon(k, "attack")).join("")}</span>
|
||||
<span class="v-name">${esc(v.name)}${v.archetype === "elite" ? ` <span class="muted">· rare</span>` : ""}</span>
|
||||
<span class="v-hp muted">${fmt(v.hp)} HP</span>
|
||||
<span class="v-res">${resistIcons(v)}</span>
|
||||
</div>`;
|
||||
}).join("")}</div>`;
|
||||
}
|
||||
|
||||
// The fight in progress, replayed from the server's swing-by-swing record.
|
||||
function fightPanel() {
|
||||
const f = S.me.action?.fight;
|
||||
if (!f) return "";
|
||||
const side = (who, name) => `<div class="fighter">
|
||||
<div class="fighter-name"><b>${esc(name)}</b><span class="small muted" data-f="${who}-hp"></span></div>
|
||||
const side = (who, name, foe) => `<div class="fighter">
|
||||
<div class="fighter-name"><b>${esc(name)}</b>${foe ? `<span class="v-res">${resistIcons(foe)}</span>` : ""}<span class="small muted" data-f="${who}-hp"></span></div>
|
||||
<div class="bar hp ${who}"><span data-f="${who}-bar"></span></div>
|
||||
<div class="bar swing"><span data-f="${who}-swing"></span></div>
|
||||
</div>`;
|
||||
return `<div class="panel fight" data-fight="${S.me.action.start}">
|
||||
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy)}</div>
|
||||
<div class="fighters">${side("you", S.me.name)}<span class="vs">⚔️</span>${side("foe", f.enemy, f.foe)}</div>
|
||||
<ul class="fight-log" data-f="log"></ul>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function fightLine(e, enemy) {
|
||||
const [, who, kind, amount, , , food] = e;
|
||||
const type = kind === "hit" || kind === "crit" ? (C.damage_types[food] ? ` ${dmgIcon(food)}` : "") : "";
|
||||
const foe = esc(enemy);
|
||||
if (kind === "eat") return `<li class="good">You eat ${esc(item(food).name.toLowerCase())} (+${amount})</li>`;
|
||||
if (kind === "wind") return `<li class="good">Second wind! Back on your feet</li>`;
|
||||
if (kind === "block") return `<li class="good">You block ${foe}'s blow</li>`;
|
||||
if (who === "you") return kind === "miss" ? `<li class="muted">You miss</li>` : `<li>You ${kind === "crit" ? "<b>crit</b>" : "hit"} for <b>${amount}</b></li>`;
|
||||
return kind === "miss" ? `<li class="muted">${foe} misses</li>` : `<li class="bad">${foe} ${kind === "crit" ? "<b>crits</b>" : "hits"} you for <b>${amount}</b></li>`;
|
||||
if (who === "you") return kind === "miss" ? `<li class="muted">You miss</li>` : `<li>You ${kind === "crit" ? "<b>crit</b>" : "hit"} for <b>${amount}</b>${type}</li>`;
|
||||
return kind === "miss" ? `<li class="muted">${foe} misses</li>` : `<li class="bad">${foe} ${kind === "crit" ? "<b>crits</b>" : "hits"} you for <b>${amount}</b>${type}</li>`;
|
||||
}
|
||||
|
||||
function animateFight(now) {
|
||||
|
|
@ -605,16 +622,16 @@ function styleNote(me) {
|
|||
const own = C.class_style[me.cls], traits = {
|
||||
sword: `${st.interval}s${me.equip.shield ? `, shield blocks ${Math.round(C.block_chance * 100)}%` : ""}`,
|
||||
hammer: `${st.interval}s, double crits`,
|
||||
staff: `${st.interval}s, accurate, ignores armour`,
|
||||
staff: `${st.interval}s, accurate`,
|
||||
bow: `${st.interval}s, shoots first`,
|
||||
};
|
||||
return `<p class="small">${weapon ? st.name[0].toUpperCase() + st.name.slice(1) : "Fists"}: ${traits[styleId]}.
|
||||
return `<p class="small">${dmgIcon(st.damage)} ${weapon ? st.name[0].toUpperCase() + st.name.slice(1) : "Fists"}: ${C.damage_types[st.damage].name.toLowerCase()}, ${traits[styleId]}.
|
||||
${own === styleId ? `<b class="good">Trade weapon: +15% damage.</b>` : `Trade weapon: ${esc(C.styles[own].name)} (+15%).`}</p>`;
|
||||
}
|
||||
|
||||
function combatPanel() {
|
||||
const me = S.me;
|
||||
const stat = (id) => [item(id).power && `+${item(id).power} power`, item(id).armour && `+${item(id).armour} armour`].filter(Boolean).join(", ");
|
||||
const stat = (id) => gearStats(item(id));
|
||||
const slot = (name, label) => {
|
||||
const id = me.equip[name];
|
||||
return `<div class="gear-slot">${id
|
||||
|
|
@ -636,11 +653,17 @@ function combatPanel() {
|
|||
</div>
|
||||
<div class="panel"><h2>Equipment</h2>
|
||||
<div class="gear">${slot("weapon", "weapon")}${slot("shield", "shield")}${slot("helmet", "helmet")}${slot("body", "body armour")}${slot("legs", "leg armour")}</div>
|
||||
<p class="small muted">Power ${me.power} · armour ${me.armour} (-${Math.round(me.reduction * 100)}% damage)</p>
|
||||
<p class="small muted">Power ${me.power} · armour ${me.armour} (-${Math.round(me.reduction * 100)}% physical) · ward ${me.ward} (-${Math.round(me.ward_reduction * 100)}% magic)</p>
|
||||
${styleNote(me)}
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// Power, armour and ward, and the armour's weight.
|
||||
function gearStats(it) {
|
||||
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`, it.ward && `+${it.ward} ward`];
|
||||
return stats.filter(Boolean).join(", ") + (it.weight ? ` (${C.weights[it.weight].name.toLowerCase()})` : "");
|
||||
}
|
||||
|
||||
// What an item does once made: healing, defence, tool speed, and who can use it.
|
||||
function itemStats(id) {
|
||||
const it = item(id);
|
||||
|
|
@ -652,7 +675,8 @@ function itemStats(id) {
|
|||
const st = C.styles[it.style], trade = Object.entries(C.class_style).find(([, s]) => s === it.style)?.[0];
|
||||
parts.push(`${it.two_handed ? "two-handed " : ""}${st.name}, ${st.interval}s${trade ? ` (${C.classes[trade].name.toLowerCase()})` : ""}`);
|
||||
}
|
||||
if (it.armour) parts.push(`+${it.armour} armour`);
|
||||
if (it.armour || it.ward) parts.push(gearStats({ ...it, power: 0 }));
|
||||
if (it.reagent) parts.push(`Reagent for ${C.weights[it.reagent].name.toLowerCase()} armour`);
|
||||
if (it.tool) parts.push(`${Math.round(it.speed * 100)}% faster ${skillOf(it.tool).name.toLowerCase()}`);
|
||||
if (it.req && it.req[1] > 1) parts.push(`needs ${skillOf(it.req[0]).name.toLowerCase()} ${it.req[1]}`);
|
||||
if (it.potion) parts.push(`${it.potion.text}, ${C.potion_charges} uses`);
|
||||
|
|
@ -685,10 +709,10 @@ function actionCard(a) {
|
|||
let extra = "";
|
||||
if (a.enemy) {
|
||||
const o = me.fight_odds[a.id], e = a.enemy, pct = (x) => `${Math.round(x * 100)}%`;
|
||||
extra = `<div class="small">${HEART} ${fmt(e.hp)} HP · hits up to <b>${o.their_max}</b> every ${e.interval}s${e.boss ? ` · <span class="tag warn">Boss</span>` : ""}</div>
|
||||
extra = `${variantRows(e, o)}
|
||||
<div class="small">Hits up to <b>${o.their_max}</b>${e.boss ? ` · <span class="tag warn">Boss</span>` : ""}</div>
|
||||
<div class="small">You hit <b>${pct(o.hit)}</b> (max ${o.max_hit}) · it hits <b>${pct(o.their_hit)}</b></div>
|
||||
<div class="small">~<b>${dur(o.kill_time)}</b> a kill · <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>
|
||||
${weaknessLine(e.kind, o)}`;
|
||||
<div class="small">~<b>${dur(o.kill_time)}</b> a kill · <b>${Math.round(o.damage)}</b> damage taken${a.gold ? ` · ${a.gold} gold` : ""}</div>`;
|
||||
if (a.drops) io += a.drops.map(([id, q, odds]) => `<span class="item-chip" title="${esc(item(id).name)}${q > 1 ? ` ×${q}` : ""}: ${Math.round(odds * 100)}% chance">${item(id).icon} ${Math.round(odds * 100)}%</span>`).join("");
|
||||
if (a.special === "sabotage") extra += `<div class="small muted">-3% raid strength per win (max 30%)</div>`;
|
||||
}
|
||||
|
|
@ -811,7 +835,7 @@ function equipRow(id, it) {
|
|||
|
||||
function equipLabel(it) {
|
||||
if (it.tool) return `Use as ${skillOf(it.tool).name.toLowerCase()} tool (${Math.round(it.speed * 100)}% faster)`;
|
||||
const stats = [it.power && `+${it.power} power`, it.armour && `+${it.armour} armour`].filter(Boolean).join(", ");
|
||||
const stats = gearStats(it);
|
||||
return `Equip ${it.slot === "body" ? "body armour" : it.slot === "legs" ? "leg armour" : it.slot} (${stats})`;
|
||||
}
|
||||
|
||||
|
|
@ -1147,7 +1171,7 @@ function characterSheet(uid) {
|
|||
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(", ");
|
||||
: 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>
|
||||
|
|
@ -1183,7 +1207,8 @@ function characterSheet(uid) {
|
|||
<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>
|
||||
${mine ? `<div class="stat"><div class="k">Prayer points</div><div class="v">${fmt(p.pp)}</div><div class="note">${p.prayers.length ? p.prayers.map((id) => esc(C.prayers.find((x) => x.id === id).name)).join(", ") : "No prayers active"}</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">Armour</div><div class="v">${p.armour}</div><div class="note">${Math.round(p.reduction * 100)}% less physical</div></div>
|
||||
<div class="stat"><div class="k">Ward</div><div class="v">${p.ward}</div><div class="note">${Math.round(p.ward_reduction * 100)}% less magic</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">${p.slots_used} / ${p.slots} bank slots</div></div>` : ""}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue