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
+41 -1
View File
@@ -265,7 +265,7 @@ function awaitImportThree() {
test('dungeon declares directCamera and plants its cameras on open cells', () => {
const THREE = awaitImportThree();
return THREE.then((T) => {
return THREE.then(async (T) => {
const dungeonMod = venues.find((v) => v.name === 'Dungeon');
assert.ok(dungeonMod, 'Dungeon venue not registered');
assert.equal(dungeonMod.directCamera, true, 'Dungeon must own its camera rig');
@@ -383,5 +383,45 @@ test('dungeon declares directCamera and plants its cameras on open cells', () =>
assert.ok(Math.abs(h - en.height) < 0.05,
`${en.spec.kind} stands ${h.toFixed(2)} tall, not ${en.height}`);
}
// Murk dims the venue: the same update on a murky map must read
// darker lights and closer fog than on a clear one.
{
const { genDungeon } = await import('../src/venues/dungeon/map.js');
const murky = genDungeon({ seed: 4242, summary: { bpm: 70, meanCentroid: 0.05, meanLoudness: 0.008 } });
const clear = genDungeon({ seed: 4242, summary: { bpm: 170, meanCentroid: 0.6, meanLoudness: 0.001 } });
assert.ok(murky.character.murk > clear.character.murk + 0.3, 'murk spread too small');
const buildMurky = (map) => {
const sc = new T.Scene();
const spec = { ...dungeonSpec, seed: 6, dungeon: { map } };
const inst = dungeonMod.build({
scene: sc, seed: 4242, params: {}, actorSpec: actors.monolith,
venueSpec: spec, THREE: T,
});
const cam = stubCam();
dungeonMod.update({
instance: inst, scene: sc, camera: cam,
timeline: { time: 10, frame: 600, progress: 0.05 },
features: {}, params: { light: 0.85, speed: 0.7 },
palette: [[1, 0.9, 0.8], [0.5, 0.5, 0.7], [0.3, 0.4, 0.9], [0.9, 0.5, 0.4]],
personality, framing, opacity: 1,
actorSpec: actors.monolith,
venueSpec: spec,
venueState: { light: 0.7, fog: 0.35, popMul: 0.8, reachMul: 0.9 },
THREE: T,
});
return { inst, fogNear: sc.fog ? sc.fog.near : Infinity };
};
const murkyBuilt = buildMurky(murky);
const clearBuilt = buildMurky(clear);
const murkyInst = murkyBuilt.inst, clearInst = clearBuilt.inst;
assert.ok(murkyInst.ambient.intensity < clearInst.ambient.intensity,
'murk must dim ambient');
assert.ok(murkyInst.key.intensity < clearInst.key.intensity,
'murk must dim the key');
assert.ok(murkyInst.camFill.intensity < clearInst.camFill.intensity,
'murk must dim the lantern fill');
assert.ok(murkyBuilt.fogNear < clearBuilt.fogNear,
'murk must pull fog closer');
}
});
});