// PROCITY Lane B — fixture_plan.js // A hand-authored CityPlan (CITY_SPEC schema v1) so the streetscape is buildable *before* // Lane A ships generatePlan(). Everything here is deterministic layout math (no Math.random, // no THREE) — pure JSON-serialisable data. When Lane A lands, index.html swaps in // generatePlan(seed) and this file becomes the fallback + schema reference. // // LAYOUT (metres, +Y up, ground = XZ, origin = centre of the main square): // // north (backstreets: houses up the cross street) // │ cross st (X=0) // NW shops (Z=+11, face −Z) ──────┼────── NE shops // ═══════════════ MAIN STREET (Z=0, east-west) ═══════════════ // SW shops (Z=−11, face +Z) ──────┼────── SE shops // // Orientation convention (documented once, obeyed by buildings.js): // A lot is an oriented rectangle centred (x,z), footprint w (frontage) × d (depth). // `ry` rotates the building about +Y. At ry=0 the shopfront outward-normal is +Z. // Rotation by ry maps +Z → (sin ry, 0, cos ry), so: // front faces +Z → ry = 0 (south side of an E-W street) // front faces −Z → ry = π (north side of an E-W street) // front faces +X → ry = +π/2 (west side of a N-S street) // front faces −X → ry = −π/2 (east side of a N-S street) import { rng, pick, irange } from '../core/prng.js'; // ── Shop-type registry (Lane A owns web/js/core/registry.js; it hasn't landed yet, so Lane B // carries the slice it needs. facade pools are the CITY_SPEC table. Lane F reconciles.) ── export const SHOP_REGISTRY = { record: { facades: ['timber-teal', 'arcade-tile', 'djsim-record'], signBg: '#1f2e2b', signFg: '#e8d9a0' }, opshop: { facades: ['weatherboard', 'fibro-blue', 'djsim-opshop'], signBg: '#2c2820', signFg: '#f0e2c0' }, toy: { facades: ['stucco-pink', 'deco-pastel'], signBg: '#7a1f5a', signFg: '#ffe6f2' }, book: { facades: ['federation', 'sandstone'], signBg: '#2a231a', signFg: '#f3e6c4' }, video: { facades: ['stripmall', 'djsim-video'], signBg: '#141a2c', signFg: '#8fb7ff' }, pawn: { facades: ['besser', 'grimy', 'djsim-pawn'], signBg: '#f5c400', signFg: '#c1121f' }, milkbar: { facades: ['djsim-milkbar', 'brickveneer'], signBg: '#b21f1f', signFg: '#fff3d6' }, dept: { facades: ['terrazzo', 'djsim-dept'], signBg: '#20313a', signFg: '#ffd75e' }, stall: { facades: ['market'], signBg: '#2c2820', signFg: '#f0e2c0' }, }; export const SHOP_TYPES = Object.keys(SHOP_REGISTRY); // Parody-name word banks (kept small; deterministic pick per shop). 90s-AU flavour. const NAME_BANK = { record: { a: ['Groove', 'Wax', 'Vinyl', 'Spin', 'Needle', 'Rotary'], b: ['Junction', 'Merchants', 'Traders', 'Exchange', 'Bin'] }, opshop: { a: ['Vinnies', 'Salvos', 'Second', 'Thrifty', 'Endeavour', 'Lifeline'], b: ['Chance', 'Op Shop', 'Finds', 'Bazaar', 'Nook'] }, toy: { a: ['Toyworld', 'Kiddie', 'Wonder', 'Bright', 'Jolly'], b: ['Toys', 'Playtime', 'Corner', 'Emporium'] }, book: { a: ['Dog-Eared', 'Second Story', 'Turning', 'Foxed', 'Marginalia'], b: ['Books', 'Book Barn', 'Reads', 'Pages'] }, video: { a: ['Blockheads', 'Video', 'Reel', 'Nightowl', 'Civic'], b: ['Video', 'Rental', 'Ezy Hire', 'Movies'] }, pawn: { a: ['Cash', 'Quick', 'Second Chance', 'City', 'Honest'], b: ['Converters', 'Pawn', 'Loans', 'Traders'] }, milkbar: { a: ['Corner', 'Lucky', 'Rio', 'Astoria', 'Sunny'], b: ['Milk Bar', 'Deli', 'Cafe', 'Store'] }, dept: { a: ['Coles', 'Waltons', 'Grace Bros', 'Fossey’s', 'Venture'], b: ['Variety', 'Department', 'Emporium', ''] }, stall: { a: ['Trash & Treasure', 'Sunday', 'Boot', 'Market'], b: ['Stall', 'Table', 'Trestle'] }, }; function makeName(seed, type) { const r = rng(seed, 'name', 0); const bank = NAME_BANK[type] || NAME_BANK.opshop; const a = pick(r, bank.a), b = pick(r, bank.b); return b ? `${a} ${b}`.trim() : a; } // Deterministic hours: most open 9–5-ish, some late, a few closed-feeling. function makeHours(seed) { const r = rng(seed, 'hours', 0); const open = irange(r, 7, 10); const close = irange(r, 16, 21); return [open, close]; } // Facade skin for a shop, seeded from its type pool. function pickFacade(seed, type) { const r = rng(seed, 'facade', 0); return pick(r, (SHOP_REGISTRY[type] || SHOP_REGISTRY.opshop).facades); } // ── Geometry constants for the fixture town ── const W = 8; // terraced shop frontage width (m) const D = 8; // shop depth (m) const N_FRONT = 11; // north-side frontage plane Z (fronts face −Z) const S_FRONT = -11; // south-side frontage plane Z (fronts face +Z) const CROSS_W = -11; // cross-street west frontage plane X (fronts face +X) const CROSS_E = 11; // cross-street east frontage plane X (fronts face −X) // The terraced blocks. Four along the north frontage + two on the south near the plaza, spread so // the main street spans ~6 chunks (streaming visibly kicks in walking end-to-end). Every archetype // shows up. 8m shops; the plaza gap sits around the origin. const BLOCKS = [ { key: 'NW2', side: 'N', xs: [-140, -132, -124, -116], types: ['record', 'opshop', 'book', 'milkbar'] }, { key: 'NW1', side: 'N', xs: [-52, -44, -36, -28], types: ['video', 'toy', 'pawn', 'record'] }, { key: 'NE1', side: 'N', xs: [28, 36, 44, 52], types: ['opshop', 'book', 'milkbar', 'video'] }, { key: 'NE2', side: 'N', xs: [116, 124, 132, 140], types: ['dept', 'toy', 'pawn', 'opshop'] }, { key: 'SW', side: 'S', xs: [-52, -44, -36, -28], types: ['milkbar', 'record', 'opshop', 'book'] }, { key: 'SE', side: 'S', xs: [28, 36, 44, 52], types: ['toy', 'video', 'pawn', 'record'] }, ]; let _uid = 0; const uid = (p) => `${p}${_uid++}`; function buildShopLots(citySeed, lots, shops, blocks) { _uid = 0; for (const bd of BLOCKS) { const north = bd.side === 'N'; const z = north ? N_FRONT + D / 2 : S_FRONT - D / 2; const ry = north ? Math.PI : 0; const frontEdge = north ? 'S' : 'N'; const xs = bd.xs, types = bd.types; const minX = Math.min(...xs) - W / 2, maxX = Math.max(...xs) + W / 2; const zNear = north ? N_FRONT : S_FRONT; const zFar = north ? N_FRONT + D : S_FRONT - D; const blockId = `block-${bd.key}`; blocks.push({ id: blockId, district: 'main', poly: [[minX, Math.min(zNear, zFar)], [maxX, Math.min(zNear, zFar)], [maxX, Math.max(zNear, zFar)], [minX, Math.max(zNear, zFar)]], }); xs.forEach((x, i) => { const type = types[i % types.length]; const lotId = uid('lot-'); const isAnchor = type === 'dept'; const storeys = isAnchor ? 3 : (i % 3 === 0 ? 2 : 1); lots.push({ id: lotId, block: blockId, x, z, w: W, d: D, ry, frontEdge, use: isAnchor ? 'anchor' : 'shop', }); const seed = citySeed + _uid * 2654435761 % 0xffffffff; const s = (seed >>> 0); shops.push({ id: uid('shop-'), lot: lotId, type, name: makeName(s, type), sign: makeName(s, type), seed: s, facadeSkin: pickFacade(s, type), storeys, hours: makeHours(s), }); }); } } function buildHouses(citySeed, lots) { // Backstreets: houses lining the cross street north of the square. const specs = [ { x: CROSS_W - 5, z: 36, ry: Math.PI / 2, frontEdge: 'E' }, { x: CROSS_W - 5, z: 60, ry: Math.PI / 2, frontEdge: 'E' }, { x: CROSS_E + 5, z: 48, ry: -Math.PI / 2, frontEdge: 'W' }, { x: CROSS_E + 5, z: 72, ry: -Math.PI / 2, frontEdge: 'W' }, ]; specs.forEach((sp, i) => { lots.push({ id: `lot-house-${i}`, block: 'block-back', x: sp.x, z: sp.z, w: 9, d: 10, ry: sp.ry, frontEdge: sp.frontEdge, use: 'house', }); }); } function buildStalls(citySeed, lots, shops) { // Two market stalls in the square (brickpave plaza around the origin). const specs = [{ x: -9, z: 6, ry: 0 }, { x: 9, z: -6, ry: Math.PI }]; specs.forEach((sp, i) => { const lotId = `lot-stall-${i}`; lots.push({ id: lotId, block: 'block-square', x: sp.x, z: sp.z, w: 4, d: 3, ry: sp.ry, use: 'stall', frontEdge: 'S' }); const s = (citySeed + 7777 + i * 131) >>> 0; shops.push({ id: `shop-stall-${i}`, lot: lotId, type: 'stall', name: makeName(s, 'stall'), sign: makeName(s, 'stall'), seed: s, facadeSkin: 'market', storeys: 1, hours: [8, 14], }); }); } /** * fixturePlan(citySeed) → CityPlan (schema v1). Deterministic in citySeed. */ export function fixturePlan(citySeed = 20261990) { citySeed = citySeed >>> 0; const lots = [], shops = [], blocks = []; buildShopLots(citySeed, lots, shops, blocks); buildHouses(citySeed, lots); buildStalls(citySeed, lots, shops); const r = rng(citySeed, 'city', 0); const plan = { version: 1, citySeed, name: pick(r, ['Wollamby', 'Dunroamin', 'Barretta', 'Coolabah', 'Mungindi', 'Yarraville Junction']), size: { w: 1024, d: 1024 }, districts: [ { id: 'main', kind: 'mainstreet', cx: 0, cz: 0 }, { id: 'square', kind: 'market', cx: 0, cz: 0 }, { id: 'back', kind: 'backstreets', cx: 0, cz: 55 }, ], streets: { nodes: [ { id: 'n0', x: -155, z: 0 }, // west end, main { id: 'n1', x: 0, z: 0 }, // central intersection / square { id: 'n2', x: 155, z: 0 }, // east end, main { id: 'n3', x: 0, z: 95 }, // north end, cross street ], edges: [ { id: 'e0', a: 'n0', b: 'n1', width: 14, kind: 'main' }, { id: 'e1', a: 'n1', b: 'n2', width: 14, kind: 'main' }, { id: 'e2', a: 'n1', b: 'n3', width: 8, kind: 'side' }, ], }, blocks, lots, shops, // Which sky this town gets (Lane B lighting reads this; seeded so a town's weather is stable). sky: pick(r, ['golden-arvo', 'spring-puffy', 'endless-blue', 'high-cirrus', 'summer-storm', 'outback-dusk']), }; return plan; } // Default fixture used by index.html when citygen is absent. export default fixturePlan;