PROCITY/web/js/interiors/kit41.js
m3ultra 422f17263e Lane C R41 §41.4: the shops get their stock — and the measuring instrument was wrong
110 fittings placed across 12 shop types via new kit41.js (96-row catalogue, 67 ids in
pools) and ONE generic kitProp builder; recipes name pools, never asset ids. glb.js now
honours seeded per-fitting variants, multi-GLB booths, and a MEASURED autoYaw instead of a
110-row facing table.

THE INSTRUMENT WAS WRONG. drawSweep cleared every interior EXCEPT current, so every
historical interior number carried a phantom second room — 'toy', untouched this round,
'gained' 4 draws. R39/R40's celebrated 'worst 188, margin 162' was inflated. Corrected, and
with 110 new fittings placed: worst room in the game is 122 @ dept/auto, MARGIN 228.
Per type: record 61->78, pub 45->59, band_room 39->50, stall 71->82, book 51->61,
pawn 60->70, video 54->63, rsl 52->60, dept 116->122, opshop 102->105, milkbar 68->69,
toy 86->86 (+0, the control).

The pub finally has a bar (plywood_bar), banquettes, pokies, a stage flank. Milk bars get a
bain-marie and pie warmer. The pawn shop gets an 808 and an SP-1200 on the bench. Record
shops get a rigged DJ booth. Market stalls get 2-4 diggable bins each, closing R39's measured
44%-no-crate hole to 0/40.

WARDROBE, route R5: 257.5 KB total (246 KB atlas + 17 KB index), ONE fetch, one material,
one draw per fitting — 52 layers picked BY NAME, alpha-cropped, shelf-packed, 3.7% of source
bytes, lazy behind ?stock=real. LICENCE CALL MADE EXPLICITLY: AUDIT.md says no real brands
ship and the source library is prompted FROM real labels — the adidas jacket renders a
legible trefoil. Screened on a contact sheet, then relabelled in parody voice (Paddy's
Bootleg, Kaymart, Coogee Knits, Mombo, Stoosh).

FIVE DEFECTS FOUND AND FIXED, one long-standing: buyMesh has been INVISIBLE on every GLB
fitting since R9 (hiding every buyable item under ?stock=real) · DJ booths were
nondeterministic (parts attached in network order) · booth gear floated at 0.92 m · batch.js
dropped userData sub-tags on merge, so any post-batch flag read was already a no-op · the
path guarantee could empty a room (pub/wide/4242 returned a bar, a stage and nothing else) —
new restore pass, 129 pulled -> 101 restored over 480 rooms, path re-proved on every restore.

Gates: 288-room sweep x 4 arms all pass; soak 60 rooms avg 3.83 ms worst 10.3 ms, leaks 0;
960-room corpus 0 path-fails 0 carves; determinism byte-equal across fresh contexts
(sha256 c26b4848); ?noassets=1 60 rooms with 0 GLB / 0 manifest / 0 wardrobe / 0 stock
fetches. DJ control nodes recorded on fitting.controlNodes for a future driver (glTF drops
the limit constraints — a driver must clamp travel itself).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 18:11:07 +10:00

291 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 (25). 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 612 m wide, 714 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;
}