Dungeon themes: per-song visual identity (crypt/overgrown/ember/flooded/windswept)

One album must not render the same dungeon twice. dungeonTheme() scores
the measured summary per theme — pace terms gate on tempoConfidence so
doubled detectors never read fast, crypt is penalised for blazing
masters — plus small seeded jitter so ambiguous songs vary across seeds
while decided races stay decided. The plan carries the theme name;
dressing mix, planting/timber/puddle densities follow it, and the facade
builds all materials (stone/floor multipliers, torch + light color, wood,
fog tint) from the same record.

Calibrated bank spread is 4/4/4/4/1 (windswept holds runner); corners
stable across seeds; plan reshaping, facade wiring, and murk-vs-clear
gates covered by tests.
This commit is contained in:
Dejvino
2026-10-08 22:20:04 +02:00
parent c96e810d44
commit 6a2af2e45f
7 changed files with 648 additions and 55 deletions
+9 -3
View File
@@ -60,7 +60,7 @@ look.venue.module = dungeon;
look.venue.name = dungeon.name;
look.venue.archetype = dungeon.archetype;
const mapSeed = mapSeedParam !== null ? Number(mapseedParam) : (look.venue.seed ?? 7);
const map = genDungeon({ seed: mapSeed });
const map = genDungeon({ seed: mapSeed, summary: track.summary });
look.venue.dungeon = { map };
// ---------------------------------------------------------- 2D map
@@ -69,6 +69,9 @@ const mc = document.getElementById('map');
const mg = mc.getContext('2d');
const S = mc.width / map.w;
document.getElementById('maptitle').textContent = `${songName} · map seed ${mapSeed} · ${map.rooms.length} rooms · route ${map.route.totalLen.toFixed(0)}u`;
const ch = map.character || {};
const chLine = `<div class="kv"><span>character</span><b>seal ${(ch.indoorBias ?? 0).toFixed(2)} · murk ${(ch.murk ?? 0).toFixed(2)} · size ×${(ch.sizeMul ?? 1).toFixed(2)}</b></div>` +
`<div class="kv"><span>theme</span><b>${map.theme ?? 'crypt'}</b></div>`;
for (let y = 0; y < map.h; y++) {
for (let x = 0; x < map.w; x++) {
if (!map.grid[y * map.w + x]) { mg.fillStyle = '#07090d'; }
@@ -109,6 +112,9 @@ for (const l of map.items.loot || []) {
mg.strokeStyle = '#fde047'; mg.lineWidth = 1; mg.beginPath();
mg.arc((l.x / 2 + map.w / 2) * S, (l.z / 2 + map.h / 2) * S, 5, 0, 7); mg.stroke();
}
for (const t of map.items.timbers || []) {
dot(t.x, t.z, '#92400e', 2);
}
if (map.portal) {
dot(map.portal.x, map.portal.z, '#f0abfc', 5);
mg.strokeStyle = '#f0abfc'; mg.lineWidth = 1.5; mg.beginPath();
@@ -123,7 +129,7 @@ mg.fillStyle = '#fff';
map.rooms.forEach((rm) => {
mg.fillText(String(rm.id), (rm.x + 0.3) * S, (rm.y + 1) * S);
});
document.getElementById('mapmeta').innerHTML =
document.getElementById('mapmeta').innerHTML = chLine +
map.rooms.map((rm) =>
`<div class="kv"><span>room ${rm.id} · ${rm.zone}${rm.indoor ? '' : ' · open'}</span><b>${rm.w}×${rm.h}${map.watch[rm.id] ? ' · ⌂' : ''}${map.portal && map.portal.roomId === rm.id ? ' · ◉' : ''}</b></div>`).join('') +
`<div class="legend">
@@ -135,7 +141,7 @@ document.getElementById('mapmeta').innerHTML =
<i style="background:#166534"></i>veg · <i style="background:#6b7280"></i>boulder ·
<i style="background:#f87171"></i>lurker · <i style="background:#fb923c"></i>sentry ·
<i style="background:#facc15"></i>critter · <i style="background:#a5f3fc"></i>wisp ·
<i style="background:#fde047"></i>loot (halt)</div></div>`;
<i style="background:#fde047"></i>loot (halt) · <i style="background:#92400e"></i>timber</div></div>`;
// ---------------------------------------------------------- 3D stills
// Fake viz feed so windows/portal/puddles show their IMAGE (the compositor