// PROCITY Lane C — public API. The Vuntra move: every shop door opens into a unique, believable, // seeded interior, generated on demand in <50ms, byte-identical every revisit (shop.seed), themed // by shop.type. This is a standalone library + test page; Lane B wires it to real doors later via // the procity:enterShop / procity:exitShop events. // // import { buildInterior } from './js/interiors/interiors.js'; // import * as THREE from 'three'; // const room = buildInterior(shop, THREE); // shop = a CityPlan shop record + its lot dims // scene.add(room.group); // // … player walks around, hits room.exits … // room.dispose(); // frees all per-room geometry/material/textures // // buildInterior(shop, THREE, opts?) → { // group, // THREE.Group, self-contained (lights included). Add to a scene. // spawn: { x, z, ry }, // where/what-heading the player enters at (just inside the door) // exits: [ { x, z, w, toStreet } ], // door(s) back to the street // places: [ …meshes with userData ], // interactables: counter, bins, case, fridge, returns, exit // counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = where a shopkeeper (Lane D) // // stands, in room-local space, facing the customer. // dims: { W, D, H, archetype, type }, // room metrics // placement, // deterministic placement summary (deep-equal per seed) // pathOK, // door→counter connectivity held (always true; carved if needed) // dispose(), // release GPU resources; removes group from its parent // } // // `shop` is tolerant. Recognized fields: { id, type, name, seed, storeys, lot:{w,d} }. // • seed : uint32 — the ONLY randomness source (falls back to a hash of id/name if absent) // • type : any CITY_SPEC type or thriftgod alias (see theme.js); defaults to 'opshop' // • lot : { w, d } metres — room adapts to it; if absent, archetype ranges decide the size // • storeys: ceiling height driver (1 → 3.2m … up to 4.5m) // opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter? } // // Registry override (Lane F): call mergeRegistry(registry) ONCE at init to let authored registry data // override recipe fields. It is deliberately NOT a per-build option — a per-call mutation of the shared // recipe table would break the same-seed determinism guarantee for every later build. import { Ctx } from './context.js'; import { getRecipe, mergeRegistry, SHOP_TYPES } from './theme.js'; import { ARCHETYPE_KEYS, chooseArchetype, computeDims, buildShell } from './shell.js'; import { layout } from './layout.js'; import { preloadManifest, upgradeRoom } from './glb.js'; import { xmur3 } from '../core/prng.js'; export { SHOP_TYPES, ARCHETYPE_KEYS, mergeRegistry }; export { preloadManifest } from './glb.js'; // Lane F: preload once so GLB upgrades apply synchronously let glbManifest = null; // resolved manifest cache (once the depot answers) function normalizeShop(shop) { const s = shop || {}; let seed = s.seed; if (seed == null) seed = xmur3(`${s.id ?? ''}:${s.name ?? ''}:${s.type ?? ''}`)(); const lot = s.lot && s.lot.w && s.lot.d ? { w: +s.lot.w, d: +s.lot.d } : (s.lotW && s.lotD ? { w: +s.lotW, d: +s.lotD } : null); return { id: s.id ?? 'shop', type: s.type ?? 'opshop', name: s.name ?? '', seed: seed >>> 0, storeys: s.storeys ?? 1, lot, raw: s, }; } export function buildInterior(shop, THREE, opts) { opts = opts || {}; // tolerate undefined AND explicit null const t0 = (typeof performance !== 'undefined' ? performance.now() : 0); const norm = normalizeShop(shop); const recipe = getRecipe(norm.type); const ctx = new Ctx(THREE, norm.seed); // room shape → dims → shell const archetype = chooseArchetype(recipe, ctx.stream('archetype'), opts.archetype); const dims = computeDims(ctx, recipe, archetype, norm.lot, norm.storeys); const shell = buildShell(ctx, { recipe, archetype, dims }); // fittings + wall decor + stock + walkable-path guarantee const lay = layout(ctx, { recipe, dims, shell, adapter: opts.stockAdapter || null, shop: norm }); // OPTIONAL GLB hero-prop upgrade (off by default). Placeholder-persists: primitives stay until (and // unless) the depot answers. `opts.useGLB` enables it; the manifest may be preloaded (glb.preloadManifest) // or passed as opts.manifest. Never blocks the build; never required for a fully-usable room. if (opts.useGLB) { const manifest = opts.manifest || (glbManifest && glbManifest.__resolved) || null; if (manifest) upgradeRoom(ctx, lay.placed, manifest); else preloadManifest(opts.manifestUrl).then(m => { if (m && !ctx._disposed) { glbManifest = { __resolved: m }; upgradeRoom(ctx, lay.placed, m); } }); } const group = shell.group; const buildMs = (typeof performance !== 'undefined' ? performance.now() : 0) - t0; let disposed = false; const api = { group, spawn: shell.spawn, exits: shell.exits, places: lay.places, counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose dims: { ...dims, archetype, type: recipe.key }, recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter }, placement: lay.placement, pathOK: lay.pathOK, carved: lay.carved, buildMs, _debug: { grid: lay.grid, spawnCell: lay.spawnCell, targetCell: lay.targetCell, ctx }, counts: () => ctx.counts(), dispose() { if (disposed) return; disposed = true; if (group.parent) group.parent.remove(group); // drop lights/children refs, then free tracked GPU resources ctx.disposeAll(); }, }; return api; }