// PROCITY Lane C — THE R41 FITTING CATALOGUE (§41.4). // // Lane E landed 110 new interior GLBs in `web/assets/manifest.json` (fittings 23 → 133): 40 purpose-built // record/op-shop fittings from `3dstore`, 39 decimated pub/venue props, 31 DJ-gear models. This file is // the ONE place that knows what they are, how big they are and which shop they belong in. // // WHY A HARDCODED TABLE AND NOT "READ THE MANIFEST": // `?noassets=1` must run 100% primitive with ZERO fetches, and `buildInterior` is SYNCHRONOUS — the room // is built the instant the door opens, long before any manifest promise could resolve. So the primitive // placeholder has to know the prop's real-world footprint WITHOUT the manifest, or the occupancy grid // reserves a slot the GLB doesn't fit. Every row below is transcribed from the manifest entry it names // (LANE_E_NOTES §41 tables); `tools`-free drift is caught by `kitAudit()` at the bottom, which the test // page runs against the live manifest and reports any mismatch. // // COST DISCIPLINE (the round's binding number): E fought this cargo from 14,140 draws down to 117 total, // and the interior margin is 162 draws against the ≤350 law. **Every id below is 1 draw** except the four // rigged DJ heroes (2–5). So a pool entry costs one draw when the GLB is live and ~0 when it isn't (the // primitive frame batches). Spend by COUNT, not by asset. // // THE THREE TRAPS E FLAGGED, and what this file does about each: // 1. `-long`/`-wide` pairs are THE SAME MESH ROTATED 90° (`tubs_4x2_wide`/`_vertical`, `tubs_3x3`/`_long`, // `tubs_shelf_long`/`_wide`, `wooden_rack_long`/`_wide`, `counter_station`/`_side`). Only ONE of each // pair is ever named in a POOL — shipping both into one room is paying twice for one prop. `PAIRS` // records them and `kitAudit()` asserts no pool contains both halves. // 2. `vinyl_sleeve` is a 3 mm sleeve whose footprint depth is FLOORED to 0.01 m. It is wall/surface // dressing only — it appears in `sleeve_wall` (wallMounted) and in no floor pool. // 3. `beam_roof` (6.5 m) and `shelf_board_long` (4.6 m) / `wall_shelves_big` (4.65 m) are LONGER THAN MOST // INTERIORS ARE WIDE (archetypes run 6–12 m wide, 7–14 m deep, and a wall fitting must also clear the // door corridor). They are in NO pool. `maxRun` on the wall pools is the general form of that rule. // // Row form: [id, w, d, h, form] — w×d = the manifest footprint in metres, h = manifest height, `form` names // the primitive silhouette `fittings.js kitProp()` builds as the asset-free placeholder. // ── the catalogue ──────────────────────────────────────────────────────────────────────── // form vocabulary (all built base-at-y=0, footprint centred on local (0,0), +Z = the "front"): // tub shallow open tub / crate, goods ride up and crest the rim // rack tiered open rack (legs + boards) // stack vertical column of tubs // board wall-hung shelf board (wallMounted) // unit plain cabinet/box body // cab tall lit cabinet (pokie / jukebox) // table top + legs // seat bench/couch (base + back) // barrel cylinder (keg, bin, urn, drum, topiary) // post slim vertical post with a small head // slab low flat box (pallet, case, sub) // speaker tall black cabinet with grille faces // deck flat desktop unit with a disc/plate (turntables, samplers, mixers) // panel thin upright board (window display, barrier, A-frame) // column tall square structural post // trolley shelved castored trolley export const KIT = { // ── 3dstore · floor record bins (kind `bin`) ────────────────────────────────────────── tub_black: [0.36, 0.53, 0.229, 'tub'], tub_white: [0.34, 0.53, 0.229, 'tub'], tub_black_long: [0.53, 0.36, 0.229, 'tub'], // = tub_black rotated (PAIRS) tub_white_long: [0.53, 0.36, 0.229, 'tub'], // = tub_white rotated (PAIRS) crate_blue: [0.34, 0.30, 0.322, 'tub'], bin_endoffunk: [0.36, 0.34, 0.34, 'tub'], chiu_bin: [0.37, 0.43, 0.35, 'tub'], chiu_bin_half: [0.24, 0.43, 0.35, 'tub'], chiu_bin_low: [0.34, 0.43, 0.35, 'tub'], rack_bin_wooden: [0.34, 0.40, 0.338, 'tub'], mos_bin_bottom: [0.34, 0.40, 0.34, 'tub'], mos_bin_lower_mid:[0.34, 0.36, 0.34, 'tub'], mos_bin_upper_mid:[0.34, 0.26, 0.34, 'tub'], mos_bin_top: [0.34, 0.20, 0.34, 'tub'], // ── 3dstore · multi-tier racks (kind `rack`) ────────────────────────────────────────── chiu_rack_long: [1.88, 0.43, 0.795, 'rack'], chiu_rack_short: [1.38, 0.43, 0.795, 'rack'], tubs_3x3: [0.53, 1.30, 0.555, 'rack'], tubs_3x3_long: [1.30, 0.53, 0.51, 'rack'], // = tubs_3x3 rotated (PAIRS) tubs_4x2_wide: [1.56, 0.53, 0.475, 'rack'], tubs_4x2_vertical:[0.53, 1.56, 0.475, 'rack'], // = tubs_4x2_wide rotated (PAIRS) tubs_shelf_long: [1.56, 0.55, 0.565, 'rack'], tubs_shelf_wide: [0.55, 1.56, 0.575, 'rack'], // = tubs_shelf_long rotated (PAIRS) wooden_rack_wide: [1.80, 0.78, 0.93, 'rack'], wooden_rack_long: [0.78, 1.80, 0.81, 'rack'], // = wooden_rack_wide rotated (PAIRS) bin_big_4x4: [0.59, 1.50, 1.255, 'rack'], mos_stack: [0.37, 0.40, 1.275, 'stack'], // ── 3dstore · wall / surface / structure ────────────────────────────────────────────── wall_shelves_long:[2.60, 0.05, 1.10, 'board'], shelf_board_short:[0.48, 1.95, 0.03, 'board'], aircon_unit: [1.15, 0.25, 0.35, 'unit'], vinyl_sleeve: [0.32, 0.01, 0.32, 'panel'], // TRAP 2 — 3 mm, wall dressing only window_booth: [1.85, 0.09, 0.72, 'panel'], library_trolley: [0.41, 0.88, 0.74, 'trolley'], counter_station: [0.88, 0.50, 0.87, 'unit'], counter_station_side:[0.50, 0.88, 0.87, 'unit'], // = counter_station rotated (PAIRS) column_square: [0.22, 0.22, 2.60, 'column'], column_wide: [0.36, 0.46, 2.60, 'column'], // ── pub / venue props (kind `pub`) ──────────────────────────────────────────────────── pokie: [0.75, 0.78, 1.70, 'cab'], jukebox: [0.97, 0.41, 1.52, 'cab'], plywood_bar: [1.51, 0.82, 1.05, 'unit'], bain_marie: [0.89, 0.76, 0.95, 'unit'], pie_warmer: [0.61, 0.71, 0.55, 'unit'], banquette: [1.55, 1.02, 0.85, 'seat'], old_couch: [1.80, 0.95, 0.85, 'seat'], lounge_chair: [0.69, 0.69, 0.80, 'seat'], bistro_table: [1.18, 1.18, 0.92, 'table'], picnic_table: [1.38, 1.41, 0.75, 'table'], keg: [0.40, 0.40, 0.60, 'barrel'], bin_round: [0.53, 0.51, 0.75, 'barrel'], ash_urn: [0.32, 0.33, 0.75, 'barrel'], raffle_drum: [0.37, 0.37, 0.90, 'barrel'], topiary: [0.61, 0.61, 1.20, 'barrel'], planter: [0.98, 0.96, 0.55, 'barrel'], champ_bucket: [0.24, 0.20, 0.35, 'barrel'], can_stack: [0.38, 0.32, 0.45, 'barrel'], ashtray_post: [0.41, 0.41, 0.95, 'post'], rope_post: [0.32, 0.32, 1.00, 'post'], fire_ext: [0.34, 0.19, 0.60, 'post'], concrete_plinth: [0.76, 0.74, 1.20, 'column'], marble_sink: [1.15, 0.56, 0.85, 'unit'], wheelie_bin: [0.72, 0.92, 1.10, 'unit'], distro_box: [0.63, 0.87, 1.20, 'unit'], flight_case: [0.60, 0.36, 0.75, 'slab'], pallet: [0.79, 0.73, 0.20, 'slab'], cable_snake: [0.45, 0.47, 0.20, 'slab'], sub_bass: [0.87, 0.63, 0.70, 'slab'], speaker_stack: [0.52, 0.52, 1.80, 'speaker'], crowd_barrier: [1.97, 0.30, 1.10, 'panel'], sandwich_board: [0.58, 0.56, 1.00, 'panel'], smoke_machine: [0.17, 0.24, 0.38, 'slab'], strobe_light: [0.22, 0.24, 0.40, 'slab'], beer_umbrella: [2.68, 2.65, 2.30, 'post'], firepit: [0.76, 0.56, 0.50, 'barrel'], broken_basin: [0.59, 0.49, 0.60, 'unit'], urinal_trough: [0.31, 0.26, 0.70, 'unit'], // ── DJ gear (kind `djgear`) — desktop items; they ride a bench, they are not floor props ── deck_1200_rigged: [0.51, 0.56, 0.11, 'deck'], deck_1200: [0.45, 0.51, 0.12, 'deck'], sl_deck_1: [0.25, 0.23, 0.04, 'deck'], sl_deck_2: [0.22, 0.22, 0.04, 'deck'], sl_deck_3: [0.25, 0.26, 0.04, 'deck'], sl_deck_4: [0.25, 0.33, 0.04, 'deck'], cdj_deck: [0.63, 0.42, 0.15, 'deck'], mixer_ttm54i_rigged: [0.23, 0.42, 0.11, 'deck'], mixer_pmc05pro_rigged:[0.26, 0.40, 0.11, 'deck'], mixer_shdj1200_rigged:[0.26, 0.39, 0.11, 'deck'], mixer_ttm54i: [0.23, 0.42, 0.11, 'deck'], mixer_pmc06pro: [0.15, 0.47, 0.07, 'deck'], mixer_pmc10: [0.48, 0.22, 0.22, 'deck'], mixer_pmx9000: [0.48, 0.37, 0.18, 'deck'], drum_808: [0.53, 0.47, 0.15, 'deck'], drum_tr1000: [0.47, 0.37, 0.16, 'deck'], sampler_mpc3000: [0.47, 0.59, 0.12, 'deck'], sampler_s900: [0.47, 0.56, 0.135, 'deck'], sampler_sp1200: [0.44, 0.62, 0.19, 'deck'], headphones_hd25: [0.23, 0.16, 0.30, 'slab'], cart_case: [0.22, 0.18, 0.21, 'slab'], zip_drive: [0.14, 0.23, 0.04, 'slab'], }; // TRAP 1 — the same mesh, rotated 90°. `kitAudit()` asserts no pool ships both halves of a pair. export const PAIRS = [ ['tub_black', 'tub_black_long'], ['tub_white', 'tub_white_long'], ['tubs_3x3', 'tubs_3x3_long'], ['tubs_4x2_wide', 'tubs_4x2_vertical'], ['tubs_shelf_long', 'tubs_shelf_wide'], ['wooden_rack_wide', 'wooden_rack_long'], ['chiu_rack_long', 'chiu_rack_short'], // same rack, two lengths — one per room reads better ['counter_station', 'counter_station_side'], ['mixer_ttm54i', 'mixer_ttm54i_rigged'], // the static copy of a hero: never both in one room ]; // TRAP 3 — ids longer than a small interior is wide. Named here so the rule is visible, not implied. export const OVERSIZE = { beam_roof: 6.5, shelf_board_long: 4.6, wall_shelves_big: 4.65 }; // The four DRIVABLE models and their control-node contracts (LANE_E_NOTES §41). R41 PLACES them and // records the names; it does NOT drive them — the driver is a later round's work (D/F). Drive by the BARE // node name (the cap/stem/rib are parented under it); glTF drops the source's limit constraints, so any // future driver must CLAMP THE TRAVEL ITSELF. export const CONTROL_NODES = { deck_1200_rigged: ['Platter_SPIN', 'Arm_YAW', 'Fader_PITCH', 'Btn_STARTSTOP'], mixer_ttm54i_rigged: ['Fader_CROSS'], mixer_pmc05pro_rigged: ['Fader_CROSS'], mixer_shdj1200_rigged: ['Fader_CROSS'], }; // ── the pools — WHICH shop gets WHAT ────────────────────────────────────────────────────── // A pool is just an ordered id list; `kitProp` picks one on the fitting's own seeded stream. Ordered // lists (not objects) so the seeded pick is stable regardless of JS key order. export const POOLS = { // record / stall floor bins — these mint `kind:'bin'` (diggable) only where the recipe says `dig:true` record_bins: ['tub_black', 'tub_white', 'crate_blue', 'bin_endoffunk', 'chiu_bin', 'chiu_bin_low', 'rack_bin_wooden', 'mos_bin_bottom'], // op-shop / bargain floor tubs — same silhouette family, dig OFF (R39: what an op shop yields is John's call) bargain_tubs: ['tub_white', 'tub_black', 'crate_blue', 'chiu_bin_half', 'mos_bin_lower_mid', 'rack_bin_wooden'], // browsable multi-tier racks (the shop-floor rows) shop_racks: ['chiu_rack_long', 'tubs_shelf_long', 'tubs_4x2_wide', 'wooden_rack_wide', 'tubs_3x3_long', 'bin_big_4x4', 'mos_stack'], // wall-hung shelving — `maxRun` on the spec is the general form of TRAP 3 (nothing longer than the wall) wall_boards: ['wall_shelves_long', 'shelf_board_short'], // wall services (no stock rides on these — an air-con with books on it is not a shop) wall_service: ['aircon_unit'], // record-shop wall dressing: sleeves pinned up (TRAP 2 — wall only, never the floor) sleeve_wall: ['vinyl_sleeve'], // shopfront window ledge / display window_dress: ['window_booth'], // book shop library: ['library_trolley'], // dept-store hall structure columns: ['column_wide', 'column_square', 'concrete_plinth'], // ── pub / cafe / milkbar ── pub_seating: ['banquette', 'old_couch', 'lounge_chair', 'bistro_table', 'picnic_table'], pub_floor: ['pokie', 'jukebox', 'keg', 'wheelie_bin', 'bin_round', 'ash_urn', 'ashtray_post', 'fire_ext', 'rope_post', 'sandwich_board', 'raffle_drum', 'can_stack', 'planter', 'topiary', 'marble_sink', 'pallet', 'champ_bucket', 'flight_case'], rsl_floor: ['pokie', 'raffle_drum', 'bistro_table', 'planter', 'topiary', 'bin_round', 'ash_urn', 'rope_post', 'jukebox'], milkbar_hot: ['bain_marie', 'pie_warmer'], milkbar_floor: ['can_stack', 'bin_round', 'sandwich_board', 'champ_bucket'], // stage-side venue kit venue_kit: ['speaker_stack', 'sub_bass', 'flight_case', 'distro_box', 'cable_snake', 'crowd_barrier', 'smoke_machine', 'strobe_light', 'pallet'], // pawn-shop "unredeemed pledges" — gear on the floor pawn_gear: ['flight_case', 'cart_case', 'distro_box', 'sub_bass'], }; // ── DJ booths — the ONE place a fitting carries several GLBs ──────────────────────────────── // DJ gear is DESKTOP kit (a 1200 is 0.11 m tall): it needs a surface, so the booth is a primitive bench // with the models sitting ON it. `parts` are local offsets on the bench top; `glb.js` attaches one GLB per // part. Draw cost is stated per booth because this is the round's only multi-draw fitting. export const BOOTHS = { // RECORD SHOP HERO — behind the counter. The only room in the game that gets the RIGGED pair. // 5 (deck_1200_rigged) + 2 (mixer_ttm54i_rigged) + 1 (deck_1200 static) = 8 draws. record_hero: { w: 1.55, d: 0.62, h: 0.92, draws: 8, parts: [ { id: 'deck_1200_rigged', x: -0.48, z: 0, ry: 0 }, { id: 'mixer_ttm54i_rigged', x: 0.00, z: 0, ry: 0 }, { id: 'deck_1200', x: 0.48, z: 0, ry: 0 }, ], }, // VENUE BOOTH — statics only. E: "a static copy is 1 draw and looks the same standing still." 3 draws. venue: { w: 1.40, d: 0.58, h: 0.95, draws: 3, parts: [ { id: 'cdj_deck', x: -0.42, z: 0, ry: 0 }, { id: 'mixer_pmx9000', x: 0.06, z: 0, ry: 0 }, { id: 'sl_deck_3', x: 0.50, z: 0, ry: 0 }, ], }, // PAWN COUNTER — the unredeemed-pledges bench. Drum machine + sampler, statics. 2 draws. pawn: { w: 1.25, d: 0.55, h: 0.88, draws: 2, parts: [ { id: 'drum_808', x: -0.32, z: 0, ry: 0 }, { id: 'sampler_sp1200', x: 0.30, z: 0, ry: 0 }, ], }, }; export const kitEntry = (id) => KIT[id] || null; // ── the drift check ──────────────────────────────────────────────────────────────────────── // Measure the thing, not the note about the thing: this compares every row above against the LIVE // manifest and against the trap rules. The test page exposes it as PROCITY_C.kitAudit(manifest). // Returns { rows, checked, dimErrors[], missing[], pairErrors[], oversize[] } — empty arrays = clean. export function kitAudit(manifest) { const out = { rows: Object.keys(KIT).length, checked: 0, dimErrors: [], missing: [], pairErrors: [], oversize: [] }; const F = (manifest && manifest.fittings) || null; if (F) { for (const [id, row] of Object.entries(KIT)) { const e = F[id]; if (!e) { out.missing.push(id); continue; } out.checked++; const near = (a, b) => Math.abs(a - b) < 0.005; if (!near(row[0], e.footprint[0]) || !near(row[1], e.footprint[1]) || !near(row[2], e.height)) out.dimErrors.push({ id, table: [row[0], row[1], row[2]], manifest: [e.footprint[0], e.footprint[1], e.height] }); } for (const id of Object.keys(OVERSIZE)) if (!F[id]) out.missing.push(id + ' (oversize row)'); } // TRAP 1: no pool may contain both halves of a rotated pair. for (const [name, pool] of Object.entries(POOLS)) for (const [a, b] of PAIRS) if (pool.includes(a) && pool.includes(b)) out.pairErrors.push(`${name}: ${a} + ${b}`); // TRAP 3: no pool (and no booth) may name an oversize id. const boothIds = Object.values(BOOTHS).flatMap(b => b.parts.map(p => p.id)); for (const [name, pool] of Object.entries(POOLS)) for (const id of pool) if (OVERSIZE[id]) out.oversize.push(`${name}: ${id}`); for (const id of boothIds) { if (OVERSIZE[id]) out.oversize.push(`booth: ${id}`); if (!KIT[id]) out.missing.push(`booth part ${id}`); } for (const pool of Object.values(POOLS)) for (const id of pool) if (!KIT[id]) out.missing.push(`pool id ${id}`); return out; }