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:
@@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user