pull AtlasLoot rares + interactives into kg pixel space

- new tools/atlasloot_extras.py: matches AL bosses to kg cls>=3 enemies by name, fits per-(dungeon,wing) affine x/y transform, applies to AL pinType=None entries to recover ~14 rare bosses kg's spawn-bound data model omits (Blind Hunter, Stonespine, Deathsworn Captain, Spirestone Butcher, Bannok Grimaxe, Jed Runewatcher, Tsu'zee, …) plus 140+ AtlasLoot interactives (postboxes, summon stones, levers)
- WING_FORCE map disambiguates multi-wing dungeons (BRS, Dire Maul, Scarlet Monastery) where the same AL coord transforms into multiple wings
- frontend renders rare extras as silver-blue skull pins, non-rare extras as muted squares; both have hover-tooltips with their AtlasLoot name
- start, graveyard, dot_yellow, gateway, door_locked icon types from kg now render with distinct shapes (were silently empty before)
- kg_build_data.py merges atlasloot_extras.json into each dungeon's 'extras' field

Note: Ascension always-spawns rare bosses (vs retail's RNG), so they're now reliably visible on the planner.
This commit is contained in:
2026-04-25 23:18:50 +02:00
parent 9600ce35c2
commit 724ae08394
5 changed files with 2699 additions and 4 deletions
+89 -2
View File
@@ -194,6 +194,17 @@ function renderOverlay() {
}
}
// AtlasLoot-derived extras: rare bosses + interactives kg doesn't ship.
// These are dungeon-level (not per-floor) and only render on floor 0
// unless we get richer data; for single-floor dungeons that's all that
// matters, and for multi-floor we leave them on the first floor by
// default — the user can drag a Note over them on any floor.
if (state.floorIndex === 0 && state.current?.extras) {
for (const ex of state.current.extras) {
svg.appendChild(makeExtraPin(ex));
}
}
// Route polyline
if (wps.length > 1) {
const path = document.createElementNS(SVG_NS, "polyline");
@@ -419,6 +430,42 @@ function makeEnemyPin(e) {
return g;
}
function makeExtraPin(ex) {
// Silver-blue ring with skull glyph for rares; muted square for non-rare
// interactives (postboxes, summon spots, etc.). Strip the trailing
// "(Rare)" / "(Rare, Wanders)" tag from the displayed tooltip — we
// surface "rare" via the visual treatment.
const g = document.createElementNS(SVG_NS, "g");
g.setAttribute("class", ex.rare ? "extra rare" : "extra");
g.setAttribute("transform", `translate(${ex.pos[0]},${ex.pos[1]})`);
if (ex.rare) {
const c = document.createElementNS(SVG_NS, "circle");
c.setAttribute("r", 26);
c.setAttribute("fill", "#bfd6f0");
c.setAttribute("stroke", "#3b6db0");
c.setAttribute("stroke-width", "5");
g.appendChild(c);
const t = document.createElementNS(SVG_NS, "text");
t.setAttribute("y", 11);
t.setAttribute("font-size", "30");
t.setAttribute("text-anchor", "middle");
t.setAttribute("fill", "#1a3360");
t.setAttribute("font-weight", "900");
t.textContent = "☠";
g.appendChild(t);
} else {
const r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", -10); r.setAttribute("y", -10);
r.setAttribute("width", 20); r.setAttribute("height", 20);
r.setAttribute("fill", "#7e8290");
r.setAttribute("stroke", "#1a1208");
r.setAttribute("stroke-width", "2");
g.appendChild(r);
}
g.dataset.tooltip = ex.name + (ex.rare ? "" : "");
return g;
}
function makeIconMarker(ic) {
const g = document.createElementNS(SVG_NS, "g");
g.setAttribute("class", `icon icon-${ic.type}`);
@@ -438,14 +485,54 @@ function makeIconMarker(ic) {
t.setAttribute("font-weight", "700");
t.textContent = "i";
g.appendChild(t);
} else if (ic.type === "door") {
} else if (ic.type === "door" || ic.type === "door_locked") {
const r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", -10); r.setAttribute("y", -14);
r.setAttribute("width", 20); r.setAttribute("height", 28);
r.setAttribute("fill", "#B58A3F");
r.setAttribute("fill", ic.type === "door_locked" ? "#7a4a1a" : "#B58A3F");
r.setAttribute("stroke", "#000");
r.setAttribute("stroke-width", "2");
g.appendChild(r);
} else if (ic.type === "start") {
// Green entry-flag triangle
const p = document.createElementNS(SVG_NS, "polygon");
p.setAttribute("points", "-12,-14 14,0 -12,14");
p.setAttribute("fill", "#6ad17b");
p.setAttribute("stroke", "#0a2a12");
p.setAttribute("stroke-width", "2");
g.appendChild(p);
} else if (ic.type === "graveyard") {
// Tombstone: rounded-top rect with a cross
const r = document.createElementNS(SVG_NS, "rect");
r.setAttribute("x", -12); r.setAttribute("y", -14);
r.setAttribute("width", 24); r.setAttribute("height", 28);
r.setAttribute("rx", 10); r.setAttribute("ry", 10);
r.setAttribute("fill", "#9aa1aa");
r.setAttribute("stroke", "#000");
r.setAttribute("stroke-width", "2");
g.appendChild(r);
const t = document.createElementNS(SVG_NS, "text");
t.setAttribute("y", 6);
t.setAttribute("font-size", "20");
t.setAttribute("text-anchor", "middle");
t.setAttribute("fill", "#1a1208");
t.setAttribute("font-weight", "900");
t.textContent = "✝";
g.appendChild(t);
} else if (ic.type === "dot_yellow") {
const c = document.createElementNS(SVG_NS, "circle");
c.setAttribute("r", 10);
c.setAttribute("fill", "#f0c674");
c.setAttribute("stroke", "#1a1208");
c.setAttribute("stroke-width", "2");
g.appendChild(c);
} else if (ic.type === "gateway") {
const c = document.createElementNS(SVG_NS, "circle");
c.setAttribute("r", 14);
c.setAttribute("fill", "#9b59b6");
c.setAttribute("stroke", "#000");
c.setAttribute("stroke-width", "2");
g.appendChild(c);
}
if (ic.comment) {
g.dataset.tooltip = ic.comment;
File diff suppressed because it is too large Load Diff