From 0e9d3fb0f2d5f658ff5cda934ab13107646ce774 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 14 Jul 2026 12:08:12 +1000 Subject: [PATCH] Lane C: on-demand seeded shop interiors (library + test page) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every shop door opens into a unique, believable, seeded interior — generated in ~4ms, byte-identical every revisit (shop.seed), themed by shop.type. - interiors.js: buildInterior(shop, THREE, opts) public API — pure fn of shop, returns {group, spawn, exits, places, dims, placement, dispose()}. - theme.js: 9 CITY_SPEC type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + type aliasing + one-time mergeRegistry() override seam for Lane F. Standalone (no hard registry dep). - shell.js: room shell from lot x archetype (cosy/gallery/wide/hall/pokey), glazed shopfront + street backdrop, blocked back doorway, interior lighting. - fittings.js: parametric kit ported from 90sDJsim + extended (bins, crates, 4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner racks, armchair, escalator, pegboard, barred window, returns slot, art). - layout.js: per-archetype zones, thriftgod shuffled wall-slot system, occupancy grid, guaranteed door->counter flood-fill path (pull/carve). - stock.js: v1 visual stock (pooled canvas sleeves/spines/boxes/garments/snacks with price stickers) + stockAdapter hook for BaseGod content later. - context.js: seed sub-streams + shared-geometry cache + leak-free disposeAll(). - glb.js: optional GLB hero-prop upgrade via Lane E manifest (off by default, primitive fallback). - interior_test.html: standalone page — seed/type/archetype, first-person walk, wireframe/occupancy/path debug, 50-room soak (perf + leak + determinism). Acceptance (verified): same seed -> identical placement (0/810 mismatches); 9 types x 5 archetypes render sensibly (docs/shots/laneC grid); build <50ms (steady ~4ms, soak worst 8ms); leak-free dispose (geo/tex delta 0); door->counter path always exists (0 fails, 0 carves); runs with zero assets and zero network. Adversarial multi-agent review: 5 findings, all fixed and re-verified. Co-Authored-By: Claude Opus 4.8 --- C-progress.md | 147 +++++++ docs/shots/laneC/.gitkeep | 0 docs/shots/laneC/_grid_9types_5archetypes.jpg | Bin 0 -> 122597 bytes web/interior_test.html | 284 ++++++++++++++ web/js/interiors/context.js | 168 ++++++++ web/js/interiors/fittings.js | 369 ++++++++++++++++++ web/js/interiors/glb.js | 66 ++++ web/js/interiors/interiors.js | 117 ++++++ web/js/interiors/layout.js | 365 +++++++++++++++++ web/js/interiors/shell.js | 213 ++++++++++ web/js/interiors/stock.js | 213 ++++++++++ web/js/interiors/theme.js | 236 +++++++++++ 12 files changed, 2178 insertions(+) create mode 100644 C-progress.md create mode 100644 docs/shots/laneC/.gitkeep create mode 100644 docs/shots/laneC/_grid_9types_5archetypes.jpg create mode 100644 web/interior_test.html create mode 100644 web/js/interiors/context.js create mode 100644 web/js/interiors/fittings.js create mode 100644 web/js/interiors/glb.js create mode 100644 web/js/interiors/interiors.js create mode 100644 web/js/interiors/layout.js create mode 100644 web/js/interiors/shell.js create mode 100644 web/js/interiors/stock.js create mode 100644 web/js/interiors/theme.js diff --git a/C-progress.md b/C-progress.md new file mode 100644 index 0000000..9959103 --- /dev/null +++ b/C-progress.md @@ -0,0 +1,147 @@ +# LANE C — INTERIORS · progress (PROCITY-C) + +*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens +into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.* + +Last updated: 2026-07-14 · owner: PROCITY-C · reviewer: Fable + +--- + +## What shipped + +All files under my lane's ownership (`web/js/interiors/*`, `web/interior_test.html`) — no other lane's +files touched. Lane A's `core/registry.js` hadn't landed, so the lane is **fully standalone** (its 9 +type recipes live in `theme.js`) with a `mergeRegistry()` hook for Lane F to wire the registry later. + +| file | role | +|---|---| +| [interiors.js](web/js/interiors/interiors.js) | **public API** — `buildInterior(shop, THREE, opts)` | +| [theme.js](web/js/interiors/theme.js) | 9 shop-type recipes (archetype/wallpaper/floor bias, clutter, counter pos, fittings mix, stock kind, signage) + aliasing + registry-override hook | +| [shell.js](web/js/interiors/shell.js) | room shell: floor/walls/ceiling from lot×archetype, glazed shopfront + street backdrop, blocked back doorway, interior lighting | +| [fittings.js](web/js/interiors/fittings.js) | parametric fittings kit (ported from 90sDJsim + extended): bins, crates, racks, 4 shelf types, VHS aisle, glass case, counter+till, fridge, magazine/spinner racks, armchair, escalator, pegboard, barred window, returns slot, art frames | +| [layout.js](web/js/interiors/layout.js) | the placer: per-archetype zones, shuffled wall-slot system, occupancy grid, **guaranteed door→counter flood-fill path** | +| [stock.js](web/js/interiors/stock.js) | v1 visual stock: pooled canvas-texture sleeves/spines/boxes/garments/snacks with price stickers (dig.js trick) + `stockAdapter` hook for BaseGod data | +| [context.js](web/js/interiors/context.js) | build/dispose/seed backbone: independent seeded sub-streams, shared-geometry cache, leak-free `disposeAll()` | +| [glb.js](web/js/interiors/glb.js) | **optional** GLB hero-prop upgrade — reads Lane E's `manifest.json`, swaps primitives for depot GLBs, placeholder-persists. OFF by default; primitive fallback always | +| [interior_test.html](web/interior_test.html) | standalone test page (below) | + +## Public API (the contract Lane B/F build against) + +```js +import { buildInterior } from './js/interiors/interiors.js'; +import * as THREE from 'three'; + +const room = buildInterior(shop, THREE, opts?); +scene.add(room.group); +// … player walks; on hitting room.exits[i], fire procity:exitShop … +room.dispose(); // frees ALL per-room GPU resources; removes group from parent + +// room = { +// group, // THREE.Group, self-contained (lights included) +// spawn: { x, z, ry }, // enter just inside the door, facing −Z (into the shop) +// exits: [ { x, z, w, toStreet } ], // door back to street +// places: [ …meshes w/ userData ], // interactables: counter, bin, case, fridge, returns, exit +// dims: { W, D, H, archetype, type }, +// placement, // deterministic summary (deep-equal per seed) +// pathOK, buildMs, counts(), dispose() +// } +``` + +`shop` is tolerant — recognized fields `{ id, type, name, seed, storeys, lot:{w,d} }`: +- **seed** (uint32) is the ONLY randomness source; falls back to a hash of id/name/type if absent. +- **type** — any CITY_SPEC type (`record opshop toy book video pawn milkbar dept stall`) **or** a + thriftgod/Overpass alias (`music charity pawnbroker market video_games …`); defaults to `opshop`. +- **lot** `{w,d}` metres — the room adapts to it (never exceeds it); absent → archetype ranges decide. +- **storeys** drives ceiling height 3.2–4.5m. +- `opts`: `{ archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, useGLB?, manifest?, manifestUrl? }`. + Registry override is a **one-time global** — `import { mergeRegistry } from interiors.js; mergeRegistry(registry)` + at init (NOT a per-build opt — a per-call mutation of the shared recipe table would break same-seed determinism). + +**Coordinate convention** (shared with a future Lane B wiring): `+Z` = door/street side, `−Z` = back, +origin = room centre, floor `y = 0`. Everything is in the returned `group`'s local space. + +### For Lane B (door wiring, later) +`buildInterior` is a pure function — call it on `procity:enterShop`, add `room.group` to the scene, +teleport the player to `room.spawn`. On `procity:exitShop` (player reaches `room.exits[0]`) call +`room.dispose()` and restore the street. The street window fake is Lane B's; I own the inside view +(a bright canvas street backdrop plane is already behind the glazing). + +### For content phase (BaseGod / GODVERSE) +Pass `opts.stockAdapter = (shop, slotKind) => ({ texture }) | ({ mesh }) | null`. Returning a texture +skins that stock item's face; a mesh replaces the item; null → procedural placeholder. No layout/ +fittings changes needed. `slotKind ∈ { sleeve, spine, box, snack, magazine, garment, treasure }`. + +## Test page — `web/interior_test.html` + +Serve: `cd web && python3 -m http.server 8130` → open `/interior_test.html`. +- seed input · type dropdown (9) · archetype override (auto + 5) · lot `w×d`/auto · re-roll · seed ± +- first-person walk (PointerLock + WASD) with occupancy-grid collision + room clamp +- debug toggles: **wireframe** (F), **occupancy grid** (G), **path** (spawn/counter markers) +- **▶ 50-room soak test**: builds+disposes 50 seeded rooms, asserts ms/room, leaked geometries vs + baseline, and determinism (same seed → identical placement) + +## Acceptance — all met + +| requirement | result | +|---|---| +| Same seed twice → identical room (deep-equal placement) | ✅ 0 mismatches / 810 builds | +| All 9 types × 5 archetypes render sensibly | ✅ contact sheet `docs/shots/laneC/_grid_9types_5archetypes.jpg` | +| Build < 50ms/room | ✅ steady-state ~4ms, clean-soak worst ~8ms (first-ever build ~60ms one-time cold-JIT; occasional ~60ms GC pauses only under back-to-back torture loops — not a real per-room cost) | +| Dispose leak-free (info.memory → baseline) | ✅ soak leak: geo 0, tex 0 | +| Flood-fill door→counter always exists | ✅ 0 path-fails, 0 carves / 810 builds | +| Runs with zero assets & zero network | ✅ seeded flat-colour fallback under every texture; map only assigned on load | + +**Full sweep** (9 types × {auto+5 archetypes} × 3 seeds × {null,4×4,5×14,20×20,3×3} lots = 810 builds): +`throws 0 · pathFail 0 · determinismFail 0 · carved 0`. Warm perf: all < 50ms (worst 33.2ms). +**Soak** (50 rooms): `avg 4.0ms · worst 9.6ms · leakGeo 0 · leakTex 0 · detFail 0 · pathFail 0`. + +## Key decisions & notes for review + +- **90sDJsim `fittings.js` ported** (it was written to be lifted): `clothesRack`, `shelfUnit`, + `recordBin`, `table`, `counter`, `pegboard`, garment-silhouette canvas trick. Extended with the + fittings CITY_SPEC's registry needs (cube/metal/book/VHS shelving, glass case, fridge, spinner, + magazine rack, armchair, escalator prop, returns slot, barred window, listening corner). +- **thriftgod `buildShop` ported**: the 5 archetypes, the shuffled wall-slot system, bins with fanned + sleeves wearing covers, corner counter+till, crooked wall art, blocked back doorway. +- **Determinism** via independent seeded sub-streams keyed by a salt string (`ctx.stream('wallpaper')` + etc.) so adding a fitting in one subsystem never shifts another subsystem's picks. +- **Leak-free** via a tracked build context: per-room geometry/material/CanvasTexture/tiled-file-texture + are freed in `dispose()`; globally-cached file textures (`core/loaders.loadTex`) are shared and not + disposed (bounded cache). Dispose-before-texture-load race is guarded (`_disposed`). +- **Perf**: shared-geometry cache (`ctx.boxGeo/planeGeo/cylGeo`) collapses the hundreds of identical + stock items in a room down to ~80–110 geometries; spine density tuned so a book barn is ~600 meshes. +- **GLB upgrades**: `web/assets/manifest.json` (Lane E) is read if present, primitive fallback always. + Not present yet → 100% primitives, which is why the test page runs asset-free. + +## Integration with sibling lanes (landed in parallel) +- **Lane A `core/registry.js`** — its `SHOP_TYPE_IDS` match my 9 recipe keys exactly. I kept the + interior recipes local (the registry doesn't carry archetype-bias/clutter/counter-pos/stock detail), + with `mergeRegistry()` as the one-time override seam for Lane F. +- **Lane E `web/assets/manifest.json`** — its `fittings` section (9 depot GLBs with footprints) is now + wired through `glb.js` (opt-in `useGLB`, primitive fallback). Depot GLBs may not be uploaded yet; + verified the fallback (unreachable depot → primitives persist, no crash, no leak). + +## Adversarial review — 5 findings, all fixed + +Ran a 5-dimension adversarial multi-agent review (determinism / disposal-leaks / path-guarantee / +three-api / edge-robustness) with a per-finding verify pass. 5 real defects surfaced; all fixed and +re-verified (830-build sweep + 50-room soak green after each fix): + +| # | sev | file | defect | fix | +|---|---|---|---|---| +| 1 | med | interiors.js | per-build `opts.registry` permanently mutated the shared recipe table → latent same-seed determinism break across differing registry states | removed per-build option; registry override is now a one-time global `mergeRegistry()` (Lane F setup) | +| 2 | low | interiors.js | explicit `null` opts threw (`opts = {}` default only covers `undefined`) | normalize `opts = opts \|\| {}` | +| 3 | low | shell.js | empty `archetypeBias` array → `bias[0][0]` on `[]` throws | fall back to uniform bias when missing **or** empty | +| 4 | med | layout.js | fittings pulled by the path loop left phantom interactables in `places` (dangling after dispose) | collect `places` from survivors at the end, not incrementally at placement | +| 5 | low | layout.js | `rebuildOcc` re-stamped wall-mounted fittings that `placeAtWall` never stamped → phantom floor obstacles after a blocker-pull | mark wall-mounted `noStamp`; skip in `rebuildOcc` **and** exclude from the removable pool | + +Verify evidence (post-fix): `nullOpts ok · phantomPlaces 0/332 · determinismAcrossBuilds identical · +emptyBias ok · sweep 810 {throws 0, pathFail 0, detFail 0} · soak {avg 4.2ms, worst 8ms, leakGeo 0, +leakTex 0}`. (Reviewers also confirmed no disposal/leak or three.js-contract defects.) + +## Not done (out of scope for v1 / depends on other lanes) +- Real item data / economy (content phase — `stockAdapter` hook is ready). +- GLB hero props are **wired** (`glb.js`, opt-in) but not visually validated end-to-end — needs the + depot GLBs actually uploaded (Lane E) + `useGLB` flipped on by Lane B/F. Primitive path is the tested baseline. +- Interior-mapping shader for street windows (Lane B stretch goal). +- Back-room beyond the blocked doorway (v2). diff --git a/docs/shots/laneC/.gitkeep b/docs/shots/laneC/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/shots/laneC/_grid_9types_5archetypes.jpg b/docs/shots/laneC/_grid_9types_5archetypes.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8cd7f046502b257c4fc1db4ae5b41d54c9a684e4 GIT binary patch literal 122597 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 *|wi7amRQ5hRQ+29yx2W+P|9Euv{1G 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 CY(k&_ literal 0 HcmV?d00001 diff --git a/web/interior_test.html b/web/interior_test.html new file mode 100644 index 0000000..8866ce0 --- /dev/null +++ b/web/interior_test.html @@ -0,0 +1,284 @@ + + + + + +PROCITY · Lane C — Interior Test + + + +

