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>
291 lines
17 KiB
JavaScript
291 lines
17 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.
|
||
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;
|
||
}
|