Dungeon loot: patterns, masterworks, relics, uniques

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-10-24 13:48:16 +01:00
parent 60b446acfd
commit 8afcba79fc
8 changed files with 358 additions and 18 deletions

View file

@ -888,7 +888,7 @@ function variantRows(e, o) {
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-name">${esc(v.name)}${v.archetype === "elite" && !e.boss ? ` <span class="muted">· rare</span>` : ""}</span>
<span class="v-hp muted">${fmt(v.hp)} HP</span>
<span class="v-res">${resistIcons(v)}</span>
</div>`;
@ -924,7 +924,7 @@ function combatFrame() {
const traits = (v) => `<span class="v-attack">${v.attack.map((k) => dmgIcon(k, "attack")).join("")}</span>
<span class="v-res">${v.vulnerable.map((k) => `<span class="${k === mine ? "yours" : ""}">${dmgIcon(k, "vuln")}</span>`).join("")}${v.resists.map((k) => `<span class="${k === mine ? "yours" : ""}">${dmgIcon(k, "resist")}</span>`).join("")}</span>`;
foe = `<div class="foe-head"><span class="foe-ico">${encounter?.icon || ""}</span><b>${esc(f.enemy)}</b></div>
<div class="pack">${f.pack.map((v, i) => unit(`e${i}`, `${esc(v.name)}${v.archetype === "elite" ? ` <span class="muted">· rare</span>` : ""}`, true,
<div class="pack">${f.pack.map((v, i) => unit(`e${i}`, `${esc(v.name)}${v.archetype === "elite" && !v.boss ? ` <span class="muted">· rare</span>` : ""}`, true,
` <span class="traits-inline">${traits(v)}</span>`)).join("")}</div>
<ul class="fight-log" data-f="log"></ul>`;
}
@ -943,9 +943,11 @@ function abilityBar(me) {
const tile = (id, on) => {
const ab = C.abilities[id], manual = on && me.manual.includes(id);
const why = on ? "" : ab.needs === "shield" && !shield && me.levels.combat >= ab.level ? "needs a shield" : `combat ${ab.level}`;
const cd = ab.cd ? ` · ${manual ? ab.cd : ab.cd * C.auto_cd}s cooldown` : "";
const tip = `${ab.name}: ${ab.tip}${cd}${why ? ` · ${why}` : ""}`;
return `<span class="ab ${on ? (manual ? "manual" : "auto") : "locked"}" data-ab="${id}">
const base = (ab.cd || 0) * (me.fighter.mods?.[id]?.cd ?? 1);
const cd = base ? ` · ${+(manual ? base : base * C.auto_cd).toFixed(1)}s cooldown` : "";
const boosts = (me.fighter.boosts?.[id] || []).map((n) => ` · ✦ ${n}`).join("");
const tip = `${ab.name}: ${ab.tip}${cd}${boosts}${why ? ` · ${why}` : ""}`;
return `<span class="ab ${on ? (manual ? "manual" : "auto") : "locked"} ${boosts && on ? "boosted" : ""}" data-ab="${id}">
<button class="ab-press" data-press="${id}" title="${esc(tip)}" ${on ? "" : "disabled"}>${ab.icon}<span class="ab-cd" data-ab-cd></span>
${on ? "" : `<span class="ab-lv">${why === "needs a shield" ? glyph("checked-shield", "#d4dae2") : ab.level}</span>`}</button>
${on ? `<button class="ab-mode" data-mode="${id}" title="${esc(manual ? `Yours: press it while you watch. Look away ${C.watch}s and it's back on auto. Tap for auto.`
@ -1027,7 +1029,8 @@ function fightState(f, t) {
if (who && SWINGS.has(kind) && (label === "" || C.damage_types[label])) who.swung = time;
if (who && kind === "cast") {
const spec = src[0] === "e" ? C.enemy_abilities[label] : C.abilities[label];
who.cast = { label, from: time, until: time + spec.cast / f.pace };
const mod = src[0] === "e" ? 1 : f.members.find((m) => m.id === src)?.mods?.[label]?.cast ?? 1;
who.cast = { label, from: time, until: time + spec.cast * mod / f.pace };
}
const ab = C.abilities[label];
if (who?.cds && ab && (ab.cast ? kind === "cast" : kind === "use" || SWINGS.has(kind)) && who.cds[label] !== time) {
@ -1101,7 +1104,7 @@ function animateFight(now) {
const id = el.dataset.ab;
if (id === "potion") { el.classList.toggle("spent", !!me.drank); continue; }
const ab = C.abilities[id], used = me.cds?.[id];
const cd = (ab.cd || 0) * (me.manualAt?.[id] ? 1 : C.auto_cd) / f.pace;
const cd = (ab.cd || 0) * (mine.mods?.[id]?.cd ?? 1) * (me.manualAt?.[id] ? 1 : C.auto_cd) / f.pace;
const leftCd = used == null || over ? 0 : Math.max(0, used + cd - t);
el.querySelector("[data-ab-cd]").style.height = `${cd ? leftCd / cd * 100 : 0}%`;
el.classList.toggle("fired", used != null && t - used < 0.6);
@ -1168,7 +1171,16 @@ function gearChips(it, who = S.me) {
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>`;
return `<div class="stat-grid">${row.join("")}${req}<span></span>${bonus || "<span></span>"}</div>
${it.unique ? `<div class="small unique-text">✦ ${esc(it.unique)}</div>` : ""}`;
}
// A pattern: what it teaches, and the button to learn it (used up).
function patternRow(id, it) {
const made = item(it.pattern), skill = skillOf(it.skill), known = (S.me.patterns || []).includes(id);
const short = S.me.levels[it.skill] < it.level;
return `<div class="row"><div class="small">Teaches ${made.icon} <b>${esc(made.name)}</b> · ${skill.icon} ${skill.name} ${it.level}</div>
${known ? `<span class="tag good">Known</span>` : `<button class="btn sm gold" data-learn="${id}" ${short ? `disabled title="Needs ${esc(skill.name.toLowerCase())} ${it.level}"` : ""}>Learn</button>`}</div>`;
}
// What an item does once made: healing, defence, tool speed, and who can use it.
@ -1227,6 +1239,12 @@ function actionCard(a) {
extra = `${e.boss || own ? `<div>${e.boss ? `<span class="tag warn">Boss</span> ` : ""}${own}</div>` : ""}${variantRows(e, o)}${fightingHere(a, why)}`;
if (a.gold) io += `<span class="item-chip" title="Gold: ${a.gold[0]}–${a.gold[1]} a kill">${COIN} ${a.gold[0]}–${a.gold[1]}</span>`;
if (a.drops) io += a.drops.map(chance).join("");
if (a.loot) {
const l = a.loot, pct = (x) => `${Math.round(x * 100)}%`;
io += `<span class="item-chip" title="${esc(`${item(l.relic).name}: from every boss, to reforge masterworks`)}">${item(l.relic).icon}</span>`
+ `<span class="item-chip" title="${esc(`Patterns (${l.patterns.length}): ${l.patterns.map((p) => item(item(p).pattern).name).join(", ")}. ${pct(l.odds.pattern[0])} from a mini-boss, ${pct(l.odds.pattern[1])} from the last boss`)}">${item(l.patterns[0]).icon} ${l.patterns.length}</span>`
+ `<span class="item-chip" title="${esc(`${item(l.unique).name}: ${item(l.unique).unique}`)}">${item(l.unique).icon} ${pct(l.odds.unique)}</span>`;
}
if (a.special === "sabotage") extra += `<div class="small muted">-1% raid strength per win (max 30%)</div>`;
}
if (a.special === "drill") {
@ -1359,6 +1377,8 @@ function itemPanel(id) {
${it.heal ? `<div class="row"><div class="small muted">Heals ${it.heal} resting between fights.</div>
${S.me.food === id ? `<span class="tag good">Your fighting food</span>` : `<button class="btn sm" data-food="${id}">Eat this first</button>`}</div>` : ""}
${it.drink ? `<div class="row small muted">+${it.drink} HP in a fight, drunk once a fight under ${Math.round(C.potion_below * 100)}% HP.</div>` : ""}
${it.pattern ? patternRow(id, it) : ""}
${it.relic ? `<div class="row small muted">Reforges masterworks with a pattern from ${esc(C.actions.find((a) => a.id === it.relic).name)}.</div>` : ""}
${it.food ? `<div class="row small muted">Feeds ${it.food} townsfolk for 2 days.</div>` : ""}
${C.city_uses[id].length
? `<div class="row"><div class="k">Donate to the city · +${it.value} favour each</div><div class="btns">${qtyBtns("data-donate")}</div>
@ -2280,6 +2300,7 @@ document.addEventListener("click", (e) => {
}
if (d.mode) return act("/api/manual", { id: d.mode, on: !S.me.manual.includes(d.mode) });
if ("pull" in d) return act("/api/pull", {});
if (d.learn) return act("/api/learn", { item: d.learn });
if (d.aim) return d.aim === S.uid ? undefined : act("/api/target", { id: d.aim });
if (d.start) return act("/api/action", { id: d.start });
if (d.once) return act("/api/action", { id: d.once, once: true });