Files
music-video-gen/flow-state/tools/new-venue.js
T
Dejvino 46fab512df Venues: persistent room, motivated camera, ride; drop Pylon Grid
- Venue as one continuous place: persistent per-song layer, interpolated
  venueState, story jumps (one re-hang max + recap) with camera cuts
- Room camera shot plan (venueShots.js): per-section size/target, monotonic
  look sweep, traverse glide body, room-side alternation
- New Ride venue: generated vehicle + actor passengers, radio RTT screen,
  chase/stationary modes, ride archetype across generator/camera/staging
- Flora cost trim (5x(12+6+6) -> 4x(8+4+4) SDF evals, 204ms -> 140ms worst)
- Delete Pylon Grid shader (2.5s/frame SwiftShader); drop its metadata row
- tools/venue-check.js headless gate harness + venue-gallery explorer
2026-09-17 19:35:26 +02:00

101 lines
5.9 KiB
JavaScript

#!/usr/bin/env node
// Scaffold a new venue — mirrors tools/new-scene.js --model.
// Venue IS the ground (role:'ground'), kind:'model' with build/update stubs
// that map venueSpec.perSection + stageNode pattern.
// Usage: node tools/new-venue.js "Harbour" --archetype=chamber
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const VENUES = join(root, 'src/venues');
const REGISTRY = join(root, 'src/venues/registry.js');
const ARCHETYPES = ['chamber','stage','ride'];
const args = process.argv.slice(2);
const flags = new Map(args.filter(a=>a.startsWith('--')).map(a=>{ const [k,v]=a.replace(/^--/,'').split('='); return [k, v===undefined?true:v]; }));
const name = args.find(a=>!a.startsWith('--'));
if (!name) { console.error(`usage: node tools/new-venue.js "Name" --archetype=<${ARCHETYPES.join('|')}>`); process.exit(1); }
const archetype = String(flags.get('archetype')||'chamber');
if (!ARCHETYPES.includes(archetype)) { console.error(`unknown archetype '${archetype}'`); process.exit(1); }
const kebab = name.trim().toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-|-$/g,'');
const camel = kebab.replace(/-([a-z0-9])/g, (_,c)=>c.toUpperCase());
const file = join(VENUES, `${kebab}.js`);
if (existsSync(file)) { console.error(`${kebab}.js already exists`); process.exit(1); }
const salt = (text, lo, hi)=>{ let h=2166136261; for(let i=0;i<text.length;i++){ h^=text.charCodeAt(i); h=Math.imul(h,16777619)>>>0; } return +(lo+(h%1000)/1000*(hi-lo)).toFixed(2); };
const spread = salt(kebab, 0.5, 1.1);
const light = salt(kebab+'light', 0.5, 0.9);
const source = `// VENUE: ${name} — ${archetype} archetype. One venue = one place.
// Primitives only in V1; per-section evolution via venueState {light,fog,popMul,reachMul} from VenueSpec.perSection.
import { actorToGeometry } from '../actors/meshes.js';
export const ${camel} = {
name: '${name.trim()}',
archetype: '${archetype}',
family: '${archetype==='chamber'?'minimal':'structural'}',
kind: 'model',
venue: true,
consumes: ['form','ink','staging'],
traits: ['shape','space','style'],
texture: 0.35,
// How much viz bloom this room can take before its highlights clip.
// Dim rooms (plaster, floor light) ~1.1; rooms with a big emissive screen ~0.55.
reach: 0.9,
params: {
density: { type: 'float', range: [0.2, 0.9], default: ${spread}, uniform: 'u_density', bias: 'density' },
light: { type: 'float', range: [0.3, 1.2], default: ${light}, uniform: 'u_light', bias: 'energy' },
palette: { type: 'palette', count: 4 },
},
reactive: {},
build({ scene, seed, params, actorSpec, venueSpec, THREE }) {
const T = THREE;
const group = new T.Group(); group.name='${kebab}';
const mat = new T.MeshStandardMaterial({ color: new T.Color(0.14,0.13,0.12), roughness: 0.85 });
const floor = new T.Mesh(new T.PlaneGeometry(22,22), mat);
floor.rotation.x = -Math.PI/2; floor.position.y = -1.4; group.add(floor);
scene.add(group);
const amb = new T.AmbientLight(0xffffff, 0.42); scene.add(amb);
scene.background = null; scene.fog = new T.Fog(0x0e0e12, 12, 28); scene.userData.transparentBackground = true;
const pop = new T.Group(); pop.name='pop'; scene.add(pop);
return { group, mat, amb, pop, seed, venueSpec: venueSpec||null, venueState: null, builtKey: null, popCount: 0 };
},
update({ instance, scene, camera, timeline, features, params, palette, personality, framing, opacity, actorSpec, venueSpec, venueState, THREE }) {
const T = THREE; const t = timeline.time;
const pal = palette && palette.length ? palette : [[0.9,0.9,0.92],[0.7,0.6,0.8]];
const vs = venueSpec || instance.venueSpec;
const vState = venueState || instance.venueState || null;
if (vState) instance.venueState = vState;
const c0 = pal[0];
instance.mat.color.setRGB(c0[0]*0.26, c0[1]*0.24, c0[2]*0.28);
if (scene.fog && vState) { const fogMul = vState.fog; const space = personality?.space; const base = space ? 12 + space.depth*6 : 15; scene.fog.near = base * (0.85 + fogMul*0.4); }
if (vState) {
const target = 0.55 * (params.light||0.7) * (0.75 + vState.light*0.5);
// stub emissive if needed: venue surface placeholder
void target;
}
const identity = personality?.identity || null;
if (vs && identity && actorSpec && actorSpec.form && (!instance.popCount || instance.builtKey !== \`\${vs.seed}:\${vs.population.count}:\${identity.cast.protagonist.sides}:\${actorSpec.seed}\`)) {
// rebuild pop on spec change — stub: real venue would populate via stageNode/actorToGeometry
instance.builtKey = \`\${vs.seed}:\${vs.population.count}:\${identity.cast.protagonist.sides}:\${actorSpec.seed}\`;
instance.popCount = vs.population.count;
}
// Gate visibility by popMul roughly
if (vState) {
const mul = vState.popMul; const floorCount = Math.max(1, Math.floor((instance.popCount||0) * (0.3 + mul*0.8)));
for (let i=0;i<instance.pop.children.length;i++) instance.pop.children[i].visible = i < floorCount && opacity>0.01;
}
void T; void t; void params; void opacity;
},
};
export default ${camel};
`;
writeFileSync(file, source);
let registry = readFileSync(REGISTRY,'utf8');
const importLine = `import { ${camel} } from './${kebab}.js';`;
if (!registry.includes(importLine)) {
const lastImport = registry.lastIndexOf("} from './");
const eol = registry.indexOf('\n', lastImport);
registry = registry.slice(0,eol+1)+importLine+'\n'+registry.slice(eol+1);
}
registry = registry.replace(/const MODULES = \[/, `const MODULES = [\n ${camel},`);
writeFileSync(REGISTRY, registry);
console.log(`created src/venues/${kebab}.js`);
console.log(`registered ${camel} (${archetype})`);