- 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
101 lines
5.9 KiB
JavaScript
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})`);
|