// 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); }