Files
Dejvino 6a2af2e45f 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.
2026-10-08 22:20:04 +02:00

232 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>flow-state · dungeon debug</title>
<style>
:root { color-scheme: dark; }
body { margin: 0; padding: 20px 24px 60px; background:#0b0d12; color:#d6dae3; font: 13px/1.5 ui-monospace, monospace; }
h1 { font-size:15px; color:#7d8698; margin:0 0 4px; }
h2 { font-size:12px; color:#6b7280; margin:22px 0 8px; text-transform:uppercase; letter-spacing:.1em; }
#status { color:#8b94a7; margin: 8px 0 16px; }
#status b { color:#e6eaf2; }
.wrap { display:flex; gap:16px; flex-wrap:wrap; align-items:flex-start; }
#map { background:#05070a; border:1px solid #1c2030; image-rendering:pixelated; }
#mapmeta { max-width:340px; }
#mapmeta .kv { display:flex; justify-content:space-between; gap:12px; padding:1px 0; }
#mapmeta .kv span { color:#8b94a7; }
.legend { margin-top:10px; font-size:12px; color:#8b94a7; }
.legend i { display:inline-block; width:10px; height:10px; margin-right:5px; vertical-align:baseline; }
.grid { display:grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap:10px; }
.card { background:#11141b; border:1px solid #1c2030; border-radius:4px; padding:8px; }
.card h3 { margin:0 0 4px; font-size:12px; color:#e6eaf2; }
.card h3 .mode-walk { color:#7dd3fc; } .card h3 .mode-room { color:#a3e635; } .card h3 .mode-portal { color:#f0abfc; }
canvas.still { width:100%; display:block; background:#05070a; aspect-ratio:16/9; }
a { color:#7dd3fc; }
</style>
</head>
<body>
<h1>flow-state · dungeon debug</h1>
<div id="status">starting…</div>
<h2>map — <span id="maptitle"></span></h2>
<div class="wrap">
<canvas id="map" width="440" height="440"></canvas>
<div id="mapmeta"></div>
</div>
<h2>stills — song timeline, both camera modes</h2>
<div id="out" class="grid"></div>
<script type="module">
import { Engine } from '/src/engine/Engine.js';
import { createLayer } from '/src/engine/Layer.js';
import { song } from '/src/audio/songbank.js';
import { generateLook } from '/src/look/LookGenerator.js';
import { venues } from '/src/venues/registry.js';
import { dungeon } from '/src/venues/dungeon.js';
import { genDungeon } from '/src/venues/dungeon/map.js';
import { hashString } from '/src/engine/rng.js';
import { blit } from '/src/checks/blit.js';
import * as THREE from 'three';
const out = document.getElementById('out');
const status = document.getElementById('status');
const params = new URLSearchParams(location.search);
const songName = params.get('song') || 'ember';
const mapSeedParam = params.get('mapseed');
const TIMES = (params.get('times') || '5,25,45,65,85,105').split(',').map(Number);
const { track } = song(songName);
const look = generateLook(track, { seed: hashString(songName), venueEnabled: true });
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, summary: track.summary });
look.venue.dungeon = { map };
// ---------------------------------------------------------- 2D map
const ZC = { hall: '#232b3d', room: '#2b3450', forest: '#14301c', wallwalk: '#33302a' };
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'; }
else {
const r = map.rooms.find((rm) => x >= rm.x && x < rm.x + rm.w && y >= rm.y && y < rm.y + rm.h);
mg.fillStyle = r ? (ZC[r.zone] || '#222') : '#1a2030';
}
mg.fillRect(x * S, y * S, S, S);
}
}
// Route.
mg.strokeStyle = '#f5b942'; mg.lineWidth = 2; mg.beginPath();
map.route.pts.forEach((p, i) => {
const cx = (p.x / 2 + map.w / 2) * S, cy = (p.z / 2 + map.h / 2) * S;
if (i === 0) mg.moveTo(cx, cy); else mg.lineTo(cx, cy);
});
mg.closePath(); mg.stroke();
const dot = (x, z, color, r = 3) => {
mg.fillStyle = color; mg.beginPath();
mg.arc((x / 2 + map.w / 2) * S, (z / 2 + map.h / 2) * S, r, 0, 7); mg.fill();
};
for (const p of map.items.props) dot(p.x, p.z, '#8b94a7', 2);
for (const p of map.items.puddles) dot(p.x, p.z, '#38bdf8', 3);
for (const p of map.items.torches) dot(p.x, p.z, '#fb923c', 2);
for (const p of map.items.windows) dot(p.x, p.z, '#22d3ee', 2);
for (const p of map.items.gates) dot(p.x, p.z, '#ef4444', 3);
for (const v of map.items.vegetation || []) {
dot(v.x, v.z, v.kind === 'boulder' ? '#6b7280' : '#166534', 2);
}
const ECOL = { lurker: '#f87171', sentry: '#fb923c', critter: '#facc15', wisp: '#a5f3fc' };
for (const e of map.items.entities || []) {
dot(e.x, e.z, ECOL[e.kind] || '#f87171', 3);
mg.fillStyle = '#0b0d12';
mg.fillText(e.kind[0].toUpperCase(), (e.x / 2 + map.w / 2) * S - 3, (e.z / 2 + map.h / 2) * S + 3);
}
for (const l of map.items.loot || []) {
dot(l.x, l.z, '#fde047', 3);
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();
mg.arc((map.portal.x / 2 + map.w / 2) * S, (map.portal.z / 2 + map.h / 2) * S, 7, 0, 7); mg.stroke();
}
mg.fillStyle = '#a3e635';
for (const w of Object.values(map.watch)) {
const cx = (w.x / 2 + map.w / 2) * S, cy = (w.z / 2 + map.h / 2) * S;
mg.fillRect(cx - 3, cy - 3, 6, 6);
}
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 = 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">
<div><i style="background:#f5b942"></i>route (closed tour)</div>
<div><i style="background:#a3e635"></i>watch cell (room camera plant)</div>
<div><i style="background:#f0abfc"></i>portal · <i style="background:#ef4444"></i>gate ·
<i style="background:#fb923c"></i>torch · <i style="background:#22d3ee"></i>window ·
<i style="background:#38bdf8"></i>puddle · <i style="background:#8b94a7"></i>prop ·
<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) · <i style="background:#92400e"></i>timber</div></div>`;
// ---------------------------------------------------------- 3D stills
// Fake viz feed so windows/portal/puddles show their IMAGE (the compositor
// would bind the real viz texture; the lab stands in a test pattern).
function fakeViz() {
const c = document.createElement('canvas');
c.width = 256; c.height = 144;
const g = c.getContext('2d');
const grad = g.createLinearGradient(0, 0, 256, 144);
grad.addColorStop(0, '#0ff'); grad.addColorStop(0.5, '#80f'); grad.addColorStop(1, '#f0f');
g.fillStyle = grad; g.fillRect(0, 0, 256, 144);
g.fillStyle = '#fff';
for (let i = 0; i < 24; i++) g.fillRect((i * 53) % 256, (i * 97) % 144, 20, 8);
return c;
}
const vizCanvas = fakeViz();
const engine = new Engine({ width: 480, height: 270 });
const layer = createLayer(dungeon, {
params: look.venue.params || {},
seed: look.venue.seed ?? 7,
opacity: 1, blend: 'normal',
actorSpec: look.actors?.monolith || null,
venueSpec: look.venue,
});
layer.setPalette(look.palette);
layer.setPersonality(look.personality);
if (layer.setActor && look.actors?.monolith) layer.setActor(look.actors.monolith);
if (layer.setVenue) layer.setVenue(look.venue);
engine.compositor.setLayers([layer]);
function bindFakeViz() {
const tex = new THREE.CanvasTexture(vizCanvas);
dungeon.bindVizTexture(layer.instance, tex);
}
bindFakeViz();
const fps = track.fps || 60;
const modes = [
{ target: 'walk', label: 'walk · first-person (actor hidden)' },
{ target: 'room', label: 'room · plant watches actor' },
];
for (const t of TIMES) {
const frame = Math.round(t * fps);
const feat = track.at ? track.at(frame) : {};
const tl = { frame, time: frame / fps, progress: frame / (track.frameCount || 1) };
for (const m of modes) {
layer.setFraming({
scale: 1, shift: [0, 0],
venueShot: { target: m.target, pos: [0, 1, 2], look: [0, 0.4, -5], size: 'normal', dolly: 1.0 },
});
for (let k = 0; k < 4; k++) {
layer.render(engine.renderer, engine.compositor.layerTarget, {
timeline: { ...tl, frame: frame - (3 - k), time: (frame - (3 - k)) / fps },
features: feat, prevTexture: null,
});
}
layer.render(engine.renderer, engine.compositor.layerTarget, {
timeline: tl, features: feat, prevTexture: null,
});
const pix = Uint8Array.from(engine.readPixels(engine.compositor.layerTarget));
const card = document.createElement('div');
card.className = 'card';
// Where the actor actually is: nearest route sample to the built hero.
const hp = layer.instance.hero.position;
let bi = 0, bd = Infinity;
map.route.pts.forEach((p, i) => {
const d = Math.hypot(p.x - hp.x, p.z - hp.z);
if (d < bd) { bd = d; bi = i; }
});
const rid = map.route.roomIds[bi];
const zn = map.route.zones[bi];
card.innerHTML = `<h3>t=${t}s · <span class="mode-${m.target}">${m.target}</span> · ` +
(rid >= 0 ? `room ${rid} (${zn})` : `corridor (${zn})`) + `</h3>`;
const cv = document.createElement('canvas');
cv.width = 480; cv.height = 270; cv.className = 'still';
card.appendChild(cv); out.appendChild(card);
blit(cv, pix, 480, 270);
}
}
status.innerHTML = `done — <b>${songName}</b> · map seed <b>${mapSeed}</b> · ` +
`${map.rooms.length} rooms · route ${map.route.totalLen.toFixed(0)}u · ` +
`${TIMES.length * modes.length} stills`;
window.__DUNGEON_DEBUG_DONE__ = true;
</script>
</body>
</html>