+
+

PROCITY · Lane C

+
+
+
+
+
+ + + +
+
+ + + +
+
+
+
+
+
Click to walk · WASD move · mouse look · Esc release · G grid · F wireframe
+ + + + + diff --git a/web/js/interiors/context.js b/web/js/interiors/context.js new file mode 100644 index 0000000..7e45ed8 --- /dev/null +++ b/web/js/interiors/context.js @@ -0,0 +1,168 @@ +// PROCITY Lane C — build context: the seed + disposal backbone every interior module shares. +// +// Two hard constraints this file exists to satisfy (CITY_SPEC + LANE_C acceptance): +// 1. DETERMINISM. All randomness in an interior derives from `shop.seed`. We hand out +// INDEPENDENT sub-streams keyed by a salt string, so adding a fitting in one subsystem +// never shifts the wallpaper pick in another. Math.random() is banned in generation. +// 2. LEAK-FREE DISPOSAL. The 50-room soak must return renderer.info.memory to baseline. +// Every geometry / material / CanvasTexture we mint per room is tracked and freed in +// dispose(). Globally-cached FILE textures (loaders.loadTex) are shared across rooms by +// design and are NOT disposed here — that's a bounded cache, not a leak. + +import { mulberry32, xmur3 } from '../core/prng.js'; +import * as THREE_NS from 'three'; + +// Deterministic 32-bit hash of a salt string → xor'd into the seed for an independent stream. +const hash = (s) => xmur3(s)(); + +// One shared TextureLoader for per-surface tiled loads (each surface needs its own repeat). +const _texLoader = new THREE_NS.TextureLoader(); + +export class Ctx { + constructor(THREE, seed) { + this.THREE = THREE; + this.seed = seed >>> 0; + this._geometries = new Set(); // per-room BufferGeometry — dispose all + this._materials = new Set(); // per-room Material — dispose all + this._canvasTex = new Set(); // per-room CanvasTexture + tiled file textures — dispose all + this._streams = new Map(); // salt → mulberry32 fn (memoized so a salt is stable) + this._geoCache = new Map(); // dims-key → shared geometry (stock items reuse; disposed once) + this._disposed = false; + } + + // An independent, memoized seeded stream for a subsystem. stream('wallpaper') always returns + // the same sequence for a given seed, regardless of what other subsystems do. + stream(salt) { + let s = this._streams.get(salt); + if (!s) { s = mulberry32((this.seed ^ hash(salt)) >>> 0); this._streams.set(salt, s); } + return s; + } + + // A one-shot seeded value 0..1 (fresh stream, not advancing anyone else's). + rand(salt) { return mulberry32((this.seed ^ hash(salt)) >>> 0)(); } + + // ---- tracked resource factories ------------------------------------------------- + + geom(g) { this._geometries.add(g); return g; } + + // Shared geometry caches — the same box/plane/cyl dims are reused across the many stock items in a + // room (a book barn has hundreds of identically-sized spines). Cuts allocation + build time hugely; + // each cached geometry is tracked once and disposed once. Dims rounded to 2mm to key the cache. + boxGeo(w, h, d) { + const k = `b${Math.round(w * 500)},${Math.round(h * 500)},${Math.round(d * 500)}`; + let g = this._geoCache.get(k); + if (!g) { g = this.geom(new this.THREE.BoxGeometry(w, h, d)); this._geoCache.set(k, g); } + return g; + } + planeGeo(w, h) { + const k = `p${Math.round(w * 500)},${Math.round(h * 500)}`; + let g = this._geoCache.get(k); + if (!g) { g = this.geom(new this.THREE.PlaneGeometry(w, h)); this._geoCache.set(k, g); } + return g; + } + cylGeo(r, h, seg) { + const k = `c${Math.round(r * 500)},${Math.round(h * 500)},${seg}`; + let g = this._geoCache.get(k); + if (!g) { g = this.geom(new this.THREE.CylinderGeometry(r, r, h, seg)); this._geoCache.set(k, g); } + return g; + } + + // MeshStandardMaterial with sane 90s-matte defaults; colour accepts hex/number/THREE.Color. + mat(color = 0xcccccc, roughness = 0.9, opts = {}) { + const m = new this.THREE.MeshStandardMaterial({ + color: new this.THREE.Color(color), roughness, metalness: 0, ...opts, + }); + this._materials.add(m); + return m; + } + + // Cheap box helper — the workhorse. Geometry shared via cache; material tracked for disposal. + box(w, h, d, material, x = 0, y = 0, z = 0, parent = null) { + const me = new this.THREE.Mesh(this.boxGeo(w, h, d), material); + me.position.set(x, y, z); + if (parent) parent.add(me); + return me; + } + + plane(w, h, material, parent = null) { + const me = new this.THREE.Mesh(this.planeGeo(w, h), material); + if (parent) parent.add(me); + return me; + } + + // Cylinder helper (rails, uprights, spinner posts). rz rotates for horizontal rails. + cyl(r, h, material, x = 0, y = 0, z = 0, parent = null, rz = 0, seg = 12) { + const me = new this.THREE.Mesh(this.cylGeo(r, h, seg), material); + me.position.set(x, y, z); + me.rotation.z = rz; + if (parent) parent.add(me); + return me; + } + + // Per-room CanvasTexture (tracked → disposed). Use for signs and stock sleeves. + canvasTexture(canvas) { + const t = new this.THREE.CanvasTexture(canvas); + t.colorSpace = this.THREE.SRGBColorSpace; + t.anisotropy = 4; + this._canvasTex.add(t); + return t; + } + + // Skin a material with a tiled file texture, thriftgod-style: the map is assigned ONLY inside the + // load callback (so nothing renders a mapless-but-marked texture → no "no image data" warnings, + // and the flat-colour fallback shows until the file resolves). Each tiled surface gets its OWN + // texture (per-wall/floor repeat differs). The texture is TRACKED immediately for disposal; if the + // room is disposed before the load finishes, the callback no-ops (guarded by _disposed). + skin(material, url, { repeat = null, tint = 0xcccccc } = {}) { + const t = _texLoader.load(url, () => { + if (this._disposed) return; + material.map = t; material.color.set(tint); material.needsUpdate = true; + }, undefined, () => { /* missing asset → fallback colour stays */ }); + t.colorSpace = this.THREE.SRGBColorSpace; + t.anisotropy = 4; + if (repeat) { t.wrapS = t.wrapT = this.THREE.RepeatWrapping; t.repeat.set(repeat[0], repeat[1]); } + this._canvasTex.add(t); // per-room texture → freed in disposeAll() + return material; + } + + disposeAll() { + this._disposed = true; // late texture-load callbacks will no-op after this + for (const g of this._geometries) g.dispose(); + for (const m of this._materials) m.dispose(); + for (const t of this._canvasTex) t.dispose(); + this._geometries.clear(); + this._materials.clear(); + this._canvasTex.clear(); + this._streams.clear(); + this._geoCache.clear(); + } + + // Snapshot for the leak assertion / debugging. + counts() { + return { + geometries: this._geometries.size, + materials: this._materials.size, + canvasTextures: this._canvasTex.size, + }; + } +} + +// Seeded helpers that take an explicit stream fn (so callers stay deterministic). +export const pick = (r, arr) => arr[(r() * arr.length) | 0]; +export const irange = (r, lo, hi) => lo + ((r() * (hi - lo + 1)) | 0); +export const frange = (r, lo, hi) => lo + r() * (hi - lo); +export const chance = (r, p) => r() < p; +export function shuffle(r, arr) { + for (let i = arr.length - 1; i > 0; i--) { + const j = (r() * (i + 1)) | 0; + [arr[i], arr[j]] = [arr[j], arr[i]]; + } + return arr; +} +// Weighted pick: items = [{...,weight}]; returns the item. +export function weighted(r, items) { + const total = items.reduce((s, it) => s + (it.weight || 1), 0); + let t = r() * total; + for (const it of items) { t -= (it.weight || 1); if (t <= 0) return it; } + return items[items.length - 1]; +} diff --git a/web/js/interiors/fittings.js b/web/js/interiors/fittings.js new file mode 100644 index 0000000..381bef8 --- /dev/null +++ b/web/js/interiors/fittings.js @@ -0,0 +1,369 @@ +// PROCITY Lane C — parametric fittings kit. Ported from 90sDJsim/web/world/fittings.js (the kit was +// "written to be lifted") and extended with the fittings the CITY_SPEC registry needs: cube/metal/ +// book/VHS shelving, glass case, fridge, magazine rack, spinner rack, armchair, escalator prop, +// returns slot, barred window, listening corner, crate. +// +// Every builder is `(ctx, o, r) → Fitting` where: +// ctx : the shared Ctx (tracked box/mat/cyl for leak-free disposal, canvas textures) +// o : sizing opts (w/h/d/count…), all optional — sensible defaults per fitting +// r : a seeded stream (mulberry32) so a shop is byte-identical every revisit +// +// Fitting = { +// group, // THREE.Group built at LOCAL origin, sitting on floor y=0, +// // footprint centred on (0,0). The layout placer positions it. +// footprint: { w, d }, // floor bounding box (metres) for the occupancy grid +// height, // top of the fitting (metres) +// slots: [ StockSlot… ], // where stock.js should place visual stock (may be empty) +// places: [ mesh… ], // interactables to surface in buildInterior().places +// } +// +// StockSlot = { kind:'sleeve'|'spine'|'box'|'garment'|'treasure'|'snack'|'magazine', +// x,y,z, ry, run, depth, height, count, lean } (local to group) +// +// GLB UPGRADE: if Lane E's web/assets/manifest.json maps a fitting id → depot GLB, buildInterior +// swaps the primitive for the GLB (placeholder-persists). The kit itself is 100% primitives so the +// test page runs with zero assets and zero network. + +const MAT = { + chrome: (ctx) => ctx.mat('#b8bcc4', 0.35, { metalness: 0.8 }), + wire: (ctx) => ctx.mat('#8a8d94', 0.4, { metalness: 0.6 }), + wood: (ctx) => ctx.mat('#6d4a30', 0.8), + darkwood: (ctx) => ctx.mat('#4a3524', 0.8), + lightwood: (ctx) => ctx.mat('#9c8d76', 0.75), + peg: (ctx) => ctx.mat('#c8a878', 0.85), + glass: (ctx) => ctx.mat('#bfe0ea', 0.06, { transparent: true, opacity: 0.22 }), + metal: (ctx) => ctx.mat('#33343c', 0.5, { metalness: 0.5 }), + black: (ctx) => ctx.mat('#2a2a30', 0.5, { metalness: 0.35 }), +}; + +// A muted 90s-Australian stock/garment palette for placeholder colours (real skins come via stock). +const GARB = ['#7a4a5a', '#4a5a7a', '#5a7a4a', '#8a7a4a', '#6a4a7a', '#a06a5a', '#4a6a6a', '#907a8a', + '#b0894a', '#4f6270', '#75565a', '#5d6b45']; +const garb = (ctx, r) => ctx.mat(GARB[(r() * GARB.length) | 0], 0.85); + +// ─────────────────────────────────────────────────────────────────────────────────── +// RECORD STORE + +// Angled record bins on a timber base; each bin fans a run of leaning sleeves. +function recordBin(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.2, d = 0.72, bins = o.bins ?? (2 + (r() * 2 | 0)); + const wood = MAT.wood(ctx); + ctx.box(w, 0.5, d, wood, 0, 0.25, 0, g); // base cabinet + const slots = []; + for (let b = 0; b < bins; b++) { + const cx = -w / 2 + (w / bins) * (b + 0.5); + // faceW = sleeve width across the bin; run = packing length front-to-back; tilt = fan-back angle. + slots.push({ kind: 'sleeve', x: cx, y: 0.5, z: 0, faceW: (w / bins) * 0.82, run: d * 0.82, + height: 0.32, count: 14, tilt: -0.42 }); + } + g.userData = { kind: 'bin', interactable: true }; + return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: 0.9, slots, places: [g] }; +} + +// Open plastic/timber crate of records on the floor — the dig-through classic. +function crate(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 0.55, d = o.d || 0.45, h = 0.34; + const m = ctx.mat(r() < 0.5 ? '#8a6a45' : '#4a5a6a', 0.8); + const th = 0.02; + ctx.box(w, th, d, m, 0, h * 0.72 - th / 2, 0, g); // raised floor (records ride up) + ctx.box(th, h, d, m, -w / 2, h / 2, 0, g); ctx.box(th, h, d, m, w / 2, h / 2, 0, g); + ctx.box(w, h, th, m, 0, h / 2, -d / 2, g); ctx.box(w, h * 0.6, th, m, 0, h * 0.3, d / 2, g); + const slots = [{ kind: 'sleeve', x: 0, y: h * 0.72, z: 0, faceW: w * 0.82, run: d * 0.82, + height: 0.28, count: 12, tilt: -0.3 }]; + g.userData = { kind: 'bin', interactable: true }; + return { group: g, footprint: { w: w + 0.06, d: d + 0.06 }, height: h, slots, places: [g] }; +} + +// A listening corner: a low bench + a boxy hi-fi + headphones on a stand. +function listeningCorner(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const wood = MAT.darkwood(ctx), black = MAT.black(ctx); + ctx.box(1.0, 0.42, 0.5, MAT.wood(ctx), 0, 0.21, 0, g); // bench + ctx.box(0.5, 0.16, 0.34, black, -0.2, 0.5, 0, g); // amp/deck + ctx.box(0.16, 0.04, 0.16, MAT.metal(ctx), -0.2, 0.6, 0, g); // platter + ctx.cyl(0.02, 0.7, MAT.chrome(ctx), 0.34, 0.35, 0, g); // headphone stand post + ctx.box(0.16, 0.14, 0.1, black, 0.34, 0.74, 0, g); // cans + g.userData = { kind: 'prop' }; + return { group: g, footprint: { w: 1.1, d: 0.6 }, height: 0.9, slots: [], places: [] }; +} + +// ─────────────────────────────────────────────────────────────────────────────────── +// SHELVING FAMILY + +function _shelfFrame(ctx, g, w, h, d, m, shelves) { + [-w / 2, w / 2].forEach(sx => ctx.box(0.05, h, d, m, sx, h / 2, 0, g)); // uprights + ctx.box(w, 0.04, d, m, 0, 0.04, 0, g); // base + ctx.box(w, 0.04, d, m, 0, h, 0, g); // top + ctx.box(w, h, 0.03, MAT.darkwood(ctx), 0, h / 2, -d / 2 + 0.015, g); // back panel + const levels = []; + for (let i = 1; i <= shelves; i++) { const sy = h * i / (shelves + 1); ctx.box(w, 0.04, d, m, 0, sy, 0, g); levels.push(sy); } + return levels; +} + +// Timber/laminate shelf unit (bric-a-brac). Wall-hugging. +function wallShelf(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.4, h = o.h || 1.8, d = o.d || 0.4, shelves = o.shelves ?? 4; + const levels = _shelfFrame(ctx, g, w, h, d, MAT.lightwood(ctx), shelves); + const slots = levels.map(sy => ({ kind: 'box', x: 0, y: sy + 0.02, z: 0.02, ry: 0, + run: w - 0.24, depth: d * 0.7, height: h / (shelves + 2), count: 4, lean: false })); + return { group: g, footprint: { w, d }, height: h, slots, places: [] }; +} + +// Wire/metal shelving (op-shop, pawn, milkbar back-of-house look). +function metalShelf(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.2, h = o.h || 1.9, d = o.d || 0.42, shelves = o.shelves ?? 4; + const levels = _shelfFrame(ctx, g, w, h, d, MAT.wire(ctx), shelves); + const slots = levels.map(sy => ({ kind: 'box', x: 0, y: sy + 0.02, z: 0, ry: 0, + run: w - 0.2, depth: d * 0.72, height: h / (shelves + 2), count: 5, lean: false })); + return { group: g, footprint: { w, d }, height: h, slots, places: [] }; +} + +// Cube / pigeonhole shelving (toy, dept). A grid of open cubes, each a little display box. +function cubeShelf(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const cols = o.cols ?? (2 + (r() * 2 | 0)), rows = o.rows ?? 4; + const cw = 0.42, ch = 0.42, d = o.d || 0.4; + const w = cols * cw, h = rows * ch; + const m = ctx.mat(r() < 0.5 ? '#c9b28a' : '#d0d3d8', 0.75); + for (let c = 0; c <= cols; c++) ctx.box(0.03, h, d, m, -w / 2 + c * cw, h / 2, 0, g); + for (let rr = 0; rr <= rows; rr++) ctx.box(w, 0.03, d, m, 0, rr * ch, 0, g); + ctx.box(w, h, 0.02, MAT.darkwood(ctx), 0, h / 2, -d / 2, g); + const slots = []; + for (let c = 0; c < cols; c++) for (let rr = 0; rr < rows; rr++) + slots.push({ kind: 'box', x: -w / 2 + (c + 0.5) * cw, y: rr * ch + 0.05, z: 0.02, ry: 0, + run: cw - 0.1, depth: d * 0.6, height: ch - 0.08, count: 1, lean: false }); + return { group: g, footprint: { w, d }, height: h, slots, places: [] }; +} + +// Tall bookshelf, many narrow shelves fanned with leaning spines. +function bookshelf(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.0, h = o.h || 2.1, d = o.d || 0.3, shelves = o.shelves ?? 5; + const levels = _shelfFrame(ctx, g, w, h, d, MAT.darkwood(ctx), shelves); + const slots = levels.map(sy => ({ kind: 'spine', x: 0, y: sy + 0.02, z: 0, ry: 0, + run: w - 0.16, depth: d * 0.7, height: h / (shelves + 2) - 0.04, count: 11, lean: true })); + return { group: g, footprint: { w, d }, height: h, slots, places: [] }; +} + +// VHS aisle — double-sided tall shelf of tape spines, the video-store centrepiece row. +function vhsAisle(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.6, h = o.h || 1.7, d = o.d || 0.5, shelves = o.shelves ?? 4; + const m = ctx.mat('#3a3f4a', 0.6); + [-w / 2, w / 2].forEach(sx => ctx.box(0.06, h, d, m, sx, h / 2, 0, g)); + ctx.box(w, 0.05, d, m, 0, 0.05, 0, g); ctx.box(w, 0.1, d, m, 0, h, 0, g); + ctx.box(w, h, 0.03, m, 0, h / 2, 0, g); // central spine divider (double-sided) + const slots = []; + for (let i = 1; i <= shelves; i++) { + const sy = h * i / (shelves + 1); + ctx.box(w, 0.04, d, m, 0, sy, 0, g); + slots.push({ kind: 'spine', x: 0, y: sy + 0.02, z: d / 4, ry: 0, run: w - 0.18, depth: d * 0.35, height: h / (shelves + 2) - 0.03, count: 12, lean: false }); + slots.push({ kind: 'spine', x: 0, y: sy + 0.02, z: -d / 4, ry: Math.PI, run: w - 0.18, depth: d * 0.35, height: h / (shelves + 2) - 0.03, count: 12, lean: false }); + } + return { group: g, footprint: { w, d }, height: h, slots, places: [] }; +} + +// Spinner rack — rotating wire stand (paperbacks / postcards). +function spinnerRack(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const h = o.h || 1.6, r0 = 0.4; + const wire = MAT.wire(ctx); + ctx.cyl(0.03, h, wire, 0, h / 2, 0, g); + ctx.cyl(0.28, 0.04, wire, 0, 0.04, 0, g); + const slots = []; + for (let tier = 0; tier < 4; tier++) { + const ty = 0.5 + tier * 0.32; + for (let f = 0; f < 4; f++) { + const a = f / 4 * Math.PI * 2; + slots.push({ kind: 'box', x: Math.cos(a) * r0, y: ty, z: Math.sin(a) * r0, ry: Math.PI / 2 - a, + run: 0.28, depth: 0.04, height: 0.26, count: 1, lean: false }); + } + } + return { group: g, footprint: { w: 2 * r0 + 0.2, d: 2 * r0 + 0.2 }, height: h, slots, places: [] }; +} + +// ─────────────────────────────────────────────────────────────────────────────────── +// GARMENTS + +// Straight clothes rail on two uprights, hung with garment slots. +function clothesRack(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.6, h = o.h || 1.6, n = o.count ?? (9 + (r() * 4 | 0)); + const chrome = MAT.chrome(ctx); + [-w / 2, w / 2].forEach(sx => { ctx.cyl(0.02, h, chrome, sx, h / 2, 0, g); ctx.box(0.45, 0.03, 0.45, chrome, sx, 0.02, 0, g); }); + ctx.cyl(0.018, w, chrome, 0, h - 0.05, 0, g, Math.PI / 2); + const slots = [{ kind: 'garment', x: 0, y: h - 0.05, z: 0, ry: 0, run: w - 0.2, depth: 0.34, height: 0.52, count: n, lean: false }]; + return { group: g, footprint: { w: w + 0.1, d: 0.5 }, height: h, slots, places: [] }; +} + +// ─────────────────────────────────────────────────────────────────────────────────── +// TABLES / COUNTERS / CASES + +// Trestle / bargain table with scattered goods on top. +function trestleTable(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.4, h = o.h || 0.75, d = o.d || 0.7; + const wood = MAT.wood(ctx), dark = MAT.darkwood(ctx); + ctx.box(w, 0.05, d, wood, 0, h, 0, g); + [[-w / 2 + 0.08, -d / 2 + 0.08], [w / 2 - 0.08, -d / 2 + 0.08], [-w / 2 + 0.08, d / 2 - 0.08], [w / 2 - 0.08, d / 2 - 0.08]] + .forEach(([lx, lz]) => ctx.box(0.06, h, 0.06, dark, lx, h / 2, lz, g)); + const slots = [{ kind: 'box', x: 0, y: h + 0.03, z: 0, ry: 0, run: w - 0.3, depth: d - 0.2, height: 0.2, count: o.items ?? 7, lean: false, scatter: true }]; + return { group: g, footprint: { w, d }, height: h + 0.25, slots, places: [] }; +} + +// Shop counter with till and register screen. `interactable` — the pay point. +function counter(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.8, h = o.h || 1.0, d = o.d || 0.6; + const wood = MAT.wood(ctx), dark = MAT.darkwood(ctx); + const body = ctx.box(w, h, d, wood, 0, h / 2, 0, g); + ctx.box(w + 0.1, 0.06, d + 0.1, dark, 0, h, 0, g); // benchtop + ctx.box(0.34, 0.22, 0.28, MAT.black(ctx), w / 2 - 0.35, h + 0.15, 0, g); // till + const scr = ctx.plane(0.14, 0.1, ctx.mat('#0a1a0a', 0.4, { emissive: new ctx.THREE.Color('#123') }), g); + scr.position.set(w / 2 - 0.35, h + 0.28, 0.14); scr.rotation.x = -0.5; + body.userData = { kind: 'counter', interactable: true }; + g.userData = { kind: 'counter', interactable: true }; + return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 0.3, slots: [], places: [g] }; +} + +// Glass display case — timber base + tinted glass top, a few "treasures" inside. +function glassCase(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.2, d = o.d || 0.5; + ctx.box(w, 0.72, d, ctx.mat('#5a4632', 0.6), 0, 0.36, 0, g); // base + ctx.box(w, 0.46, d, MAT.glass(ctx), 0, 0.95, 0, g); // glass top + const slots = [{ kind: 'treasure', x: 0, y: 0.78, z: 0, ry: 0, run: w - 0.2, depth: d - 0.15, height: 0.16, count: 3 + (r() * 3 | 0), lean: false }]; + g.userData = { kind: 'case', interactable: true }; + return { group: g, footprint: { w, d }, height: 1.2, slots, places: [g] }; +} + +// ─────────────────────────────────────────────────────────────────────────────────── +// SPECIALS + +// Wall pegboard hung with small goods (video posters / pawn tools / hardware). +function pegboard(ctx, o = {}, r) { + // Built centred on local y=0 so it mounts by its centre at mountY. + const g = new ctx.THREE.Group(); + const w = o.w || 1.4, h = o.h || 1.2; + ctx.box(w, h, 0.03, MAT.peg(ctx), 0, 0, 0, g); + const slots = [{ kind: 'box', x: 0, y: 0, z: 0.05, ry: 0, run: w - 0.3, depth: 0.05, height: h - 0.3, count: o.items ?? 8, lean: false, scatter: true, wall: true }]; + return { group: g, footprint: { w, d: 0.12 }, height: h, slots, places: [], wallMounted: true, mountY: 1.4 }; +} + +// Drinks fridge — glass door, glowing interior, stacked cans/bottles (milk bar). +function fridge(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 0.9, h = o.h || 2.0, d = o.d || 0.6; + const shell = ctx.mat('#e8ecef', 0.5); + ctx.box(w, h, d, shell, 0, h / 2, 0, g); + ctx.box(w - 0.08, h - 0.5, 0.02, ctx.mat('#0c1418', 0.7, { emissive: new ctx.THREE.Color('#0a1a24') }), 0, h / 2 + 0.05, d / 2 - 0.03, g); // dark interior + ctx.box(w - 0.06, h - 0.46, 0.02, MAT.glass(ctx), 0, h / 2 + 0.05, d / 2, g); // glass door + const slots = []; + for (let i = 0; i < 4; i++) slots.push({ kind: 'snack', x: 0, y: 0.4 + i * 0.42, z: d / 2 - 0.12, ry: 0, run: w - 0.16, depth: 0.1, height: 0.3, count: 8, lean: false }); + g.userData = { kind: 'fridge', interactable: true }; + return { group: g, footprint: { w, d }, height: h, slots, places: [g] }; +} + +// Magazine rack — angled tiers of mag faces (milk bar window, book shop). +function magazineRack(ctx, o = {}, r) { + // Floor-standing tiered rack (leans against a wall / sits in the window). Base at y=0. + const g = new ctx.THREE.Group(); + const w = o.w || 1.0, h = o.h || 1.4, tiers = 4; + const m = ctx.mat('#7a5a3a', 0.75); + ctx.box(w, 0.04, 0.35, m, 0, 0.06, 0, g); + const slots = []; + for (let t = 0; t < tiers; t++) { + const ty = 0.35 + t * (h - 0.4) / tiers; + ctx.box(w, 0.03, 0.28, m, 0, ty, -0.08, g); + slots.push({ kind: 'magazine', x: 0, y: ty + 0.02, z: 0.02, tilt: -0.35, run: w - 0.1, depth: 0.24, height: (h - 0.4) / tiers - 0.02, count: 4, lean: true }); + } + return { group: g, footprint: { w, d: 0.4 }, height: h, slots, places: [] }; +} + +// Reading armchair — a cosy corner nook (book shop). +function armchair(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const m = ctx.mat(['#6a4a3a', '#4a5a4a', '#5a4a5a'][(r() * 3) | 0], 0.9); + ctx.box(0.7, 0.16, 0.7, m, 0, 0.42, 0, g); // seat + ctx.box(0.7, 0.5, 0.14, m, 0, 0.7, -0.28, g); // back + ctx.box(0.12, 0.4, 0.7, m, -0.29, 0.55, 0, g); ctx.box(0.12, 0.4, 0.7, m, 0.29, 0.55, 0, g); // arms + ctx.box(0.7, 0.42, 0.7, m, 0, 0.21, 0, g); // base + g.userData = { kind: 'prop' }; + return { group: g, footprint: { w: 0.8, d: 0.8 }, height: 0.95, slots: [], places: [] }; +} + +// Barred shop window (pawn) — a wall-mounted frame with vertical bars over dim glass. +function barredWindow(ctx, o = {}, r) { + // Built centred on local y=0 so it mounts by its centre at mountY. + const g = new ctx.THREE.Group(); + const w = o.w || 1.4, h = o.h || 1.1; + ctx.box(w, h, 0.06, ctx.mat('#4a4038', 0.7), 0, 0, -0.03, g); // frame/backing + ctx.box(w - 0.12, h - 0.12, 0.02, ctx.mat('#26303a', 0.3, { transparent: true, opacity: 0.5 }), 0, 0, 0.01, g); + const bar = MAT.metal(ctx); + const nb = 6; + for (let i = 0; i <= nb; i++) ctx.cyl(0.015, h - 0.1, bar, -w / 2 + 0.06 + i * ((w - 0.12) / nb), 0, 0.04, g); + return { group: g, footprint: { w, d: 0.12 }, height: h, slots: [], places: [], wallMounted: true, mountY: 1.2 }; +} + +// Returns slot — a wall/counter chute with a "RETURNS" plate (video store). +function returnsSlot(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + ctx.box(0.5, 0.8, 0.4, ctx.mat('#5a4632', 0.7), 0, 0.4, 0, g); + ctx.box(0.34, 0.06, 0.05, MAT.black(ctx), 0, 0.62, 0.2, g); // the slot + g.userData = { kind: 'returns', interactable: true }; + return { group: g, footprint: { w: 0.5, d: 0.4 }, height: 0.8, slots: [], places: [g] }; +} + +// Escalator prop — the dept-store grand-hall centrepiece (static, decorative). +function escalator(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const w = o.w || 1.2, len = o.len || 4.0, rise = o.rise || 2.2; + const metal = MAT.metal(ctx), rail = MAT.chrome(ctx); + // inclined stepped ramp + const steps = 12; + for (let i = 0; i < steps; i++) { + const t = i / (steps - 1); + ctx.box(w, 0.12, len / steps + 0.02, metal, 0, 0.1 + t * rise, -len / 2 + t * len, g); + } + ctx.box(w + 0.1, 0.3, len, ctx.mat('#3a3f47', 0.6), 0, rise / 2 - 0.1, 0, g); // side skirt + // inclined handrails (a box the length of the slope, tilted to match the ramp) + const slope = Math.hypot(rise, len), ang = Math.atan2(rise, len); + [-w / 2 - 0.05, w / 2 + 0.05].forEach(sx => { + const rl = ctx.box(0.06, 0.06, slope, rail, sx, 0.9 + rise / 2, 0, g); + rl.rotation.x = ang; + }); + g.userData = { kind: 'prop' }; + return { group: g, footprint: { w: w + 0.2, d: len }, height: rise + 1.0, slots: [], places: [] }; +} + +// Art frame (wall decor) — a framed canvas plane. Colour placeholder (art-*.jpg is Lane E future). +function artFrame(ctx, o = {}, r) { + const g = new ctx.THREE.Group(); + const sz = o.w || (0.7 + r() * 0.5); + ctx.box(sz + 0.08, sz + 0.08, 0.04, ctx.mat('#4a3a28', 0.6), 0, 0, -0.01, g); // frame + const artCol = new ctx.THREE.Color().setHSL(r(), 0.35, 0.45); + ctx.plane(sz, sz, ctx.mat(artCol, 0.7), g); + return { group: g, footprint: { w: sz, d: 0.08 }, height: sz, slots: [], places: [], wallMounted: true, mountY: 1.5 }; +} + +export const FITTINGS = { + recordBin, crate, listeningCorner, + wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, + clothesRack, + trestleTable, counter, glassCase, + pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame, +}; + +// Build a fitting by kind. Unknown kinds fall back to a wall shelf (never crash). +export function buildFitting(kind, ctx, o, r) { + const fn = FITTINGS[kind] || FITTINGS.wallShelf; + const fit = fn(ctx, o || {}, r); + // frameCount = children present before any procedural stock is added, so the optional GLB upgrade + // (glb.js) can hide just the primitive frame and keep the stock sitting on the detailed prop. + fit.frameCount = fit.group.children.length; + return fit; +} diff --git a/web/js/interiors/glb.js b/web/js/interiors/glb.js new file mode 100644 index 0000000..d1a8b03 --- /dev/null +++ b/web/js/interiors/glb.js @@ -0,0 +1,66 @@ +// PROCITY Lane C — optional GLB-upgrade layer. Reads Lane E's web/assets/manifest.json and swaps a +// primitive fitting for a detailed depot GLB where one exists ("reserve the detailed GLB hero props +// for where the camera gets close" — RESEARCH). Strictly ADDITIVE and OFF by default: +// • the whole lane runs 100% on primitives with no manifest and no network (LANE_C acceptance); +// • enable per build with opts.useGLB (interiors.js), or globally via preloadManifest(). +// +// Pattern (house law): promise-cached loader, PLACEHOLDER-PERSISTS — the primitive shows immediately +// and stays until the GLB resolves; if the depot is unreachable the primitive stays forever (never a +// crash, never a blank). The GLB is footprint-fitted to the primitive it replaces, so occupancy/paths +// already computed by layout.js stay valid. + +import { loadGLB } from '../core/loaders.js'; +import { clone as skeletonClone } from 'three/addons/utils/SkeletonUtils.js'; + +// my fitting kind → manifest.fittings id (only kinds with a depot GLB; others stay primitive) +const KIND_TO_GLB = { + crate: 'record_crate', recordBin: 'record_crate', + metalShelf: 'wire_shelf', wallShelf: 'wire_shelf', + clothesRack: 'clothes_rack', + bookshelf: 'bookshelf', + cubeShelf: 'cube_shelf_wide', + counter: 'counter', + trestleTable: 'work_table', +}; + +let _manifestP = null; +// Fetch + cache the manifest. Returns null (not throw) if absent/unreachable → primitives everywhere. +export function preloadManifest(url = 'assets/manifest.json') { + return (_manifestP ||= fetch(url).then(r => (r.ok ? r.json() : null)).catch(() => null)); +} +export function manifestReady() { return _manifestP && typeof _manifestP.then === 'function' ? _manifestP : Promise.resolve(null); } + +// Upgrade one placed fitting in-place, if the manifest maps its kind. No-op when disabled/unavailable. +// `manifest` is the resolved object (or null). ctx is the room build context (for the _disposed guard). +export function upgradeFitting(ctx, fitting, kind, manifest) { + if (!manifest || !manifest.fittings) return; + const id = KIND_TO_GLB[kind]; + const entry = id && manifest.fittings[id]; + if (!entry || !entry.file) return; + + loadGLB(`depot:${entry.file}`).then(gltf => { + if (!gltf || ctx._disposed || !fitting.group.parent) return; // room rebuilt / disposed mid-flight + const THREE = ctx.THREE; + const inst = skeletonClone(gltf.scene); // clone-safe even if skinned + // fit the GLB to the primitive's footprint (GLB convention: origin at base, facing −Z, metres) + const bb = new THREE.Box3().setFromObject(inst); + const size = new THREE.Vector3(); bb.getSize(size); + const want = fitting.footprint, targetW = Math.max(want.w, want.d); + const glbW = Math.max(size.x, size.z) || 1; + const s = targetW / glbW; + inst.scale.setScalar(s); + inst.position.y -= bb.min.y * s; // plant on the floor + // hide the primitive FRAME (the first frameCount children) but keep procedural stock — which was + // added after the frame — visible on top of the detailed GLB shelf/counter. + const frameCount = fitting.frameCount ?? fitting.group.children.length; + for (let i = 0; i < frameCount && i < fitting.group.children.length; i++) fitting.group.children[i].visible = false; + inst.userData = { glbUpgrade: true }; + fitting.group.add(inst); + }); +} + +// Upgrade every placed fitting in a room. Called from buildInterior when GLB mode is on. +export function upgradeRoom(ctx, placed, manifest) { + if (!manifest) return; + for (const p of placed) if (!p.removed) upgradeFitting(ctx, p.fitting, p.kind, manifest); +} diff --git a/web/js/interiors/interiors.js b/web/js/interiors/interiors.js new file mode 100644 index 0000000..5312206 --- /dev/null +++ b/web/js/interiors/interiors.js @@ -0,0 +1,117 @@ +// 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 +// 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, + 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; +} diff --git a/web/js/interiors/layout.js b/web/js/interiors/layout.js new file mode 100644 index 0000000..fb57f7f --- /dev/null +++ b/web/js/interiors/layout.js @@ -0,0 +1,365 @@ +// PROCITY Lane C — the placer. Turns a recipe + room shell into a believable, non-overlapping, +// walkable arrangement of fittings + wall decor, seeded from shop.seed. +// +// Guarantees (LANE_C acceptance): +// • nothing overlaps — every fitting reserves cells in an occupancy grid before placing +// • wall art/signs don't clash — the thriftgod shuffled wall-slot system draws from one pool +// • door → counter is walkable — flood-fill verifies connectivity; blockers are pulled until it is +// • density varies by shop — count per fitting scales by recipe.clutter × area × seed +// +// Coordinates match shell.js: +Z = door/street side, −Z = back. Origin = room centre, floor y=0. + +import { buildFitting } from './fittings.js'; +import * as stock from './stock.js'; +import { frange, shuffle } from './context.js'; + +const CELL = 0.4; // occupancy grid resolution (m) +const PAD = 0.12; // clearance padding around each fitting (m) + +// ── grid helpers ───────────────────────────────────────────────────────────────────── +function makeGrid(W, D) { + const cols = Math.max(3, Math.round(W / CELL)); + const rows = Math.max(3, Math.round(D / CELL)); + const cw = W / cols, cd = D / rows; + const occ = new Uint8Array(cols * rows); // 0 free, 1 fitting, 2 wall(border) + const reserved = new Uint8Array(cols * rows); // walkable but no placement (door corridor, till front) + for (let cx = 0; cx < cols; cx++) for (let cz = 0; cz < rows; cz++) + if (cx === 0 || cz === 0 || cx === cols - 1 || cz === rows - 1) occ[cz * cols + cx] = 2; + const cellOf = (x, z) => [ + Math.min(cols - 1, Math.max(0, Math.floor((x + W / 2) / cw))), + Math.min(rows - 1, Math.max(0, Math.floor((z + D / 2) / cd))), + ]; + return { cols, rows, cw, cd, W, D, occ, reserved, cellOf }; +} + +// Rotated-AABB half extents of a footprint at yaw ry. +function halfExtents(fw, fd, ry) { + const c = Math.abs(Math.cos(ry)), s = Math.abs(Math.sin(ry)); + return [(c * fw + s * fd) / 2 + PAD, (s * fw + c * fd) / 2 + PAD]; +} +function rectCells(grid, x, z, hw, hd) { + const a = grid.cellOf(x - hw, z - hd), b = grid.cellOf(x + hw, z + hd); + return { cx0: a[0], cz0: a[1], cx1: b[0], cz1: b[1] }; +} +// Is the padded footprint clear? (wall cells value 2 are allowed — shelves hug walls.) +function areaFree(grid, rect) { + for (let cz = rect.cz0; cz <= rect.cz1; cz++) + for (let cx = rect.cx0; cx <= rect.cx1; cx++) { + const v = grid.occ[cz * grid.cols + cx]; + if (v === 1) return false; + if (grid.reserved[cz * grid.cols + cx]) return false; + } + return true; +} +function stampRect(grid, rect) { + for (let cz = rect.cz0; cz <= rect.cz1; cz++) + for (let cx = rect.cx0; cx <= rect.cx1; cx++) + if (grid.occ[cz * grid.cols + cx] === 0) grid.occ[cz * grid.cols + cx] = 1; +} +function rebuildOcc(grid, placed) { + for (let i = 0; i < grid.occ.length; i++) if (grid.occ[i] === 1) grid.occ[i] = 0; + // Skip noStamp (wall-mounted) fittings — placeAtWall never stamped them at placement, so re-stamping + // here would inject phantom floor obstacles beside a wall the player can actually walk past, and + // could trigger needless fitting-pulls or a carve when the path was fine. + for (const p of placed) if (!p.noStamp) stampRect(grid, p.rect); +} +function reserveRect(grid, x0, z0, x1, z1) { + const a = grid.cellOf(x0, z0), b = grid.cellOf(x1, z1); + for (let cz = a[1]; cz <= b[1]; cz++) for (let cx = a[0]; cx <= b[0]; cx++) + grid.reserved[cz * grid.cols + cx] = 1; +} + +// 4-connected flood fill over walkable cells (occ==0). Returns Set of reachable indices. +function floodFill(grid, startIdx) { + const seen = new Uint8Array(grid.cols * grid.rows); + const stack = [startIdx]; seen[startIdx] = 1; + const out = new Set([startIdx]); + while (stack.length) { + const i = stack.pop(), cx = i % grid.cols, cz = (i / grid.cols) | 0; + for (const [dx, dz] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) { + const nx = cx + dx, nz = cz + dz; + if (nx < 0 || nz < 0 || nx >= grid.cols || nz >= grid.rows) continue; + const ni = nz * grid.cols + nx; + if (seen[ni] || grid.occ[ni] !== 0) continue; + seen[ni] = 1; out.add(ni); stack.push(ni); + } + } + return out; +} + +// ── counter placement per recipe.counterPos ────────────────────────────────────────── +function placeCounter(ctx, recipe, W, D, r) { + const pos = recipe.counterPos || 'corner'; + const cw = Math.min(2.2, Math.max(1.4, W * 0.42)); + let x, z, ry, front; + if (pos === 'door') { // milk bar: faces the door, front-left + x = -W * 0.12; z = D / 2 - 1.8; ry = 0; front = { x, z: z + 0.9 }; + } else if (pos === 'forward') { // pawn/stall: counter up front, goods behind + x = -W * 0.05; z = D / 2 - 2.4; ry = 0; front = { x, z: z + 0.95 }; + } else if (pos === 'back') { // along the back wall, facing in + x = -W * 0.1; z = -D / 2 + 0.7; ry = Math.PI; front = { x, z: z + 0.95 }; + } else { // 'corner' — back-east, facing into the room + x = W / 2 - 0.55; z = -D / 4; ry = -Math.PI / 2; front = { x: x - 0.95, z }; + } + const f = buildFitting('counter', ctx, { w: cw }, r); + f.group.position.set(x, 0, z); f.group.rotation.y = ry; + const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry); + return { fitting: f, x, z, ry, hw, hd, front, priority: 9, kind: 'counter' }; +} + +// ── wall-slot system (shuffled pool → art + signs + wall fittings never overlap) ────── +function makeWallSlots(W, D, r) { + const slots = []; + const nZ = Math.max(2, Math.floor((D - 3.2) / 1.7)); + for (let i = 0; i < nZ; i++) { + const z = -D / 2 + 1.6 + i * ((D - 3.4) / Math.max(1, nZ - 1)); + slots.push({ wall: 'west', x: -W / 2, z, ry: Math.PI / 2, used: false }); + slots.push({ wall: 'east', x: W / 2, z, ry: -Math.PI / 2, used: false }); + } + const nX = Math.max(1, Math.floor((W - 3.0) / 1.7)); + for (let i = 0; i < nX; i++) { + const x = -W / 2 + 1.5 + i * ((W - 3.0) / Math.max(1, nX - 1)); + slots.push({ wall: 'north', x, z: -D / 2, ry: 0, used: false }); + } + shuffle(r, slots); + return slots; +} + +// ── free-standing candidate positions per zone ──────────────────────────────────────── +function zoneCandidates(zone, W, D, doorCx, r) { + const out = []; + if (zone === 'window') { + const z = D / 2 - 1.4; + for (let x = -W / 2 + 1.1; x <= doorCx - 1.2; x += 1.3) out.push({ x, z, ry: Math.PI }); + } else if (zone === 'aisle') { + for (let z = D / 2 - 3.0; z >= -D / 2 + 1.6; z -= 1.7) + for (let x = -W / 2 + 1.3; x <= W / 2 - 1.3; x += 1.9) out.push({ x, z, ry: 0 }); + } else if (zone === 'centre') { + for (let z = D / 2 - 3.5; z >= -D / 2 + 2.2; z -= 2.0) + for (let x = -W / 4; x <= W / 4; x += 1.8) out.push({ x, z, ry: 0 }); + } + return shuffle(r, out); +} + +// ── main entry ──────────────────────────────────────────────────────────────────────── +export function layout(ctx, { recipe, dims, shell, adapter, shop }) { + const { W, D } = dims; + const doorCx = shell.door.x; + const grid = makeGrid(W, D); + const rCount = ctx.stream('lay-count'), rPlace = ctx.stream('lay-place'), rWall = ctx.stream('lay-wall'); + + // Reserve the door corridor (walkable, no fittings) and the spawn cell. + reserveRect(grid, doorCx - 0.7, D / 2 - 2.0, doorCx + 0.7, D / 2 - 0.4); + + const placed = []; + const roomGroup = shell.group; + + // 1) COUNTER first (always present, reachability target). + const counter = placeCounter(ctx, recipe, W, D, rCount); + counter.rect = rectCells(grid, counter.x, counter.z, counter.hw, counter.hd); + stampRect(grid, counter.rect); + placed.push(counter); + // reserve the strip in front of the till so nothing blocks service + reserveRect(grid, counter.front.x - 0.5, counter.front.z - 0.5, counter.front.x + 0.5, counter.front.z + 0.5); + + // 2) FITTINGS from the recipe mix (skip the counter entry — already placed). + const area = W * D; + const wallSlots = makeWallSlots(W, D, rWall); + for (const spec of recipe.fittings) { + if (spec.kind === 'counter') continue; + // count scales with clutter × area, clamped to [min,max] + const density = recipe.clutter * (area / 70); + let n = Math.round(frange(rCount, spec.min, spec.max + 0.99) * Math.min(1.3, density)); + n = Math.max(spec.min, Math.min(spec.max, n)); + for (let i = 0; i < n; i++) { + const f = buildFitting(spec.kind, ctx, {}, ctx.stream(`fit-${spec.kind}-${i}`)); + let ok = false; + if (spec.zone === 'wall' || f.wallMounted) { + ok = placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec); + } else if (spec.zone === 'counter') { + ok = placeNearCounter(ctx, grid, f, counter, rPlace, placed, spec); + } else { + ok = placeFree(ctx, grid, f, spec.zone, W, D, doorCx, rPlace, placed, spec); + } + if (!ok) { /* no room — density self-caps; drop the fitting (its resources free at room end) */ } + } + } + + // 3) PATH GUARANTEE: door → counter. Pull low-priority blockers until reachable; carve if we must. + const spawnCell = idxOf(grid, shell.spawn.x, shell.spawn.z); + const targetCell = idxOf(grid, counter.front.x, counter.front.z); + let pathOK = reaches(grid, spawnCell, targetCell); + let carved = false; + const removable = () => placed + .filter(p => p.priority < 8 && p.kind !== 'counter' && !p.noStamp) // wall-mounted don't block the floor + .sort((a, b) => a.priority - b.priority); + while (!pathOK) { + const pool = removable(); + if (!pool.length) break; + const victim = pool[0]; + placed.splice(placed.indexOf(victim), 1); + if (victim.fitting.group.parent) victim.fitting.group.parent.remove(victim.fitting.group); + victim.removed = true; + rebuildOcc(grid, placed); + pathOK = reaches(grid, spawnCell, targetCell); + } + if (!pathOK) { carveCorridor(grid, spawnCell, targetCell); carved = true; pathOK = reaches(grid, spawnCell, targetCell); } + + // 4) add SURVIVORS to the room + fill them with visual stock. Collect `places` here (NOT at + // placement time) so interactables belong only to fittings that actually made it into the room — + // a fitting pulled by the path loop must not leave a phantom interactable in the returned list. + const places = [...(shell.places || [])]; + for (const p of placed) { + if (p.removed) continue; + if (!p.fitting.group.parent) roomGroup.add(p.fitting.group); + stock.fill(ctx, p.fitting, { shop, recipe, stockKind: recipe.stockKind, adapter }, ctx.stream(`stk-${p.priority}-${p.x.toFixed(2)}-${p.z.toFixed(2)}`)); + places.push(...(p.fitting.places || [])); + } + + // 5) WALL DECOR — art frames + inside signage on leftover wall slots (shuffled, never overlap). + const decor = placeWallDecor(ctx, roomGroup, recipe, wallSlots, W, D, rWall); + + // placement summary for the determinism deep-equal test + const placement = placed.filter(p => !p.removed).map(p => ({ + kind: p.kind || 'fitting', kindName: p.fitting.group.userData.kind, + x: round(p.x), z: round(p.z), ry: round(p.ry), fw: round(p.fitting.footprint.w), fd: round(p.fitting.footprint.d), + })).concat(decor.summary); + + return { + placed: placed.filter(p => !p.removed), + places, decor: decor.meshes, grid, pathOK, carved, + spawnCell, targetCell, placement, + }; +} + +// ── placement strategies ────────────────────────────────────────────────────────────── +function placeFree(ctx, grid, f, zone, W, D, doorCx, r, placed, spec) { + const cands = zoneCandidates(zone, W, D, doorCx, r); + for (const c of cands) { + const ry = c.ry + (r() - 0.5) * 0.4; + const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry); + const rect = rectCells(grid, c.x, c.z, hw, hd); + if (!areaFree(grid, rect)) continue; + f.group.position.set(c.x, 0, c.z); f.group.rotation.y = ry; + stampRect(grid, rect); + placed.push({ fitting: f, x: c.x, z: c.z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind }); + return true; + } + return false; +} + +function placeAtWall(ctx, grid, f, wallSlots, r, placed, spec) { + const backOff = f.wallMounted ? 0.06 : f.footprint.d / 2 + 0.05; + for (const slot of wallSlots) { + if (slot.used) continue; + const ry = slot.ry; + let x = slot.x, z = slot.z; + if (slot.wall === 'west') x += backOff; else if (slot.wall === 'east') x -= backOff; else z += backOff; + const y = f.wallMounted ? (f.mountY || 1.4) : 0; + const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry); + const rect = rectCells(grid, x, z, hw, hd); + if (!f.wallMounted && !areaFree(grid, rect)) continue; + f.group.position.set(x, y, z); f.group.rotation.y = ry; + if (!f.wallMounted) stampRect(grid, rect); + slot.used = true; + // noStamp: wall-mounted fittings hang above head height and don't block the floor — keep rebuildOcc + // consistent with this placement (see rebuildOcc). + placed.push({ fitting: f, x, z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind, noStamp: f.wallMounted }); + return true; + } + return false; +} + +function placeNearCounter(ctx, grid, f, counter, r, placed, spec) { + // treasures / cases beside the till, on the goods side of the counter + const side = counter.ry === 0 ? { x: counter.x + (r() < 0.5 ? -1.3 : 1.3), z: counter.z } : { x: counter.x, z: counter.z + (r() < 0.5 ? -1.3 : 1.3) }; + const ry = counter.ry; + const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry); + const rect = rectCells(grid, side.x, side.z, hw, hd); + if (!areaFree(grid, rect)) return false; + f.group.position.set(side.x, 0, side.z); f.group.rotation.y = ry; + stampRect(grid, rect); + placed.push({ fitting: f, x: side.x, z: side.z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind }); + return true; +} + +// ── wall decor (art + signage) ───────────────────────────────────────────────────────── +function placeWallDecor(ctx, roomGroup, recipe, wallSlots, W, D, r) { + const meshes = [], summary = []; + const free = wallSlots.filter(s => !s.used); + const nArt = Math.min(free.length, 2 + (r() * 3 | 0)); + const nSign = Math.min(Math.max(0, free.length - nArt), 1 + (r() * 2 | 0)); + let fi = 0; + for (let i = 0; i < nArt && fi < free.length; i++, fi++) { + const slot = free[fi]; + const sz = 0.7 + r() * 0.5; + const artCol = new ctx.THREE.Color().setHSL(r(), 0.32, 0.42); + const frame = ctx.box(sz + 0.08, sz + 0.08, 0.04, ctx.mat('#4a3a28', 0.6), 0, 0, 0, roomGroup); + const canvas = ctx.plane(sz, sz, ctx.mat(artCol, 0.75), roomGroup); + // inward normal per wall so the canvas sits a hair in front of its frame (no z-fight) + const nx = slot.wall === 'west' ? 1 : slot.wall === 'east' ? -1 : 0; + const nz = slot.wall === 'north' ? 1 : 0; + const y = 1.5 + r() * 0.3, tilt = (r() - 0.5) * 0.05; + const baseX = slot.wall === 'west' ? -W / 2 + 0.04 : slot.wall === 'east' ? W / 2 - 0.04 : slot.x; + const baseZ = slot.wall === 'north' ? -D / 2 + 0.04 : slot.z; + frame.position.set(baseX, y, baseZ); frame.rotation.y = slot.ry; frame.rotation.z = tilt; + canvas.position.set(baseX + nx * 0.03, y, baseZ + nz * 0.03); canvas.rotation.y = slot.ry; canvas.rotation.z = tilt; + meshes.push(frame, canvas); + summary.push({ kind: 'art', wall: slot.wall, x: round(slot.x), z: round(slot.z) }); + } + const signs = recipe.signFlavour || []; + for (let i = 0; i < nSign && fi < free.length; i++, fi++) { + const slot = free[fi]; + const txt = signs[(r() * signs.length) | 0] || 'SALE'; + const mesh = makeSignPlane(ctx, txt); + const off = 0.05; + const x = slot.wall === 'west' ? -W / 2 + off : slot.wall === 'east' ? W / 2 - off : slot.x; + const z = slot.wall === 'north' ? -D / 2 + off : slot.z; + mesh.position.set(x, 2.05, z); mesh.rotation.y = slot.ry; + roomGroup.add(mesh); + meshes.push(mesh); + summary.push({ kind: 'sign', wall: slot.wall, text: txt, x: round(slot.x), z: round(slot.z) }); + } + return { meshes, summary }; +} + +function makeSignPlane(ctx, text) { + const cv = document.createElement('canvas'); cv.width = 256; cv.height = 64; + const x = cv.getContext('2d'); + x.fillStyle = '#f5f2ea'; x.fillRect(0, 0, 256, 64); + x.strokeStyle = '#333'; x.lineWidth = 3; x.strokeRect(3, 3, 250, 58); + x.fillStyle = '#c1121f'; x.font = 'bold 30px Arial'; x.textAlign = 'center'; x.textBaseline = 'middle'; + x.fillText(String(text).slice(0, 18), 128, 34); + const tex = ctx.canvasTexture(cv); + const m = ctx.mat('#ffffff', 0.7); m.map = tex; m.needsUpdate = true; + const mesh = ctx.plane(1.3, 0.32, m); + return mesh; +} + +// ── small utils ───────────────────────────────────────────────────────────────────── +function idxOf(grid, x, z) { const [cx, cz] = grid.cellOf(x, z); return cz * grid.cols + cx; } +function reaches(grid, startIdx, targetIdx) { + if (grid.occ[startIdx] !== 0) { const s = nearestFree(grid, startIdx); if (s < 0) return false; startIdx = s; } + if (grid.occ[targetIdx] !== 0) { const t = nearestFree(grid, targetIdx); if (t < 0) return false; targetIdx = t; } + return floodFill(grid, startIdx).has(targetIdx); +} +function nearestFree(grid, idx) { + const cx0 = idx % grid.cols, cz0 = (idx / grid.cols) | 0; + for (let rad = 1; rad < 6; rad++) + for (let dz = -rad; dz <= rad; dz++) for (let dx = -rad; dx <= rad; dx++) { + const nx = cx0 + dx, nz = cz0 + dz; + if (nx < 1 || nz < 1 || nx >= grid.cols - 1 || nz >= grid.rows - 1) continue; + if (grid.occ[nz * grid.cols + nx] === 0) return nz * grid.cols + nx; + } + return -1; +} +// Clear a Manhattan corridor between two cells (last-resort guarantee). +function carveCorridor(grid, startIdx, targetIdx) { + let cx = startIdx % grid.cols, cz = (startIdx / grid.cols) | 0; + const tx = targetIdx % grid.cols, tz = (targetIdx / grid.cols) | 0; + const clear = (x, z) => { if (x > 0 && z > 0 && x < grid.cols - 1 && z < grid.rows - 1) grid.occ[z * grid.cols + x] = 0; }; + while (cz !== tz) { clear(cx, cz); cz += Math.sign(tz - cz); } + while (cx !== tx) { clear(cx, cz); cx += Math.sign(tx - cx); } + clear(tx, tz); +} +const round = (v) => Math.round(v * 1000) / 1000; diff --git a/web/js/interiors/shell.js b/web/js/interiors/shell.js new file mode 100644 index 0000000..a83e118 --- /dev/null +++ b/web/js/interiors/shell.js @@ -0,0 +1,213 @@ +// PROCITY Lane C — the room shell: floor / walls / ceiling / glazed shopfront / lights. +// +// Sized from the shop's lot (lot.w × lot.d) adapted to one of the 5 thriftgod archetypes +// (cosy / gallery / wide / hall / pokey). Ceiling height 3.2–4.5m by storeys. Materials are seeded +// picks of wall-*.jpg wallpapers + tex-carpet/lino/boards floors (all already in web/assets/gen/, +// all with flat-colour fallbacks). The shopfront (south, +Z) has real glazing you can see a hint of +// street through — a bright backdrop plane beyond the glass sells the inside-looking-out view. The +// back wall (north, −Z) has a blocked doorway (v1). Door + exit live on the shopfront. +// +// Coordinate convention (shared with layout.js): +// +Z = south = the street side (door + glazing). −Z = north = back of shop. +// Player spawns just inside the door facing −Z (into the shop). Origin = room centre, floor y=0. + +import { pick, frange } from './context.js'; + +// The 5 archetypes: [wMin,wMax] × [dMin,dMax] footprint ranges + base ceiling height (m). +export const ARCHETYPES = { + cosy: { w: [7, 9], d: [8, 10], h: 3.4 }, // cosy squarish + gallery: { w: [6, 7], d: [11, 14], h: 3.4 }, // narrow deep gallery + wide: { w: [10, 12], d: [7, 9], h: 3.6 }, // wide shallow + hall: { w: [9, 11], d: [10, 13], h: 4.0 }, // big high-ceilinged hall + pokey: { w: [6, 7], d: [7, 8], h: 3.2 }, // pokey little room +}; +export const ARCHETYPE_KEYS = Object.keys(ARCHETYPES); + +// Floor names live under web/assets/gen/tex-.jpg ; wallpapers under wall-.jpg. +const FLOOR_FALLBACK = { // seeded flat colour beneath each floor texture + 'carpet-swirl': '#6b5b45', 'carpet-mustard': '#8a7038', 'carpet-greygreen': '#5d6b56', + 'lino-check': '#b9b2a0', 'lino-cork': '#9c7a52', 'boards-polished': '#7a5533', +}; +const WALL_FALLBACK = { + 'floral-cream': '#d8cdb4', 'floral-gold': '#c9b18a', 'damask-mauve': '#b9a6b4', + 'stripe-sage': '#bfc8b2', 'geo-orange': '#caa06a', 'diamond-green': '#a9c0a4', + 'trellis-blue': '#aebfcf', 'woodchip-white': '#d7d2c6', +}; + +// Choose an archetype from a recipe's weighted bias (or honour an explicit override). +export function chooseArchetype(recipe, rArch, override) { + if (override && ARCHETYPES[override]) return override; + // fall back to a uniform bias if the recipe's is missing OR empty (e.g. an authored registry patch) + const bias = (recipe.archetypeBias && recipe.archetypeBias.length) + ? recipe.archetypeBias : ARCHETYPE_KEYS.map(k => [k, 1]); + const total = bias.reduce((s, [, w]) => s + w, 0); + let t = rArch() * total; + for (const [k, w] of bias) { t -= w; if (t <= 0) return k; } + return bias[0][0]; +} + +// Compute interior dims {W,D,H} from archetype + lot + storeys, all seeded/deterministic. +export function computeDims(ctx, recipe, archetype, lot, storeys) { + const a = ARCHETYPES[archetype] || ARCHETYPES.cosy; + const rw = ctx.stream('dim-w'), rd = ctx.stream('dim-d'); + let W = frange(rw, a.w[0], a.w[1]); + let D = frange(rd, a.d[0], a.d[1]); + if (lot && lot.w > 0 && lot.d > 0) { // adapt to the real lot: never exceed it + const availW = Math.max(4, lot.w - 0.6); // 0.3m wall thickness each side + const availD = Math.max(4, lot.d - 0.6); + W = Math.min(W, availW); + D = Math.min(D, availD); + } + W = Math.round(W * 20) / 20; D = Math.round(D * 20) / 20; // 5cm grid → stable deep-equal + const st = Math.max(1, storeys || 1); + let H = a.h + (st - 1) * 0.4; + H = Math.max(3.2, Math.min(4.5, H)); + return { W, D, H }; +} + +// Build the shell into a fresh Group. Returns geometry + spawn/exit metadata for the placer. +export function buildShell(ctx, { recipe, archetype, dims }) { + const { W, D, H } = dims; + const THREE = ctx.THREE; + const group = new THREE.Group(); + group.userData = { kind: 'interior', archetype, type: recipe.key }; + + const rMat = ctx.stream('shell-mat'); + const floorName = pick(rMat, recipe.floorBias); + const wallName = pick(rMat, recipe.wallpaperBias); + + // ── floor ── + const floorMat = ctx.mat(FLOOR_FALLBACK[floorName] || '#8a7a60', 0.95); + ctx.skin(floorMat, `assets/gen/tex-${floorName}.jpg`, { repeat: [Math.ceil(W / 3), Math.ceil(D / 3)] }); + const floor = ctx.plane(W, D, floorMat, group); + floor.rotation.x = -Math.PI / 2; + floor.receiveShadow = true; + + // ── ceiling ── + const ceil = ctx.plane(W, D, ctx.mat('#d9d4c8', 1.0), group); + ceil.rotation.x = Math.PI / 2; ceil.position.y = H; + + // ── side + back walls (west −X, east +X, north −Z) ── + const wallColor = WALL_FALLBACK[wallName] || '#cfc4ae'; + const wallDefs = [ + { x: -W / 2, z: 0, ry: Math.PI / 2, w: D, name: 'west' }, + { x: W / 2, z: 0, ry: -Math.PI / 2, w: D, name: 'east' }, + { x: 0, z: -D / 2, ry: 0, w: W, name: 'north' }, + ]; + const walls = {}; + for (const def of wallDefs) { + const m = ctx.mat(wallColor, 0.9); + ctx.skin(m, `assets/gen/wall-${wallName}.jpg`, { repeat: [def.w / 2.5, H / 2.5] }); + const me = ctx.plane(def.w, H, m, group); + me.position.set(def.x, H / 2, def.z); + me.rotation.y = def.ry; + walls[def.name] = me; + } + + // Back doorway (blocked v1): a dark recessed frame on the north wall. + const doorwayW = 1.1, doorwayH = 2.2; + const backX = (ctx.rand('backdoor') - 0.5) * (W - 3); + ctx.box(doorwayW + 0.2, doorwayH + 0.2, 0.08, ctx.mat('#3a3128', 0.8), backX, doorwayH / 2, -D / 2 + 0.05, group); // frame + ctx.box(doorwayW, doorwayH, 0.04, ctx.mat('#15110c', 0.9), backX, doorwayH / 2, -D / 2 + 0.10, group); // dark void + + // ── shopfront (south, +Z): glazing + door opening + street backdrop ── + const front = buildShopfront(ctx, group, { W, H, D, wallColor, wallName }); + + // ── lighting (travels with the group) ── + group.add(new THREE.AmbientLight(0xfff4de, 0.75)); + const lightXs = W > 9 ? [-W / 3, 0, W / 3] : [-W / 4, W / 4]; + const lightZs = D > 11 ? [-D / 3, 0, D / 3] : [-D / 4, D / 4]; + for (const lx of lightXs) for (const lz of lightZs) { + const l = new THREE.PointLight(0xfff0d0, 14, 12, 1.6); + l.position.set(lx, H - 0.3, lz); + group.add(l); + ctx.box(0.7, 0.06, 0.18, ctx.mat('#f5f2ea', 0.4, { emissive: new THREE.Color('#4a4636') }), lx, H - 0.06, lz, group); // fixture + } + + return { + group, dims, archetype, floorName, wallName, walls, + spawn: { x: front.doorCx, z: D / 2 - 1.5, ry: 0 }, // just inside, facing −Z into the shop + exits: [{ x: front.doorCx, z: D / 2, w: front.doorW, toStreet: true }], + door: { x: front.doorCx, w: front.doorW, z: D / 2 }, + places: front.places, + }; +} + +// The glazed shopfront wall with a door gap and a bright street backdrop behind the glass. +function buildShopfront(ctx, group, { W, H, D, wallColor }) { + const THREE = ctx.THREE; + const z = D / 2; + const th = 0.12; + const doorW = 1.3, doorH = 2.2; + const doorCx = W * 0.26; // door on the right quarter + const doorL = doorCx - doorW / 2, doorR = doorCx + doorW / 2; + const sill = 0.9, head = 2.35; // window band + const winL = -W / 2 + 0.12, winR = doorL - 0.18; // glazing fills left of the door + const wallMat = () => ctx.mat(wallColor, 0.9); + + const solid = (w, h, x, y) => { if (w > 0.01 && h > 0.01) ctx.box(w, h, th, wallMat(), x, y, z, group); }; + // left return pillar + solid(0.12, H, -W / 2 + 0.06, H / 2); + // sill (below window) + solid(winR - winL, sill, (winL + winR) / 2, sill / 2); + // head/spandrel (above window) + solid(winR - winL, H - head, (winL + winR) / 2, (head + H) / 2); + // pillar between window and door + solid(doorL - winR, H, (winR + doorL) / 2, H / 2); + // lintel above door + solid(doorW, H - doorH, doorCx, (doorH + H) / 2); + // right of door → east return + solid(W / 2 - doorR, H, (doorR + W / 2) / 2, H / 2); + + // the glass pane + const glass = ctx.mat('#bfe0ea', 0.05, { transparent: true, opacity: 0.16 }); + const pane = ctx.box(winR - winL, head - sill, 0.02, glass, (winL + winR) / 2, (sill + head) / 2, z - 0.02, group); + // a mullion or two + const mull = ctx.mat('#4a4038', 0.7); + ctx.box(0.05, head - sill, 0.05, mull, (winL + winR) / 2, (sill + head) / 2, z - 0.02, group); + + // door frame trim + a glass door leaf (the exit) + ctx.box(doorW + 0.1, 0.08, 0.14, mull, doorCx, doorH, z, group); + ctx.box(0.08, doorH, 0.14, mull, doorL, doorH / 2, z, group); + ctx.box(0.08, doorH, 0.14, mull, doorR, doorH / 2, z, group); + const leaf = ctx.box(doorW - 0.16, doorH - 0.1, 0.03, ctx.mat('#bfe0ea', 0.05, { transparent: true, opacity: 0.2 }), doorCx, doorH / 2, z - 0.03, group); + leaf.userData = { kind: 'exit', interactable: true, toStreet: true }; + // little EXIT plate above the door, facing inward + const plate = ctx.plane(0.6, 0.16, ctx.mat('#7a6a55', 0.5, { emissive: new THREE.Color('#2a2416') }), group); + plate.position.set(doorCx, doorH + 0.16, z - 0.06); plate.rotation.y = Math.PI; + + // ── street backdrop beyond the glazing: an unlit gradient plane (sky → horizon → footpath) ── + const backdrop = makeStreetBackdrop(ctx, W); + backdrop.position.set(0, H / 2, z + 1.6); + backdrop.rotation.y = Math.PI; // face back into the shop + group.add(backdrop); + + return { doorCx, doorW, places: [leaf] }; +} + +// Cheap inside-looking-out backdrop: a canvas gradient with a hint of buildings + footpath. +// Unlit (MeshBasicMaterial) so it reads as bright daylight regardless of interior lights. +function makeStreetBackdrop(ctx, W) { + const THREE = ctx.THREE; + const cv = document.createElement('canvas'); cv.width = 256; cv.height = 128; + const x = cv.getContext('2d'); + const sky = x.createLinearGradient(0, 0, 0, 128); + sky.addColorStop(0, '#8fb6d8'); sky.addColorStop(0.55, '#cfe0ec'); sky.addColorStop(0.62, '#d8d2c4'); + sky.addColorStop(1, '#9c968a'); + x.fillStyle = sky; x.fillRect(0, 0, 256, 128); + // a row of muted facades across the horizon + const cols = ['#8a7a68', '#9a8a6a', '#7a8a80', '#a08a7a', '#7a7488']; + for (let i = 0; i < 8; i++) { + x.fillStyle = cols[i % cols.length]; + const bw = 24 + (i * 37 % 20), bx = i * 33 - 8, bh = 30 + (i * 53 % 34); + x.fillRect(bx, 62 - bh, bw, bh); + } + x.fillStyle = '#6f6a60'; x.fillRect(0, 78, 256, 50); // footpath + x.fillStyle = '#5a5650'; x.fillRect(0, 74, 256, 4); // kerb line + const tex = ctx.canvasTexture(cv); + const mat = new THREE.MeshBasicMaterial({ map: tex }); + ctx._materials.add(mat); + const w = Math.max(W, 6) * 1.4; + const geo = ctx.geom(new THREE.PlaneGeometry(w, w * 0.5)); + return new THREE.Mesh(geo, mat); +} diff --git a/web/js/interiors/stock.js b/web/js/interiors/stock.js new file mode 100644 index 0000000..6186b6a --- /dev/null +++ b/web/js/interiors/stock.js @@ -0,0 +1,213 @@ +// PROCITY Lane C — visual stock (v1). Fills the fittings' `slots` with product silhouettes: +// canvas-texture cardboard sleeves / spines / boxes / snacks / magazines with price stickers +// (the dig.js sleeve trick — coloured cardboard + sticker). Real item data is the content phase; +// this stays purely cosmetic and byte-identical per seed. +// +// stock.fill(ctx, fitting, { shop, recipe, stockKind, adapter }, r) +// iterates fitting.slots and adds child meshes into fitting.group. +// +// stockAdapter hook (content phase): `adapter(shop, slotKind) => { texture } | { mesh } | null`. +// If it returns a texture, that texture skins the item face; a mesh replaces the item entirely; +// null (default) → procedural placeholder. BaseGod / GODVERSE data plugs in here without touching +// layout or fittings code. +// +// PERF + LEAK SAFETY: face textures are POOLED per room (a small seeded set reused across many +// items) → few CanvasTextures to build and dispose, fast builds, soak returns to baseline. + +import { pick } from './context.js'; + +// Muted 90s stock palette (cardboard, plastic, faded card). +const CARD = ['#b49b72', '#a6b472', '#72a6b4', '#b47298', '#8a72b4', '#b48a72', '#72b48a', '#b4a672']; +const SPINE = ['#7a2a2a', '#2a4a7a', '#2a6a4a', '#7a5a2a', '#5a2a6a', '#2a5a6a', '#6a6a2a', '#7a3a5a']; +const SNACK = ['#e04a3a', '#f0b428', '#3a9ae0', '#4ac04a', '#e04a9a', '#f07028', '#a04ae0']; + +// ── per-room face-texture pool ────────────────────────────────────────────────────── +// A pooled canvas "face" for a stock kind: coloured card + a little price sticker + label bars. +function makeFace(ctx, kind, r) { + const cv = document.createElement('canvas'); + cv.width = 64; cv.height = kind === 'spine' ? 128 : 64; + const x = cv.getContext('2d'); + const pal = kind === 'spine' ? SPINE : kind === 'snack' ? SNACK : CARD; + const base = pick(r, pal); + x.fillStyle = base; x.fillRect(0, 0, cv.width, cv.height); + // faded border + x.strokeStyle = 'rgba(0,0,0,0.18)'; x.lineWidth = 3; x.strokeRect(2, 2, cv.width - 4, cv.height - 4); + if (kind === 'spine') { + // title bars down the spine + x.fillStyle = 'rgba(255,255,255,0.82)'; + for (let i = 0; i < 3; i++) x.fillRect(10, 18 + i * 30 + (r() * 6 | 0), cv.width - 20, 8); + x.fillStyle = 'rgba(0,0,0,0.3)'; x.fillRect(8, cv.height - 20, cv.width - 16, 10); + } else { + // a cover blob + label lines + x.fillStyle = 'rgba(255,255,255,0.16)'; + x.beginPath(); x.arc(32 + (r() - 0.5) * 16, 26, 12 + r() * 6, 0, 6.3); x.fill(); + x.fillStyle = 'rgba(255,255,255,0.75)'; + x.fillRect(8, 44, 34, 5); x.fillRect(8, 52, 22, 4); + } + // the op-shop price sticker — a fluoro dot with a $ scrawl + const sx = cv.width - 18, sy = 14; + x.fillStyle = '#f4e11a'; x.beginPath(); x.arc(sx, sy, 9, 0, 6.3); x.fill(); + x.fillStyle = '#c1121f'; x.font = 'bold 11px Arial'; x.textAlign = 'center'; x.textBaseline = 'middle'; + x.fillText('$' + (1 + (r() * 9 | 0)), sx, sy + 1); + return ctx.canvasTexture(cv); +} + +function facePool(ctx, kind, r, n = 6) { + const key = `_facePool_${kind}`; + if (!ctx[key]) { ctx[key] = []; for (let i = 0; i < n; i++) ctx[key].push(makeFace(ctx, kind, r)); } + return ctx[key]; +} + +// A garment silhouette (ported from 90sDJsim garmentCanvas), pooled per room. +const GARMENTS = ['tee', 'hoodie', 'jacket', 'dress', 'pants', 'shirt']; +function makeGarment(ctx, r) { + const shape = GARMENTS[(r() * GARMENTS.length) | 0]; + const color = new ctx.THREE.Color().setHSL(r(), 0.45, 0.4 + r() * 0.2); + const cv = document.createElement('canvas'); cv.width = 128; cv.height = 160; + const x = cv.getContext('2d'); + const col = '#' + color.getHexString(); + const dark = '#' + color.clone().offsetHSL(0, 0.05, -0.14).getHexString(); + x.fillStyle = col; x.strokeStyle = dark; x.lineWidth = 4; x.lineJoin = 'round'; + const P = pts => { x.beginPath(); x.moveTo(pts[0][0], pts[0][1]); for (let i = 1; i < pts.length; i++) x.lineTo(pts[i][0], pts[i][1]); x.closePath(); x.fill(); x.stroke(); }; + if (shape === 'pants') P([[40, 8], [88, 8], [90, 152], [70, 152], [64, 62], [58, 152], [38, 152]]); + else if (shape === 'dress') { P([[48, 20], [80, 20], [104, 152], [24, 152]]); x.fillStyle = dark; x.beginPath(); x.ellipse(64, 22, 11, 6, 0, 0, 6.3); x.fill(); } + else { + P([[16, 46], [46, 26], [46, 60], [22, 78]]); P([[112, 46], [82, 26], [82, 60], [106, 78]]); + P([[42, 24], [86, 24], [86, 142], [42, 142]]); + x.fillStyle = dark; x.beginPath(); x.ellipse(64, 26, 12, 7, 0, 0, 6.3); x.fill(); + if (shape === 'hoodie') { x.fillStyle = col; x.beginPath(); x.arc(64, 24, 15, Math.PI, 0); x.fill(); x.stroke(); } + } + x.fillStyle = 'rgba(255,255,255,0.22)'; x.fillRect(55, 62, 18, 20); + return ctx.canvasTexture(cv); +} +function garmentPool(ctx, r, n = 8) { + if (!ctx._garmentPool) { ctx._garmentPool = []; for (let i = 0; i < n; i++) ctx._garmentPool.push(makeGarment(ctx, r)); } + return ctx._garmentPool; +} + +// ── fill entry point ───────────────────────────────────────────────────────────────── +export function fill(ctx, fitting, opts, r) { + for (const slot of fitting.slots || []) fillSlot(ctx, fitting.group, slot, opts, r); +} + +function fillSlot(ctx, parent, slot, opts, r) { + switch (slot.kind) { + case 'sleeve': return binFan(ctx, parent, slot, opts, r); + case 'garment': return hang(ctx, parent, slot, opts, r); + case 'treasure': return treasures(ctx, parent, slot, opts, r); + default: return shelfLine(ctx, parent, slot, opts, r); // box / spine / snack / magazine + } +} + +// Ask the adapter for a face texture (content phase); null → pooled procedural face. +function faceTex(ctx, kind, opts, r) { + if (opts.adapter) { + const got = opts.adapter(opts.shop, kind); + if (got && got.texture) return got.texture; + } + return pick(r, facePool(ctx, kind === 'magazine' ? 'card' : kind, r)); +} + +// Packed leaning sleeves in a bin/crate; front few wear covers (dig.js look). +function binFan(ctx, parent, slot, opts, r) { + const n = slot.count || 12; + const fw = slot.faceW || 0.3, h = slot.height || 0.3, run = slot.run || 0.5, tilt = slot.tilt ?? -0.4; + const step = run / Math.max(1, n); + const pool = facePool(ctx, 'card', r); + for (let i = 0; i < n; i++) { + const z = -run / 2 + (i + 0.5) * step; + const front = i >= n - 4; // front sleeves show a cover + const faceMat = front ? ctx.mat('#ffffff', 0.7) : ctx.mat(pick(r, CARD), 0.85); + if (front) { faceMat.map = pick(r, pool); faceMat.color.set('#ffffff'); faceMat.needsUpdate = true; } + const sl = ctx.box(fw, h, 0.012, faceMat, slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z, parent); + sl.rotation.x = tilt + (r() - 0.5) * 0.05; + sl.rotation.z = (r() - 0.5) * 0.04; + } +} + +// Neat row along local X. Handles box (upright), spine (edge-out), snack (small), magazine (leaning). +function shelfLine(ctx, parent, slot, opts, r) { + const kind = slot.kind; + const run = slot.run || 0.8, depth = slot.depth || 0.3, clear = slot.height || 0.3; + const scatter = slot.scatter; + const n = Math.max(1, slot.count || 4); + const pool = kind === 'spine' ? facePool(ctx, 'spine', r) : facePool(ctx, kind === 'snack' ? 'snack' : 'card', r); + + if (scatter) { // tables / pegboards: random small goods + for (let i = 0; i < n; i++) { + const s = 0.09 + r() * 0.11; + const m = ctx.mat(pick(r, CARD), 0.85); + if (r() < 0.5) { m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; } + const b = ctx.box(s, slot.wall ? s * 1.4 : s, slot.wall ? 0.03 : s, m, + slot.x + (r() - 0.5) * (run - s), slot.y + (slot.wall ? (r() - 0.5) * (clear - s) : s / 2), + slot.z + (slot.wall ? 0.03 : (r() - 0.5) * (depth - s)), parent); + b.rotation.y = (r() - 0.5) * 0.6; + } + return; + } + + if (kind === 'spine') { // books / VHS: thin spines edge-out + const th = Math.min(0.03, run / (n + 2)); + const step = (run - 0.04) / n; + for (let i = 0; i < n; i++) { + const m = ctx.mat('#ffffff', 0.7); m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; + const bh = clear * (0.8 + r() * 0.2); + const b = ctx.box(step * 0.86, bh, depth, m, slot.x - run / 2 + (i + 0.5) * step, slot.y + bh / 2, slot.z, parent); + if (slot.lean && i > n - 3) b.rotation.z = 0.18; // a couple flopped over + } + return; + } + + if (kind === 'magazine') { // leaning mag faces + const step = run / n; + for (let i = 0; i < n; i++) { + const m = ctx.mat('#ffffff', 0.7); m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; + const b = ctx.box(step * 0.9, clear * 0.9, 0.01, m, slot.x - run / 2 + (i + 0.5) * step, slot.y + clear * 0.45, slot.z, parent); + b.rotation.x = slot.tilt ?? -0.35; + } + return; + } + + // default 'box' / 'snack': upright cartons in a row, faces forward (+Z) + const bw = Math.min((run - 0.02) / n, kind === 'snack' ? 0.12 : 0.28); + const step = run / n; + for (let i = 0; i < n; i++) { + const bh = clear * (kind === 'snack' ? 0.6 : 0.62 + r() * 0.28); + const bd = Math.min(depth, kind === 'snack' ? 0.1 : 0.26); + const m = ctx.mat('#ffffff', 0.7); + m.map = faceTex(ctx, kind, opts, r); m.color.set('#ffffff'); m.needsUpdate = true; + const side = ctx.mat(pick(r, kind === 'snack' ? SNACK : CARD), 0.85); + const g = ctx.boxGeo(bw * 0.88, bh, bd); // shared geometry + const mesh = new ctx.THREE.Mesh(g, [side, side, side, side, m, side]); // face (+Z) wears the cover + mesh.position.set(slot.x - run / 2 + (i + 0.5) * step, slot.y + bh / 2, slot.z + depth / 2 - bd / 2 - 0.01); + parent.add(mesh); // face `m` + `side` already tracked via ctx.mat; geometry via ctx.geom + } +} + +// Garments hung along a rail. +function hang(ctx, parent, slot, opts, r) { + const n = slot.count || 10, run = slot.run || 1.4, h = slot.height || 0.52; + const pool = garmentPool(ctx, r); + for (let i = 0; i < n; i++) { + const gx = slot.x - run / 2 + (run) * (i / (n - 1 || 1)); + const m = ctx.mat('#ffffff', 0.92, { transparent: true, alphaTest: 0.4, side: ctx.THREE.DoubleSide }); + m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; + const g = ctx.planeGeo(0.34, h); + const p = new ctx.THREE.Mesh(g, m); + p.position.set(gx, slot.y - 0.05 - h / 2, slot.z + (r() - 0.5) * 0.03); + p.rotation.y = (r() - 0.5) * 0.15; + parent.add(p); + } +} + +// Small treasures on a glass-case shelf. +function treasures(ctx, parent, slot, opts, r) { + const n = slot.count || 4, run = slot.run || 1.0; + const cols = ['#c8a13a', '#b0b0b8', '#8a5a3a', '#3a6a8a', '#8a3a5a']; + for (let i = 0; i < n; i++) { + const s = 0.06 + r() * 0.06; + const b = ctx.box(s * (1 + r()), s, s, ctx.mat(pick(r, cols), 0.4, { metalness: 0.3 }), + slot.x - run / 2 + (run) * ((i + 0.5) / n), slot.y + s / 2, slot.z + (r() - 0.5) * (slot.depth || 0.2) * 0.5, parent); + b.rotation.y = r() * Math.PI; + } +} diff --git a/web/js/interiors/theme.js b/web/js/interiors/theme.js new file mode 100644 index 0000000..14d54f0 --- /dev/null +++ b/web/js/interiors/theme.js @@ -0,0 +1,236 @@ +// PROCITY Lane C — type theming. Every CITY_SPEC shop type gets a distinct interior recipe so a +// `record` shop instantly reads different from a `toy` shop the moment the door opens. +// +// LANE_C says "import SHOP_TYPES from core/registry.js" — Lane A hasn't landed registry.js yet, so +// this lane stays FULLY STANDALONE: the canonical recipes live here. When registry.js exists, Lane F +// can call `mergeRegistry(SHOP_TYPES)` to let authored registry data override any field. No hard +// dependency, so the test page runs with zero other lanes. + +// Fitting `kind`s referenced below must exist in fittings.js FITTINGS registry. +// Zones the layout placer understands: 'window' (front, by glazing), 'aisle' (centre rows), +// 'wall' (perimeter, uses wall-slots), 'centre' (free-standing island), 'counter' (till corner). +// +// Recipe fields: +// archetypeBias : weighted room-shape preference (keys of shell.ARCHETYPES) — 'auto' picks from here +// wallpaperBias : biased pick from web/assets/gen/wall-*.jpg (names without prefix/suffix) +// floorBias : biased pick from carpet/lino/board floor names +// clutter : 0..1 density multiplier (some shops crammed, some sparse) +// counterPos : 'door' | 'forward' | 'corner' | 'back' — where the till lives +// fittings : the mix. Each {kind, zone, min, max, priority}. Counts scale with clutter+area. +// priority: higher = more essential; flood-fill removes low-priority first if the +// door→counter path is ever blocked. Counter is always highest. +// stockKind : what stock.js renders into this shop's carriers +// signFlavour : seeded inside signage strings, themed + +export const ARCH = { cosy: 'cosy', gallery: 'gallery', wide: 'wide', hall: 'hall', pokey: 'pokey' }; + +const RECIPES = { + // ── RECORD ───────────────────────────────────────────────────────────────────── + record: { + label: 'Record Store', + archetypeBias: [['cosy', 3], ['gallery', 2], ['pokey', 1]], + wallpaperBias: ['woodchip-white', 'stripe-sage', 'geo-orange'], + floorBias: ['boards-polished', 'carpet-greygreen', 'lino-cork'], + clutter: 0.85, + counterPos: 'corner', + fittings: [ + { kind: 'recordBin', zone: 'aisle', min: 2, max: 5, priority: 5 }, + { kind: 'crate', zone: 'centre', min: 1, max: 3, priority: 3 }, + { kind: 'wallShelf', zone: 'wall', min: 1, max: 3, priority: 3 }, + { kind: 'listeningCorner', zone: 'window', min: 1, max: 1, priority: 4 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'sleeves', + signFlavour: ['NEW ARRIVALS', 'VINYL $5 EA', 'SORRY NO REFUNDS', '2ND HAND CDs', 'JAZZ · SOUL · FUNK'], + }, + + // ── OP SHOP ──────────────────────────────────────────────────────────────────── + opshop: { + label: 'Op Shop', + archetypeBias: [['cosy', 3], ['wide', 2], ['hall', 1]], + wallpaperBias: ['floral-cream', 'damask-mauve', 'trellis-blue', 'floral-gold'], + floorBias: ['carpet-swirl', 'carpet-mustard', 'lino-check'], + clutter: 1.0, + counterPos: 'corner', + fittings: [ + { kind: 'clothesRack', zone: 'centre', min: 2, max: 5, priority: 5 }, + { kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 }, + { kind: 'bookshelf', zone: 'wall', min: 1, max: 2, priority: 3 }, + { kind: 'trestleTable', zone: 'aisle', min: 1, max: 2, priority: 3 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'garments', + signFlavour: ['ALL CLOTHES $2', 'BAG SALE SAT', 'BRIC-A-BRAC', 'PROCEEDS TO CHARITY', 'HALF PRICE'], + }, + + // ── TOY ──────────────────────────────────────────────────────────────────────── + toy: { + label: 'Toy Shop', + archetypeBias: [['wide', 3], ['cosy', 2], ['hall', 1]], + wallpaperBias: ['geo-orange', 'diamond-green', 'trellis-blue'], + floorBias: ['lino-check', 'carpet-greygreen', 'lino-cork'], + clutter: 0.75, + counterPos: 'corner', + fittings: [ + { kind: 'cubeShelf', zone: 'wall', min: 2, max: 4, priority: 5 }, + { kind: 'trestleTable', zone: 'aisle', min: 1, max: 3, priority: 3 }, + { kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 4 }, + { kind: 'metalShelf', zone: 'aisle', min: 1, max: 2, priority: 3 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'boxes', + signFlavour: ['NEW! ', 'AS SEEN ON TV', 'PLAY & SAVE', 'BATTERIES EXTRA', 'LAYBY WELCOME'], + }, + + // ── BOOK ─────────────────────────────────────────────────────────────────────── + book: { + label: 'Book Barn', + archetypeBias: [['hall', 3], ['gallery', 2], ['cosy', 1]], + wallpaperBias: ['damask-mauve', 'woodchip-white', 'floral-gold'], + floorBias: ['carpet-greygreen', 'boards-polished', 'carpet-swirl'], + clutter: 0.9, + counterPos: 'corner', + fittings: [ + { kind: 'bookshelf', zone: 'aisle', min: 3, max: 5, priority: 5 }, + { kind: 'bookshelf', zone: 'wall', min: 2, max: 3, priority: 4 }, + { kind: 'spinnerRack', zone: 'centre', min: 1, max: 2, priority: 3 }, + { kind: 'armchair', zone: 'window', min: 1, max: 1, priority: 2 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'spines', + signFlavour: ['ALL BOOKS $2', 'FICTION A–M', 'RARE & COLLECTABLE', 'NO HAGGLING (haggle at till)', 'POETRY ↑'], + }, + + // ── VIDEO ────────────────────────────────────────────────────────────────────── + video: { + label: 'Video Rental', + archetypeBias: [['gallery', 3], ['cosy', 2], ['wide', 1]], + wallpaperBias: ['diamond-green', 'geo-orange', 'stripe-sage'], + floorBias: ['carpet-swirl', 'carpet-greygreen', 'lino-cork'], + clutter: 0.85, + counterPos: 'door', + fittings: [ + { kind: 'vhsAisle', zone: 'aisle', min: 2, max: 4, priority: 5 }, + { kind: 'vhsAisle', zone: 'wall', min: 1, max: 2, priority: 4 }, + { kind: 'returnsSlot', zone: 'counter', min: 1, max: 1, priority: 4 }, + { kind: 'pegboard', zone: 'wall', min: 1, max: 2, priority: 2 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'spines', + signFlavour: ['NEW RELEASES', 'OVERNIGHT $3', 'BE KIND REWIND', 'WEEKLY $2', 'DROP RETURNS HERE'], + }, + + // ── PAWN ─────────────────────────────────────────────────────────────────────── + pawn: { + label: 'Pawnbroker', + archetypeBias: [['pokey', 3], ['cosy', 2], ['gallery', 1]], + wallpaperBias: ['woodchip-white', 'stripe-sage', 'damask-mauve'], + floorBias: ['lino-cork', 'boards-polished', 'lino-check'], + clutter: 0.7, + counterPos: 'forward', // counter is right up front; goods behind it + fittings: [ + { kind: 'glassCase', zone: 'counter', min: 1, max: 3, priority: 6 }, + { kind: 'pegboard', zone: 'wall', min: 1, max: 3, priority: 4 }, // wall hooks + { kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 3 }, + { kind: 'barredWindow', zone: 'window', min: 1, max: 1, priority: 3 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'mixed', + signFlavour: ['WE BUY ANYTHING', 'CASH LOANS', 'GOLD · TOOLS · GEAR', 'ID REQUIRED', 'UNREDEEMED PLEDGES'], + }, + + // ── MILK BAR ───────────────────────────────────────────────────────────────────── + milkbar: { + label: 'Milk Bar', + archetypeBias: [['pokey', 3], ['cosy', 2]], + wallpaperBias: ['diamond-green', 'geo-orange', 'stripe-sage'], + floorBias: ['lino-check', 'lino-cork', 'carpet-mustard'], + clutter: 0.8, + counterPos: 'door', // counter faces the door — you're served on the way in + fittings: [ + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + { kind: 'fridge', zone: 'wall', min: 1, max: 3, priority: 6 }, + { kind: 'magazineRack', zone: 'window', min: 1, max: 2, priority: 4 }, + { kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 }, + { kind: 'cubeShelf', zone: 'aisle', min: 0, max: 1, priority: 2 }, + ], + stockKind: 'snacks', + signFlavour: ['MILK · BREAD · SMOKES', 'PIES HOT', 'PAPERS IN', 'PHONECARDS', 'NO SCHOOL KIDS AT LUNCH'], + }, + + // ── DEPT (anchor) ────────────────────────────────────────────────────────────── + dept: { + label: 'Department Store', + archetypeBias: [['hall', 4], ['wide', 1]], + wallpaperBias: ['floral-gold', 'damask-mauve', 'trellis-blue'], + floorBias: ['carpet-greygreen', 'carpet-swirl', 'boards-polished'], + clutter: 0.7, + counterPos: 'corner', + fittings: [ + { kind: 'cubeShelf', zone: 'aisle', min: 2, max: 4, priority: 4 }, + { kind: 'clothesRack', zone: 'centre', min: 2, max: 4, priority: 4 }, + { kind: 'glassCase', zone: 'aisle', min: 1, max: 2, priority: 3 }, + { kind: 'metalShelf', zone: 'wall', min: 2, max: 4, priority: 3 }, + { kind: 'escalator', zone: 'centre', min: 1, max: 1, priority: 5 }, // the grand-hall centrepiece + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 }, + ], + stockKind: 'mixed', + signFlavour: ['MANCHESTER ↑', 'MENSWEAR', 'HABERDASHERY', 'GROUND FLOOR', 'STAFF ONLY'], + }, + + // ── STALL (market) ─────────────────────────────────────────────────────────────── + stall: { + label: 'Market Stall', + archetypeBias: [['wide', 3], ['hall', 1]], + wallpaperBias: ['woodchip-white', 'stripe-sage'], // barely walled; open feel + floorBias: ['lino-cork', 'boards-polished'], + clutter: 0.95, + counterPos: 'forward', + fittings: [ + { kind: 'trestleTable', zone: 'aisle', min: 3, max: 6, priority: 5 }, + { kind: 'crate', zone: 'centre', min: 2, max: 5, priority: 3 }, + { kind: 'clothesRack', zone: 'wall', min: 0, max: 2, priority: 2 }, + { kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 8 }, + ], + stockKind: 'boxes', + signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'], + }, +}; + +// Aliases: accept thriftgod / Overpass-style type names and map to canonical recipes. +const ALIAS = { + music: 'record', records: 'record', + charity: 'opshop', second_hand: 'opshop', secondhand: 'opshop', antiques: 'opshop', op_shop: 'opshop', + toys: 'toy', toyshop: 'toy', + books: 'book', bookshop: 'book', bookbarn: 'book', + video_games: 'video', games: 'video', vhs: 'video', rental: 'video', + pawnbroker: 'pawn', pawnshop: 'pawn', + milk_bar: 'milkbar', deli: 'milkbar', corner_store: 'milkbar', + department: 'dept', anchor: 'dept', + market: 'stall', stalls: 'stall', +}; + +export const SHOP_TYPES = Object.keys(RECIPES); + +export function canonicalType(type) { + if (!type) return 'opshop'; + const t = String(type).toLowerCase(); + if (RECIPES[t]) return t; + if (ALIAS[t]) return ALIAS[t]; + return 'opshop'; // sensible default: an op shop takes anything +} + +export function getRecipe(type) { + const key = canonicalType(type); + return { key, ...RECIPES[key] }; +} + +// Lane F hook: let an authored registry override recipe fields without editing this lane. +// registry: { [typeKey]: partialRecipe }. Shallow-merges over the built-in recipe. +export function mergeRegistry(registry) { + if (!registry) return; + for (const [k, patch] of Object.entries(registry)) { + const key = canonicalType(k); + if (RECIPES[key]) Object.assign(RECIPES[key], patch); + } +}