John looked at R41's proof shots and named three faults. All three are fixed, at zero draw cost, and every lane corrected somebody's numbers on the way — including its own. E-LOOK (§42.1+§42.2a) — THE PREMISE WAS WRONG. "The pub props have no texture" (my brief, quoting E's own R41 note) is false: all 37 carry COLOR_0 and three.js renders it. The colour was BLEACHED — jukebox at saturation 0.02, 15 assets under 0.05; what the source library called colour was baked AO. So the fix was a palette imposed on colour already present, in place: 46 assets, draw delta +0 and tri delta +0 on every one, proved the honest way — the concatenated POSITION/NORMAL/TEXCOORD/index bytes are sha1-identical to R41, so footprints and origins provably did not move. (A first-pass bbox check ignored node rotations and "found" 3 phantom violations; thrown out for the byte comparison.) The banquette was 1990s office teal and went burgundy; the beer umbrella was white and went brewery green. THE DECK, diagnosed before being touched: 7 greys, chassis 0.05 luminance on a 0.033 bench = 1.21:1, and the one high-contrast feature was INSIDE-OUT (white ring around a dark disc; a turntable is a dark record on a light platter). Now Technics silver at 6.74:1, black vinyl, red start/stop. Node contract re-read from the shipped GLB: 4/4 + 1/1 PASS. Filed to R43, not fixed: deck_1200_rigged has NO PLINTH (bottom 43mm of 119mm lost to R41 join damage). E-CAST (§42.3) — 19 of 24 ship. Rejected: comical_luchador_01 (Mixamo Ch43, purple mask reading EL CHUPACABRA, identified from the file's own material name), a 3-heads-tall cartoon child, a robed wuxia elder, a WW2 infantryman — and dj_phrtt_01, AN UNCLOTHED BODY whose only texture is a bare-skin atlas, which was in the DEFAULT crowd and live on the depot. It is the R41 deny-list's failure class through a different door: an innocent name from another pipeline. Only rendering all 24 and LOOKING found it. Ruling 2 (prefer CC0) was REFUSED ON MEASUREMENT: Quaternius is 25 nodes / 23 joints and ZERO of 64 canonical tracks bind — it would have loaded silently and never animated. Replacements came from Mixamo's Ch## pool, filtered to the contract-clean 65-bone family (candidates ran 65..99 bones; named characters carry facial/hair bones). THE LICENCE GAP WAS UNREAD, NOT MISSING: every source FBX carries ApplicationVendor "Mixamo, Inc." internally. Adobe royalty-free, no attribution owed. B's amber rows are now real. B (§42.5) — MY ASSIGNED FIX WAS USELESS AND B PROVED IT. document.fonts.ready shipped, then measured: the atlas canvases are already byte-identical across boots, the game ships NO webfonts (document.fonts.size === 0), and ready fires 50ms BEFORE the first fillText. The real cause of unreproducible bookmarks is the wall-clock CROWD — 983 changed px of 921,600, ALL in the pedestrian band; 0 px with the crowd out. New DBG.freeze() takes stable bookmarks 2/5 -> 5/5. Also found: ?pop=0 boots 140 citizens (0 is falsy against the default). THE BENCH: D's filing correct on both counts. Front-vs-road 90.0deg -> 0.0deg, min=med=max over 60/60 instances; the streetlight arm now reaches 1.05m over the road where it reached 0.00. Both used a base yaw that pointed +Z down the street while their own comments claimed otherwise — the side term was already right, which is why it survived 41 rounds of reading. Also: on any box with CDN access 9 of 60 benches are the depot GLB, 90deg out from the house law — normalised at the geometry, once, at load. benchStops(plan) exported as one truth. C (§42.2b) — 122 vs 123 WAS THE CAMERA ASPECT, and nobody was wrong. Draw counts are post-frustum-cull and the sweep camera inherits the window aspect: 1.25->116, 1.60->122 (C's harness), 1.778->123 (F's), 2.37->124. Pinned MEASURE_ASPECT=16/9 with a 1.25 control. E's "keep the bench dark" advice was measured and declined with reason (deck-vs-bench was already 4.73:1, over threshold). "Reads as a black box" is a claim about pixels, so C measured pixels: near-black in the booth crop 57.5% -> 26.7%, distinct colours 228 -> 389. Bench rebuilt 3->7 boxes with an open record bay; headphones on the mixer; faceDoor aims booths at the player's entry — alignment within 45deg went 49% -> 92%. Two bugs found by looking: a fitting could be planted INSIDE A WALL (0.77m of a 0.86m half-depth in plaster), and every booth part sat 25mm sunk into its own worktop. D (§42.4) — five swaps IN PLACE at the same index (pickRig indexes these arrays and pedIndex is in the identity signature and the impostor atlas; removal would renumber the town). Identity signature pre- vs post-swap: 0 differing lines of 150 — who each citizen is did not move, only which body they wear. dj_phrtt_01 verified gone AT THE WIRE, not from source: a fresh default boot fetches 32 ped GLBs, none retired. THE CONTROL ARM FOUND A 41-ROUND-OLD BUG: the ped being RETIRED, comical_boy_01, binds only 3,132 of 3,460 tracks — six thumb bones lack the mixamorig prefix. It had been silently dropping six tracks per clip since it shipped. Testing the thing being thrown away found it. Imported B's benchStops mid-round: sitters now 0.0deg to the road, signatures byte-identical across the rewire. Corrected F's filing: the browse smoke was flaky-by-construction (a 16-25s race against a live sim), not deterministically red. F (§42.6) — qa.sh --strict 24 passed / 0 failed / 0 warn / 0 skipped. New Gate 11: period law (0/5 banned; CONTROL: R41's roster trips all five) and roster licence (24/24, 0 unverified; CONTROL: an amber row turns 20 red). r41_shots.py promoted warn -> strict. R41'S BYTE-HASH BAN IS REVERSED AND ITS STATED REASON WITHDRAWN AS WRONG. A gate may pin a hash under five in-run assertions (classic+flags boot, freeze().stillLive all-false, a second freeze removing 0 draws, settle until DBG.info() stops changing, hash with the dbg panel hidden): 5/5 bookmarks now pin to one sha256 over 3 fresh boots. Two findings got it there — the chunk streamer (a fixed settle is a bet on load) and the fps readout, which with everything else frozen was the last thing moving at 53-62 px. ?pop=0 fixed (Number.isFinite) — a no-op for 35 rounds. Three red flags_check gates fixed. Street: noon 282 · NIGHT 291/300 — the nine-draw margin INTACT, every R41 row reproducing to the digit. Interior at pinned 16:9: 123 @ dept/auto, margin 227. selfcheck 157,647/157,647, 0x5f76e76 unmoved. No tag — John's playtest rules the epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
319 lines
20 KiB
JavaScript
319 lines
20 KiB
JavaScript
// 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.
|
||
//
|
||
// Part fields: `id` `x` `z` `ry` — plus three added in R42 §42.2b, all optional and all absent ⇒ R41:
|
||
// `stackOn` another part's id: this part sits ON that one (y += that id's KIT height). A pair of cans
|
||
// resting on the mixer is the cheapest DJ signifier there is and it costs NO footprint —
|
||
// the bench is already full edge to edge (deck 0.51 + mixer 0.23 + deck 0.45 across 1.55 m
|
||
// leaves two 0.14 m gaps, and a HD25 is 0.23 wide), so the only free surface is upward.
|
||
// `dy` a GLB-ONLY vertical correction in metres. See BOOTH_PLINTH_FIX below — this is a
|
||
// WORKAROUND for a geometry fault E filed to R43, not a placement choice.
|
||
// `bay` (on the booth, not the part) what stands in the bench's front record bay.
|
||
//
|
||
// [R42 §42.2b — WORKAROUND, REMOVE WHEN R43 FIXES THE ASSET]
|
||
// `deck_1200_rigged` has no plinth. Lane E filed it (E-progress §42.2 "A geometry fault found while
|
||
// diagnosing"); measured again here from the shipped GLB rather than taken on report: of its 8,740
|
||
// vertices only NINE (0.10%) lie within 30 mm of the model's own y-minimum, 69 (0.79%) within 43 mm,
|
||
// and the 1st percentile of vertex y is 0.0524 — i.e. 99% of the deck sits 52 mm above the floor its
|
||
// bounding box claims. The static twin `deck_1200`, built from the same source, has 489 verts (4.39%)
|
||
// within 20 mm and a 1st percentile of 0.0140. `glb.js` plants a part at `y - bb.min.y`, and bb.min.y
|
||
// is held by that nine-vertex fragment, so the rigged chassis FLOATS over the bench. −0.043 m drops the
|
||
// feet onto the worktop; the nine stray verts end up inside the solid bench, where nothing can see them.
|
||
// The correction is GLB-only: the primitive stand-in has its base exactly on the worktop already.
|
||
export const BOOTH_PLINTH_FIX = { deck_1200_rigged: -0.043 };
|
||
|
||
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) + 1 (headphones) = 9 draws.
|
||
record_hero: {
|
||
w: 1.55, d: 0.62, h: 0.92, draws: 9,
|
||
bay: { stock: 'sleeve', items: 9 }, // records face-out in the bench front — see fittings.js djBooth
|
||
parts: [
|
||
{ id: 'deck_1200_rigged', x: -0.48, z: 0, ry: 0, dy: BOOTH_PLINTH_FIX.deck_1200_rigged },
|
||
{ id: 'mixer_ttm54i_rigged', x: 0.00, z: 0, ry: 0 },
|
||
{ id: 'deck_1200', x: 0.48, z: 0, ry: 0 },
|
||
{ id: 'headphones_hd25', x: 0.00, z: -0.05, ry: 0.45, stackOn: 'mixer_ttm54i_rigged' },
|
||
],
|
||
},
|
||
// VENUE BOOTH — statics only. E: "a static copy is 1 draw and looks the same standing still." 4 draws.
|
||
venue: {
|
||
w: 1.40, d: 0.58, h: 0.95, draws: 4,
|
||
bay: { stock: 'sleeve', items: 8 },
|
||
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 },
|
||
{ id: 'headphones_hd25', x: 0.06, z: -0.04, ry: -0.5, stackOn: 'mixer_pmx9000' },
|
||
],
|
||
},
|
||
// PAWN COUNTER — the unredeemed-pledges bench. Drum machine + sampler, statics. 2 draws.
|
||
// Its bay is BOXES, not records: a pawnbroker's bench is not a DJ booth and must not read as one.
|
||
pawn: {
|
||
w: 1.25, d: 0.55, h: 0.88, draws: 2,
|
||
bay: { stock: 'box', items: 5 },
|
||
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;
|
||
}
|