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>
215
C-progress.md
@ -3,7 +3,220 @@
|
|||||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||||
|
|
||||||
Last updated: 2026-08-03 (round 39) · owner: PROCITY-C · reviewer: Fable
|
Last updated: 2026-08-07 (round 41) · owner: PROCITY-C · reviewer: Fable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Round 41 (§41.4) — THE SHOPS GET THEIR STOCK
|
||||||
|
|
||||||
|
Lane E's 110 new interior fittings are placed, the pub is furnished, the record shop has a rigged 1200
|
||||||
|
behind the counter, and 52 hand-prompted 1990s Australian garments hang on the op-shop rails at **zero
|
||||||
|
geometry cost**. New file `web/js/interiors/kit41.js` is the catalogue; `fittings.js` grew one generic
|
||||||
|
builder that covers all 110 and a DJ-booth builder; the recipes name pools, never asset ids.
|
||||||
|
|
||||||
|
**Everything green: 0 throws · 0 path-fails · 0 carves · 0 determinism-fails · 0 leaks · worst warm build
|
||||||
|
10.3 ms (budget 50) · worst room 122 draws against the ≤350 law.**
|
||||||
|
|
||||||
|
### 0. FIRST: the instrument was measuring a room that wasn't there
|
||||||
|
|
||||||
|
`drawSweep` — this lane's own ≤350 instrument, the one F mirrors in `r39_transmission.py` — cleared every
|
||||||
|
interior from the scene **except `current`**, the live room the page happens to be showing. So every
|
||||||
|
per-type number carried a constant offset from a *second, unrelated room* (seed 1990, type = the first
|
||||||
|
entry in the list, i.e. a record shop). It was invisible while nothing changed. The moment §41.4 furnished
|
||||||
|
that record shop, `toy` — a type this round does not touch — "gained" 4 draws in both modes.
|
||||||
|
|
||||||
|
Fixed before anything was costed: the sweep now clears **all** interiors and rebuilds `current` at the end.
|
||||||
|
**Every number below is on the corrected instrument.** Against R39/R40 prints of the same function the
|
||||||
|
values are lower; that is the phantom leaving, not rooms getting cheaper. The old worst reading of 188 @
|
||||||
|
dept was 116 of dept and ~72 of a record shop nobody was looking at.
|
||||||
|
|
||||||
|
### 1. The control arm — `opts.noKit`, and why it is exact
|
||||||
|
|
||||||
|
Every R41 spec is **appended last** inside its recipe, so `layout.js` hands each surviving spec the same
|
||||||
|
draws off the same `lay-count`/`lay-place` streams in the same order. `buildInterior(..., { noKit: true })`
|
||||||
|
drops exactly those specs and reproduces the pre-R41 room. That is what makes "the kit costs N draws" a
|
||||||
|
measurement rather than a claim — and `toy`, which gets no kit by design, comes out **+0 in all three
|
||||||
|
modes**, which is the control for the control.
|
||||||
|
|
||||||
|
### 2. Draws by shop type — worst room per type, seed 1990, 5 archetypes + auto
|
||||||
|
|
||||||
|
| type | fittings placed (R41 additions) | OFF pre | OFF R41 | **ON pre** | **ON R41** | Δ | stock=real R41 |
|
||||||
|
|---|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| `record` | 2–4 tubs/crates (dig) · 1–3 tub racks · 1–2 wall boards · window ledge · 1–2 pinned sleeves · **the rigged DJ booth** | 69 | 85 | 61 | **78** | +17 | 70 |
|
||||||
|
| `opshop` | 1–2 bargain tubs · 1–2 racks · wall board | 89 | 92 | 102 | **105** | +3 | 105 |
|
||||||
|
| `book` | library trolley · 1–2 racks · 1–2 wall boards | 55 | 69 | 51 | **61** | +10 | 95 |
|
||||||
|
| `video` | ex-rental bargain tub · 1–2 wall boards · window ledge | 65 | 76 | 54 | **63** | +9 | 63 |
|
||||||
|
| `pawn` | 1–2 flight cases/sub · **808 + SP-1200 bench** · wall board | 67 | 81 | 60 | **70** | +10 | 70 |
|
||||||
|
| `stall` | **2–4 diggable bins** · 1–2 racks | 76 | 89 | 71 | **82** | +11 | 82 |
|
||||||
|
| `dept` | 0–2 columns · 1–2 racks · wall board | 80 | 87 | 116 | **122** | +6 | 122 |
|
||||||
|
| `milkbar` | **bain-marie / pie warmer** · 1–3 floor props · air-con | 72 | 80 | 68 | **69** | +1 | 69 |
|
||||||
|
| `pub` | **bar = `plywood_bar`** · 2–3 stage-flank venue kit · 3–6 seats · 3–6 floor props · air-con · DJ booth | 48 | 64 | 45 | **59** | +14 | 59 |
|
||||||
|
| `band_room` | bar = `plywood_bar` · 2–4 venue kit · 2–3 floor props · DJ booth | 43 | 56 | 39 | **50** | +11 | 50 |
|
||||||
|
| `rsl` | bar = `plywood_bar` · 1–3 venue kit · 3–6 seats · 3–5 floor props (pokies, raffle drum) | 56 | 73 | 52 | **60** | +8 | 60 |
|
||||||
|
| `toy` | **none — untouched by design** | 67 | 67 | 86 | **86** | **+0** | 120 |
|
||||||
|
|
||||||
|
**WORST ROOM IN THE GAME: 122 draws @ `dept`/auto, GLB on, with or without `?stock=real`.**
|
||||||
|
Law ≤350 → **margin 228**. It was 116 before this round, so §41.4 spent **6 draws at the worst room** and
|
||||||
|
at most 17 anywhere. `dept` deliberately gets the fewest additions of any type precisely because it owns
|
||||||
|
the worst room; the pub, which this round exists for, could afford 14 and still sit at 59.
|
||||||
|
|
||||||
|
Cost discipline came free from E's work: **every one of the 110 is 1 draw** bar the four rigged heroes.
|
||||||
|
The record shop's booth is the round's only multi-draw fitting — `deck_1200_rigged` (5) +
|
||||||
|
`mixer_ttm54i_rigged` (2) + a static `deck_1200` (1) = **8 draws, one room, once**.
|
||||||
|
|
||||||
|
### 3. Sweep + soak
|
||||||
|
|
||||||
|
- **`drawSweep` 4 arms × 12 types × 6 archetypes = 288 rooms:** all pass ≤350, **0 console errors**.
|
||||||
|
- **Soak, 60 rooms build+dispose+rebuild:** avg **3.83 ms**, worst **10.3 ms** (budget 50) · leak geo **0**
|
||||||
|
/ tex **0** · determinism **0** mismatches · path-fails **0** · carves **0** · throws **0**.
|
||||||
|
- **960-room corpus** (12 types × 5 archetypes × 8 seeds × kit/noKit, GLB on): **0 path-fails, 0 carves.**
|
||||||
|
|
||||||
|
| type | R41 fittings/room | GLB props/room (max) | diggable bins/room | rooms with none | watchPoints | browsePoints |
|
||||||
|
|---|---:|---:|---:|---:|---:|---:|
|
||||||
|
| record | 7.90 | 20.8 (28) | **8.32** | 0/40 | — | 3.00 |
|
||||||
|
| stall | 3.92 | 11.7 (18) | **4.55** | **0/40** | — | 3.00 |
|
||||||
|
| pub | 8.40 | 13.0 (19) | — | — | **7.5 / cap 8** | 3.00 |
|
||||||
|
| band_room | 5.03 | 9.1 (11) | — | — | **8.0 / cap 8** | 3.00 |
|
||||||
|
| rsl | 5.78 | 10.1 (18) | — | — | **11.4 / cap 12** | 3.00 |
|
||||||
|
| opshop | 2.95 | 13.1 (19) | — | — | — | 3.00 |
|
||||||
|
| milkbar | 3.77 | 13.5 (18) | — | — | — | 3.00 |
|
||||||
|
| book / video / pawn / dept | 3.80 / 3.10 / 3.17 / 1.90 | 11.4 / 5.1 / 9.7 / 11.8 | — | — | — | 3.00 |
|
||||||
|
|
||||||
|
**Furnishing did not cost the crowd.** watchPoints are computed after placement, so a furnished venue could
|
||||||
|
have starved its own audience; measured, pub holds 7.5 of 8, band_room 8.0 of 8, RSL 11.4 of 12.
|
||||||
|
|
||||||
|
**A measured R39 hole closed on the way past.** R39 counted 48 market-stall rooms and found **21 (44%) with
|
||||||
|
no crate at all** — the recipe's `crate` is `zone:'centre'` and 3–6 trestles take the aisle first — so RED
|
||||||
|
TICKET DAY could name a stall you cannot dig in. `stall` now carries 2–4 kit bins with the full fallback
|
||||||
|
chain: **0 of 40 stall rooms have no bin, mean 4.55.**
|
||||||
|
|
||||||
|
### 4. Four defects found by building this, all fixed
|
||||||
|
|
||||||
|
1. **`buyMesh` was invisible on any fitting with a GLB.** R9's buy-anywhere shelves are merged,
|
||||||
|
per-item-addressable meshes tagged `{ noBatch, buyMesh }` and *not* `isStock` — so `glb.js`'s
|
||||||
|
frame-hide turned **every real, buyable item in the room invisible** the moment the shelf upgraded.
|
||||||
|
Live since R9 on `bookshelf`, `cubeShelf` and `clothesRack`; never seen because `?stock=real` is opt-in
|
||||||
|
and R9 measured it GLB-off. Found the instant 52 real garments went on a rail and the rail came back
|
||||||
|
empty. `hidePrimitive` now skips `buyMesh`.
|
||||||
|
2. **The DJ booths were nondeterministic.** Parts were attached inside each load's own `.then`, so the
|
||||||
|
booth's children landed in network-completion order. Two runs of the same 240 rooms differed on exactly
|
||||||
|
the three types that have a booth and on nothing else. Loads stay parallel; attachment is now a plain
|
||||||
|
in-order loop.
|
||||||
|
3. **The booth's gear floated at 0.92 m over bare floor.** A single-GLB swap hides the whole primitive,
|
||||||
|
but a booth's GLBs are the *gear* — the bench is the fitting. Gear stand-ins are tagged `boothGear` and
|
||||||
|
only those are hidden. Which exposed a fourth: **`batch.js` rebuilt `userData` from scratch and dropped
|
||||||
|
sub-tags on merge**, so the tag never survived to be read. It now carries `boothGear`/`tillPrimitive`
|
||||||
|
through a merge when *every* mesh in the material bucket has it.
|
||||||
|
4. **The door→counter path guarantee could empty a room.** The pull loop takes the *lowest-priority*
|
||||||
|
survivor each pass, not the one standing in the corridor, so it clears the cheap furniture first and
|
||||||
|
reaches the real blocker by attrition. Measured: `pub/wide/4242` came back with a bar, a stage and
|
||||||
|
**nothing else** — a wide pub puts the bar beside the stage and leaves one ~1 m gap. Pre-existing; ten
|
||||||
|
more fittings per pub made it impossible to miss. New step 3b walks the pulled list in reverse and puts
|
||||||
|
each fitting back **only if door→counter still reaches**: over 480 rooms, **129 pulled → 101 restored,
|
||||||
|
28 genuinely lost**, and the guarantee is re-proved on every single restore. A room that pulled nothing
|
||||||
|
does nothing (byte-identical); a room that pulled N can only end with more fittings, never fewer.
|
||||||
|
|
||||||
|
### 5. E's three traps
|
||||||
|
|
||||||
|
| trap | what §41.4 does |
|
||||||
|
|---|---|
|
||||||
|
| `-long`/`-wide` pairs are the same mesh rotated 90° | `kit41.PAIRS` records all 9; **no pool contains both halves**, and `kitAudit()` asserts it. Facing is not a hand-tuned table either — `glb.js autoYaw` compares the manifest footprint against the slot the grid reserved and turns the GLB a quarter if the major axis is swapped. Pre-R41 kinds keep their hand-tuned `KIND_TO_YAW` exactly, so legacy rooms are unchanged on screen. |
|
||||||
|
| `vinyl_sleeve` depth floored to 0.01 m | wall dressing only — `sleeve_wall`, `wallMounted`, mountY 1.72, in **no floor pool**. |
|
||||||
|
| `beam_roof` 6.5 m · `shelf_board_long` 4.6 m · `wall_shelves_big` 4.65 m | in **no pool at all** (archetypes are 6–12 m wide). Listed in `kit41.OVERSIZE`, asserted by `kitAudit()`, and generalised as `spec.o.maxRun`. |
|
||||||
|
|
||||||
|
**`kitAudit(manifest)` (on `PROCITY_C`) is the drift check, not a promise:** it compares all **96
|
||||||
|
catalogue rows** against the live manifest. Result — **96/96 checked, 0 dimension errors, 0 missing, 0 pair
|
||||||
|
errors, 0 oversize; 67 pool/booth ids all resolve.** It found one real drift on the first run
|
||||||
|
(`sampler_s900` height 0.14 vs the manifest's 0.135), which is fixed.
|
||||||
|
|
||||||
|
### 6. THE WARDROBE — route R5, zero geometry
|
||||||
|
|
||||||
|
`web/assets/wardrobe/` (new): **`wardrobe_atlas_00.webp` 246,364 B + `stock_opshop_index.json` 17,347 B =
|
||||||
|
263,711 B (257.5 KB), ONE fetch, ONE material, ONE draw per fitting.**
|
||||||
|
|
||||||
|
- **52 layers selected BY NAME** (ruling 3 — no wildcard copy) from the 441 at
|
||||||
|
`~/Documents/90sDJsim/web/world/media/doll/`. Each is cropped to its **alpha bbox** (the sources are
|
||||||
|
704×1408 full-body layers that are ~90% empty; a top occupies the torso band only), scaled and
|
||||||
|
shelf-packed. **257.5 KB is 3.7% of the 52 sources' 6.74 MB and 0.49% of the 53 MB full set.**
|
||||||
|
- Garment cells are **128×152** — exactly the resolution of the procedural garment canvas they replace
|
||||||
|
(`stock.js makeGarment` is 128×160). This is a swap in art, not an increase in texture budget.
|
||||||
|
- **LAZY AND BOUNDED:** it is a stock pack under its own base, so it loads only under `?stock=real`.
|
||||||
|
A default boot fetches nothing; `?noassets=1` fetches nothing. Fail-soft: no pack → the procedural
|
||||||
|
canvas, byte-identically.
|
||||||
|
- **Where the names show:** garments come through `buildBuyableShelf`, so they are per-item addressable
|
||||||
|
and the **existing shelf buy-card path reads `artist — title`** with no new UI.
|
||||||
|
|
||||||
|
**THE LICENCE CALL, made explicitly.** `pipeline/AUDIT.md` is standing law: *no real titles/authors/brands
|
||||||
|
ship*. The source library was hand-prompted **from** real 1990s labels, so a majority of the 441 carry a
|
||||||
|
live trademark in the filename **and in the artwork** — `top_i_adidas_track_jacket.png` renders a legible
|
||||||
|
trefoil and three stripes; `bottom_i_tech_fleece_pant.png` has a swoosh on the leg. So the screen was two
|
||||||
|
stage: every candidate was eyeballed on a contact sheet and **rejected if the artwork carried a mark**
|
||||||
|
(that took out adidas/nike/versace/dr-martens/champion/coogi/stussy/fila and the rest), and the surviving
|
||||||
|
ids are then labelled in a **parody voice**: `Paddy's Bootleg`, `Coogee Knits`, `Mombo`, `Stoosh`,
|
||||||
|
`Kaymart`, `Criss Colors`, `Kangaruu`, `Army Disposals`, `Rissole Classic`, `Nan Knitted It`.
|
||||||
|
**The parody is not a compromise, it is the period detail** — Paddy's Market genuinely sold "Tommy
|
||||||
|
Hillfigga", and the source library itself already ships `adidos_shelltops` and `doc_martys_8_eye`.
|
||||||
|
Titles stay literal and Australian: *Double Pluggers · Terry Towelling Hat · Trackie Dacks · Boardies ·
|
||||||
|
Squiggle Jumper · Phat Pants · Acid Smiley Tee*.
|
||||||
|
|
||||||
|
Two small plumbing changes made this work without touching the three existing packs:
|
||||||
|
`SLOT_FOR.opshop = ['garment','box']` (a pack may now serve more than one slot kind; the three pre-R41
|
||||||
|
packs keep their single string and are handed `pack.items` **by reference**, so every code path that
|
||||||
|
matters compares reference-equal and behaves byte-identically), and an optional `index.material` block
|
||||||
|
(`alphaTest` + `DoubleSide`) — without it a cut-out tee is its own bounding box and the back of a hanging
|
||||||
|
garment is a hole. Packs that declare no block build the material they always did.
|
||||||
|
|
||||||
|
### 7. DJ CONTROL NODES — recorded, NOT driven (for D/F, a later round)
|
||||||
|
|
||||||
|
R41 **places** the rigged models and records their contracts. **Nothing drives them**, and `djBooth` ships
|
||||||
|
no animation hook on purpose so a placed node is never mistaken for a live one. The names are in
|
||||||
|
`kit41.CONTROL_NODES`, on the fitting as `fitting.controlNodes`, and on the group as
|
||||||
|
`group.userData.controlNodes`:
|
||||||
|
|
||||||
|
| id | draws | control nodes | where it is |
|
||||||
|
|---|---:|---|---|
|
||||||
|
| `deck_1200_rigged` | 5 | `Platter_SPIN` · `Arm_YAW` · `Fader_PITCH` · `Btn_STARTSTOP` | record shop, booth `record_hero`, left deck |
|
||||||
|
| `mixer_ttm54i_rigged` | 2 | `Fader_CROSS` | record shop, booth `record_hero`, centre |
|
||||||
|
| `mixer_pmc05pro_rigged` | 2 | `Fader_CROSS` | catalogued, not placed |
|
||||||
|
| `mixer_shdj1200_rigged` | 2 | `Fader_CROSS` | catalogued, not placed |
|
||||||
|
|
||||||
|
Two things a future driver must not rediscover the hard way: drive them by the **bare node name** (the
|
||||||
|
cap/stem/rib are parented under it), and **glTF drops the source's limit constraints, so clamp the travel
|
||||||
|
yourself.** Venue and pawn booths are deliberately **statics** — E's call, and correct: a static copy is
|
||||||
|
1 draw and looks the same standing still.
|
||||||
|
|
||||||
|
### 8. Determinism + `?noassets`
|
||||||
|
|
||||||
|
- **Determinism, two FRESH browser contexts** (never a reused tab), 240 rooms = 12 types × 5 archetypes ×
|
||||||
|
4 seeds, **GLB on + `?stock=real`**, comparing placement, GLB id list, browse/watch points, keeper stand
|
||||||
|
and stage: **byte-equal, sha256 `c26b4848…eae852` on both runs, 0 mismatched rooms** over 522,501 bytes
|
||||||
|
of payload.
|
||||||
|
- **`?noassets=1`, all 60 type × archetype rooms built:** **0 GLB fetches · 0 manifest fetches · 0 wardrobe
|
||||||
|
fetches · 0 stock-pack fetches.** The only 10 requests are the pre-existing `assets/gen/wall-*.jpg` /
|
||||||
|
`tex-*.jpg` shell wallpapers, which are not R41 cargo and are the same 10 before and after. Worst
|
||||||
|
primitive room **126 draws @ opshop/hall, 0 GLB upgrades, 0 path-fails** — the asset-free build is a
|
||||||
|
real, correctly-sized, walkable shop floor, because `kit41.js` is a hardcoded table and `buildInterior`
|
||||||
|
is synchronous. Control shot: `docs/shots/laneC/r41_noassets_control.png`.
|
||||||
|
|
||||||
|
### 9. Shots — `docs/shots/laneC/`
|
||||||
|
|
||||||
|
`r41_record_shop.png` · `r41_record_dj_booth.png` · `r41_pub.png` · **`r41_pub_before.png`** (the same
|
||||||
|
seed and camera with `noKit:true` — the control) · `r41_opshop_wardrobe.png` · `r41_band_room.png` ·
|
||||||
|
`r41_milkbar.png` · `r41_stall_bins.png` · `r41_noassets_control.png`.
|
||||||
|
Each carries a **1.75 m scale post** at a floor pose: no shot here contains a humanoid, so the R10 law does
|
||||||
|
not bite, but these are 110 brand-new assets at claimed real-metre scale and the bar is the proof.
|
||||||
|
|
||||||
|
### 10. Files
|
||||||
|
|
||||||
|
New: `web/js/interiors/kit41.js` · `web/assets/wardrobe/{wardrobe_atlas_00.webp, stock_opshop_index.json}`.
|
||||||
|
Changed: `fittings.js` (the kit builder + `djBooth` + counter `glbId`), `theme.js` (recipe pools),
|
||||||
|
`layout.js` (`stage` zone, `spec.gap`, `wall` as a fallback zone, restore pass, `noKit`), `glb.js`
|
||||||
|
(`fitting.glbId`, `glbParts`, `autoYaw`, the `buyMesh` fix), `stock.js` (`hang` real path, slot pools),
|
||||||
|
`stockpack.js` (multi-slot packs, `index.material`, `ry`), `batch.js` (sub-tag propagation),
|
||||||
|
`interiors.js` (`noKit`, `room.pulls`), `interior_test.html` (sweep fix, `noKit`/`stock` arms, wardrobe
|
||||||
|
preload, `kitAudit` hook).
|
||||||
|
**Not touched:** `web/assets/manifest.json`, `web/assets/models/**`, `pipeline/**`, `web/js/citizens/**`,
|
||||||
|
`web/js/world/**`, `web/index.html`, `tools/qa.sh`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,78 @@
|
|||||||
# LANE C — cross-lane notes (PROCITY-C)
|
# LANE C — cross-lane notes (PROCITY-C)
|
||||||
|
|
||||||
|
## Round 41 §41.4 — the 110 fittings are placed. Three hand-offs, one warning.
|
||||||
|
|
||||||
|
Full tables in `C-progress.md` § Round 41 (§41.4). Shots: `docs/shots/laneC/r41_*.png`.
|
||||||
|
**Worst room in the game is now 122 draws @ `dept`/auto (GLB on), law ≤350 — margin 228.**
|
||||||
|
|
||||||
|
### → LANE F: one line turns the wardrobe on in the game, and one number needs restating
|
||||||
|
|
||||||
|
**1. The wardrobe pack.** 52 named 1990s Australian garment layers, one 240 KB atlas, feeding the op-shop
|
||||||
|
clothes rails and shelves at **zero geometry cost**. It is a stock pack under its own base, so `index.html`
|
||||||
|
needs the same one line the test page has, beside the existing three preloads:
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (STOCK_REAL) preloadStockPack('opshop', { base: 'assets/wardrobe/' });
|
||||||
|
```
|
||||||
|
|
||||||
|
and, where a shop's interior is built, `stockBase: 'assets/wardrobe/'` when `shop.type === 'opshop'`
|
||||||
|
(`interior_mode.js` already passes `stockAdapter`; the adapter must be
|
||||||
|
`makeStockAdapter(getStockPack('opshop', 'assets/wardrobe/'))`). **Nothing breaks without it** — no pack
|
||||||
|
means the procedural garment canvas, byte-identically, which is also what `?noassets` and every default
|
||||||
|
boot get. Cache identity is `(type, base)`, so it coexists with the town-wide packs.
|
||||||
|
|
||||||
|
**2. `drawSweep` numbers moved, and it is the instrument, not the rooms.** The sweep cleared every interior
|
||||||
|
from the scene *except* `current` — the live room the page is showing — so every per-type number carried a
|
||||||
|
constant offset from an unrelated second room. R39/R40 prints of this function (worst 188, "margin 162")
|
||||||
|
include that phantom. The sweep now clears all interiors and rebuilds at the end. Re-baseline against the
|
||||||
|
corrected values before asserting anything tighter than ≤350.
|
||||||
|
|
||||||
|
**3. New gate surface, if you want it:** `PROCITY_C.kitAudit(manifest)` compares all 96 catalogue rows in
|
||||||
|
`kit41.js` against the live `manifest.json` and asserts Lane E's three traps (no pool ships both halves of
|
||||||
|
a rotated pair; no pool ships an oversize id; every pool/booth id resolves). Currently **96/96 checked, 0
|
||||||
|
dimension errors, 0 missing, 0 pair errors, 0 oversize**. It caught a real 5 mm drift on its first run. It
|
||||||
|
is cheap, it is non-vacuous (it has already fired), and it is exactly the check that goes stale silently.
|
||||||
|
|
||||||
|
**4. `opts.noKit`** on `buildInterior` drops every R41 spec and reproduces the pre-R41 room exactly — the
|
||||||
|
control arm for any before/after the round needs. `room.pulls = { pulled, restored, lost }` reports what
|
||||||
|
the door→counter guarantee cost that room; `lost > 0` means a genuine blocker.
|
||||||
|
|
||||||
|
### → LANE D: the DJ control nodes exist, nothing drives them, and that is deliberate
|
||||||
|
|
||||||
|
R41 **places** the rigged gear and **records** the contract. There is no driver and `djBooth` ships no
|
||||||
|
animation hook, so a placed node can never be mistaken for a live one. When you build the driver:
|
||||||
|
|
||||||
|
| id | control nodes | live where |
|
||||||
|
|---|---|---|
|
||||||
|
| `deck_1200_rigged` | `Platter_SPIN` · `Arm_YAW` · `Fader_PITCH` · `Btn_STARTSTOP` | record shop, `BOOTHS.record_hero`, left deck |
|
||||||
|
| `mixer_ttm54i_rigged` | `Fader_CROSS` | record shop, `BOOTHS.record_hero`, centre |
|
||||||
|
| `mixer_pmc05pro_rigged` · `mixer_shdj1200_rigged` | `Fader_CROSS` | catalogued, not placed |
|
||||||
|
|
||||||
|
Reach them from the room without guessing: any fitting group with `userData.djBooth` carries
|
||||||
|
`userData.controlNodes = { <id>: [node…] }`, and the GLB instance beneath it is tagged
|
||||||
|
`userData.glbUpgrade` with `userData.kind === <id>`. Two rules from E that a driver must honour: drive by
|
||||||
|
the **bare node name** (cap/stem/rib are parented under it), and **glTF drops the source's limit
|
||||||
|
constraints — clamp the travel yourself.** Venue and pawn booths are statics by design.
|
||||||
|
|
||||||
|
**Your poses are unchanged and still hold in a furnished room.** browsePoints stayed at 3.00/room across
|
||||||
|
all 12 types; watchPoints measured after furnishing: pub **7.5 of cap 8**, band_room **8.0 of 8**, RSL
|
||||||
|
**11.4 of 12**. `counter.stand` is untouched. The venues' bar is now `plywood_bar` instead of
|
||||||
|
`counter_till` — same fitting, same stand pose, same service strip, different model.
|
||||||
|
|
||||||
|
### ⚠ WARNING TO ANY LANE THAT HIDES A PRIMITIVE OR MERGES A MESH
|
||||||
|
|
||||||
|
Two latent bugs this round found the hard way, both of the same shape — **a tag that something depends on,
|
||||||
|
destroyed by a later pass**:
|
||||||
|
|
||||||
|
- `glb.js` hid every child of an upgraded fitting that was not `isStock`. R9's buy-anywhere shelves are
|
||||||
|
tagged `{ noBatch, buyMesh }` and *not* `isStock`, so **`?stock=real` + GLB made every buyable item in
|
||||||
|
the room invisible** — live since R9 on `bookshelf`/`cubeShelf`/`clothesRack`, never seen because R9
|
||||||
|
measured GLB-off.
|
||||||
|
- `batch.js` rebuilt `userData` from scratch when it merged a material bucket, dropping sub-tags such as
|
||||||
|
`tillPrimitive`. Any hide that reads a per-mesh flag after batching was already a no-op.
|
||||||
|
|
||||||
|
If you add a per-mesh flag that a later stage reads, check it survives `batchRoom` **and** the GLB swap.
|
||||||
|
|
||||||
## → Lane D + Lane F: BROWSE-POINTS interface (round-9 C1 — **LANDED**, code live)
|
## → Lane D + Lane F: BROWSE-POINTS interface (round-9 C1 — **LANDED**, code live)
|
||||||
|
|
||||||
**Status: shipped.** `room.browsePoints[]` is live on the room API (commit `5ae635a`, ref
|
**Status: shipped.** `room.browsePoints[]` is live on the room API (commit `5ae635a`, ref
|
||||||
|
|||||||
BIN
docs/shots/laneC/r41_band_room.png
Normal file
|
After Width: | Height: | Size: 921 KiB |
BIN
docs/shots/laneC/r41_milkbar.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
BIN
docs/shots/laneC/r41_noassets_control.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
docs/shots/laneC/r41_opshop_wardrobe.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
docs/shots/laneC/r41_pub.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/shots/laneC/r41_pub_before.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
docs/shots/laneC/r41_record_dj_booth.png
Normal file
|
After Width: | Height: | Size: 692 KiB |
BIN
docs/shots/laneC/r41_record_shop.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/shots/laneC/r41_stall_bins.png
Normal file
|
After Width: | Height: | Size: 1.5 MiB |
903
web/assets/wardrobe/stock_opshop_index.json
Normal file
@ -0,0 +1,903 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"kind": "opshop",
|
||||||
|
"atlas_px": 1024,
|
||||||
|
"cell_w": 128,
|
||||||
|
"cell_h": 152,
|
||||||
|
"count": 52,
|
||||||
|
"atlases": [
|
||||||
|
"wardrobe_atlas_00.webp"
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"id": "wr_0000",
|
||||||
|
"title": "Acid Smiley Tee",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.549805,
|
||||||
|
0.125,
|
||||||
|
0.658203
|
||||||
|
],
|
||||||
|
"aspect": 1.1532,
|
||||||
|
"src": "top_i_bootleg_acid_house_smiley_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0001",
|
||||||
|
"title": "Hip Hop Crew Tee",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 5,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.507812,
|
||||||
|
0.427734,
|
||||||
|
0.632812,
|
||||||
|
0.541016
|
||||||
|
],
|
||||||
|
"aspect": 1.1034,
|
||||||
|
"src": "top_i_bootleg_hip_hop_crew_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0002",
|
||||||
|
"title": "Rave Smileys Tee",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.380859,
|
||||||
|
0.427734,
|
||||||
|
0.505859,
|
||||||
|
0.542969
|
||||||
|
],
|
||||||
|
"aspect": 1.0847,
|
||||||
|
"src": "top_i_bootleg_rave_smileys_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0003",
|
||||||
|
"title": "Alien Rave Tee",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 5,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.253906,
|
||||||
|
0.300781,
|
||||||
|
0.378906,
|
||||||
|
0.423828
|
||||||
|
],
|
||||||
|
"aspect": 1.0159,
|
||||||
|
"src": "top_i_paddys_bootleg_rave_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0004",
|
||||||
|
"title": "UV Crop Top",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 3,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.302734,
|
||||||
|
0.660156,
|
||||||
|
0.427734,
|
||||||
|
0.748047
|
||||||
|
],
|
||||||
|
"aspect": 1.4222,
|
||||||
|
"src": "top_i_paddy_s_market_uv_bootleg_crop_top.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0005",
|
||||||
|
"title": "Skull Band Tee",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 6,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.380859,
|
||||||
|
0.549805,
|
||||||
|
0.505859,
|
||||||
|
0.651367
|
||||||
|
],
|
||||||
|
"aspect": 1.2308,
|
||||||
|
"src": "top_i_band_tee_bootleg.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0006",
|
||||||
|
"title": "8-Ball Tee",
|
||||||
|
"artist": "Street Stall",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.126953,
|
||||||
|
0.300781,
|
||||||
|
0.251953,
|
||||||
|
0.424805
|
||||||
|
],
|
||||||
|
"aspect": 1.0079,
|
||||||
|
"src": "top_i_8_ball_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0007",
|
||||||
|
"title": "Splatter Smiley Tee",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.507812,
|
||||||
|
0.300781,
|
||||||
|
0.632812,
|
||||||
|
0.421875
|
||||||
|
],
|
||||||
|
"aspect": 1.0323,
|
||||||
|
"src": "top_i_rave_splatter_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0008",
|
||||||
|
"title": "UV Circuit Long Sleeve",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 6,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.300781,
|
||||||
|
0.125,
|
||||||
|
0.425781
|
||||||
|
],
|
||||||
|
"aspect": 1.0,
|
||||||
|
"src": "top_i_uv_reactive_top.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0009",
|
||||||
|
"title": "UV Spiral Crop Top",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.429688,
|
||||||
|
0.660156,
|
||||||
|
0.554688,
|
||||||
|
0.746094
|
||||||
|
],
|
||||||
|
"aspect": 1.4545,
|
||||||
|
"src": "top_i_uv_reactive_crop_top_womens.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0010",
|
||||||
|
"title": "Squiggle Jumper",
|
||||||
|
"artist": "Coogee Knits",
|
||||||
|
"price": 12,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.253906,
|
||||||
|
0.549805,
|
||||||
|
0.378906,
|
||||||
|
0.657227
|
||||||
|
],
|
||||||
|
"aspect": 1.1636,
|
||||||
|
"src": "top_i_classic_knit_sweater.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0011",
|
||||||
|
"title": "Worm Knit Jumper",
|
||||||
|
"artist": "Coogee Knits",
|
||||||
|
"price": 10,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.507812,
|
||||||
|
0.549805,
|
||||||
|
0.632812,
|
||||||
|
0.647461
|
||||||
|
],
|
||||||
|
"aspect": 1.28,
|
||||||
|
"src": "top_i_mens_knit_sweater.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0012",
|
||||||
|
"title": "Signature Knit",
|
||||||
|
"artist": "Coogee Knits",
|
||||||
|
"price": 12,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.634766,
|
||||||
|
0.427734,
|
||||||
|
0.759766,
|
||||||
|
0.541016
|
||||||
|
],
|
||||||
|
"aspect": 1.1034,
|
||||||
|
"src": "top_i_signature_knit_sweater.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0013",
|
||||||
|
"title": "Puppy Jumper",
|
||||||
|
"artist": "Nan Knitted It",
|
||||||
|
"price": 3,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.634766,
|
||||||
|
0.300781,
|
||||||
|
0.759766,
|
||||||
|
0.420898
|
||||||
|
],
|
||||||
|
"aspect": 1.0407,
|
||||||
|
"src": "top_i_mens_cartoon_sweater.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0014",
|
||||||
|
"title": "Squiggle Knit Dress",
|
||||||
|
"artist": "Coogee Knits",
|
||||||
|
"price": 14,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.111328,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.75,
|
||||||
|
"src": "top_i_womens_knit_dress.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0015",
|
||||||
|
"title": "Loud Hawaiian Shirt",
|
||||||
|
"artist": "Rissole Classic",
|
||||||
|
"price": 6,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.253906,
|
||||||
|
0.427734,
|
||||||
|
0.378906,
|
||||||
|
0.544922
|
||||||
|
],
|
||||||
|
"aspect": 1.0667,
|
||||||
|
"src": "top_i_loud_hawaiian_shirt.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0016",
|
||||||
|
"title": "Loud Dog Shirt",
|
||||||
|
"artist": "Mombo",
|
||||||
|
"price": 8,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.380859,
|
||||||
|
0.300781,
|
||||||
|
0.505859,
|
||||||
|
0.422852
|
||||||
|
],
|
||||||
|
"aspect": 1.024,
|
||||||
|
"src": "top_i_mombo_loud_shirt.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0017",
|
||||||
|
"title": "Loud Art Shirt",
|
||||||
|
"artist": "Mombo",
|
||||||
|
"price": 8,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.40332,
|
||||||
|
0.150391,
|
||||||
|
0.52832,
|
||||||
|
0.286133
|
||||||
|
],
|
||||||
|
"aspect": 0.9209,
|
||||||
|
"src": "top_i_womens_loud_shirt.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0018",
|
||||||
|
"title": "Camo Field Jacket",
|
||||||
|
"artist": "Army Disposals",
|
||||||
|
"price": 15,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.761719,
|
||||||
|
0.549805,
|
||||||
|
0.886719,
|
||||||
|
0.644531
|
||||||
|
],
|
||||||
|
"aspect": 1.3196,
|
||||||
|
"src": "top_i_army_disposal_camo_jacket.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0019",
|
||||||
|
"title": "Olive Parka",
|
||||||
|
"artist": "Army Disposals",
|
||||||
|
"price": 18,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.761719,
|
||||||
|
0.300781,
|
||||||
|
0.886719,
|
||||||
|
0.420898
|
||||||
|
],
|
||||||
|
"aspect": 1.0407,
|
||||||
|
"src": "top_i_army_disposal_parka_jacket.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0020",
|
||||||
|
"title": "Heavy Hoodie",
|
||||||
|
"artist": "Stoosh",
|
||||||
|
"price": 9,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.427734,
|
||||||
|
0.125,
|
||||||
|
0.547852
|
||||||
|
],
|
||||||
|
"aspect": 1.0407,
|
||||||
|
"src": "top_i_stoosh_hoodie.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0021",
|
||||||
|
"title": "Kangaroo Pocket Hoodie",
|
||||||
|
"artist": "Best & Cheapest",
|
||||||
|
"price": 7,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.657227,
|
||||||
|
0.150391,
|
||||||
|
0.782227,
|
||||||
|
0.27832
|
||||||
|
],
|
||||||
|
"aspect": 0.9771,
|
||||||
|
"src": "top_i_kangaroo_pocket_hoodie.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0022",
|
||||||
|
"title": "Ribbed Singlet",
|
||||||
|
"artist": "Kaymart",
|
||||||
|
"price": 2,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.113281,
|
||||||
|
0.0,
|
||||||
|
0.201172,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.5921,
|
||||||
|
"src": "top_i_womens_ribbed_singlet.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0023",
|
||||||
|
"title": "Glitter Baby Tee",
|
||||||
|
"artist": "Kaymart",
|
||||||
|
"price": 3,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.634766,
|
||||||
|
0.549805,
|
||||||
|
0.759766,
|
||||||
|
0.647461
|
||||||
|
],
|
||||||
|
"aspect": 1.28,
|
||||||
|
"src": "top_i_womens_glitter_baby_tee.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0024",
|
||||||
|
"title": "Leather Jacket",
|
||||||
|
"artist": "Trash & Treasure",
|
||||||
|
"price": 25,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.761719,
|
||||||
|
0.427734,
|
||||||
|
0.886719,
|
||||||
|
0.539062
|
||||||
|
],
|
||||||
|
"aspect": 1.1228,
|
||||||
|
"src": "top_i_womens_leather_jacket.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0025",
|
||||||
|
"title": "Colourblock Windbreaker",
|
||||||
|
"artist": "Trackside",
|
||||||
|
"price": 8,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.78418,
|
||||||
|
0.150391,
|
||||||
|
0.90918,
|
||||||
|
0.276367
|
||||||
|
],
|
||||||
|
"aspect": 0.9922,
|
||||||
|
"src": "top_i_womens_windbreaker.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0026",
|
||||||
|
"title": "Star Track Jacket",
|
||||||
|
"artist": "Trackside",
|
||||||
|
"price": 9,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.126953,
|
||||||
|
0.549805,
|
||||||
|
0.251953,
|
||||||
|
0.658203
|
||||||
|
],
|
||||||
|
"aspect": 1.1532,
|
||||||
|
"src": "top_i_italia_track_jacket.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0027",
|
||||||
|
"title": "Satin Cami",
|
||||||
|
"artist": "Club Rack",
|
||||||
|
"price": 3,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.203125,
|
||||||
|
0.0,
|
||||||
|
0.310547,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.7237,
|
||||||
|
"src": "top_i_cami_top.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0028",
|
||||||
|
"title": "Silver Phat Pants",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 12,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.3125,
|
||||||
|
0.0,
|
||||||
|
0.436523,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.8355,
|
||||||
|
"src": "bottom_i_phat_pants.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0029",
|
||||||
|
"title": "Flame Phat Pants",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 14,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.438477,
|
||||||
|
0.0,
|
||||||
|
0.551758,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.7632,
|
||||||
|
"src": "bottom_i_bootleg_uv_rave_phat_pants.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0030",
|
||||||
|
"title": "Sun Phat Pants",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 12,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.553711,
|
||||||
|
0.0,
|
||||||
|
0.666992,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.7632,
|
||||||
|
"src": "bottom_i_womens_rave_phat_pant.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0031",
|
||||||
|
"title": "Colourblock Baggy Jeans",
|
||||||
|
"artist": "Criss Colors",
|
||||||
|
"price": 16,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.668945,
|
||||||
|
0.0,
|
||||||
|
0.758789,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.6053,
|
||||||
|
"src": "bottom_i_baggy_colourblock_jeans.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0032",
|
||||||
|
"title": "Baggy Jeans",
|
||||||
|
"artist": "Trash & Treasure",
|
||||||
|
"price": 8,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.760742,
|
||||||
|
0.0,
|
||||||
|
0.867188,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.7171,
|
||||||
|
"src": "bottom_i_mens_baggy_jeans.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0033",
|
||||||
|
"title": "Wide Leg Jeans",
|
||||||
|
"artist": "Trash & Treasure",
|
||||||
|
"price": 8,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.869141,
|
||||||
|
0.0,
|
||||||
|
0.978516,
|
||||||
|
0.148438
|
||||||
|
],
|
||||||
|
"aspect": 0.7368,
|
||||||
|
"src": "bottom_i_womens_wide_leg_jeans.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0034",
|
||||||
|
"title": "Camo Parachute Pants",
|
||||||
|
"artist": "Army Disposals",
|
||||||
|
"price": 10,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.150391,
|
||||||
|
0.095703,
|
||||||
|
0.298828
|
||||||
|
],
|
||||||
|
"aspect": 0.6447,
|
||||||
|
"src": "bottom_i_camo_parachute_pants.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0035",
|
||||||
|
"title": "Camo Cargo Pants",
|
||||||
|
"artist": "Army Disposals",
|
||||||
|
"price": 10,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.097656,
|
||||||
|
0.150391,
|
||||||
|
0.199219,
|
||||||
|
0.298828
|
||||||
|
],
|
||||||
|
"aspect": 0.6842,
|
||||||
|
"src": "bottom_i_army_disposal_camo_cargo_pants.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0036",
|
||||||
|
"title": "Boardies",
|
||||||
|
"artist": "Rissole Classic",
|
||||||
|
"price": 5,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.126953,
|
||||||
|
0.427734,
|
||||||
|
0.251953,
|
||||||
|
0.547852
|
||||||
|
],
|
||||||
|
"aspect": 1.0407,
|
||||||
|
"src": "bottom_i_boardies.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0037",
|
||||||
|
"title": "Zigzag Boardshorts",
|
||||||
|
"artist": "Fang",
|
||||||
|
"price": 6,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.276367,
|
||||||
|
0.150391,
|
||||||
|
0.401367,
|
||||||
|
0.295898
|
||||||
|
],
|
||||||
|
"aspect": 0.8591,
|
||||||
|
"src": "bottom_i_fang_boardshort.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0038",
|
||||||
|
"title": "Trackie Dacks",
|
||||||
|
"artist": "Trackside",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.201172,
|
||||||
|
0.150391,
|
||||||
|
0.274414,
|
||||||
|
0.298828
|
||||||
|
],
|
||||||
|
"aspect": 0.4934,
|
||||||
|
"src": "bottom_i_track_pants.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0039",
|
||||||
|
"title": "Squiggle Knit Skirt",
|
||||||
|
"artist": "Coogee Knits",
|
||||||
|
"price": 9,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "garment",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.530273,
|
||||||
|
0.150391,
|
||||||
|
0.655273,
|
||||||
|
0.28125
|
||||||
|
],
|
||||||
|
"aspect": 0.9552,
|
||||||
|
"src": "bottom_i_womens_knit_skirt.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0040",
|
||||||
|
"title": "Bucket Hat",
|
||||||
|
"artist": "Kangaruu",
|
||||||
|
"price": 6,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.751953,
|
||||||
|
0.101562,
|
||||||
|
0.829102
|
||||||
|
],
|
||||||
|
"aspect": 1.3165,
|
||||||
|
"src": "hat_i_kangaruu_bucket_hat.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0041",
|
||||||
|
"title": "Terry Towelling Hat",
|
||||||
|
"artist": "Rissole Classic",
|
||||||
|
"price": 2,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.888672,
|
||||||
|
0.549805,
|
||||||
|
0.973633,
|
||||||
|
0.639648
|
||||||
|
],
|
||||||
|
"aspect": 0.9457,
|
||||||
|
"src": "hat_i_terry_towelling_hat.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0042",
|
||||||
|
"title": "Crochet Bucket Hat",
|
||||||
|
"artist": "Nan Knitted It",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.207031,
|
||||||
|
0.751953,
|
||||||
|
0.308594,
|
||||||
|
0.822266
|
||||||
|
],
|
||||||
|
"aspect": 1.4444,
|
||||||
|
"src": "hat_i_knit_bucket_hat.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0043",
|
||||||
|
"title": "Olive Beanie",
|
||||||
|
"artist": "Army Disposals",
|
||||||
|
"price": 3,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.867188,
|
||||||
|
0.660156,
|
||||||
|
0.96875,
|
||||||
|
0.741211
|
||||||
|
],
|
||||||
|
"aspect": 1.253,
|
||||||
|
"src": "hat_i_army_disposal_beanie.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0044",
|
||||||
|
"title": "Rainbow Patch Cap",
|
||||||
|
"artist": "Paddy's Bootleg",
|
||||||
|
"price": 5,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.0,
|
||||||
|
0.660156,
|
||||||
|
0.09668,
|
||||||
|
0.75
|
||||||
|
],
|
||||||
|
"aspect": 1.0761,
|
||||||
|
"src": "hat_i_paddys_bootleg_cap.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0045",
|
||||||
|
"title": "Faded Snapback",
|
||||||
|
"artist": "Street Stall",
|
||||||
|
"price": 4,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.098633,
|
||||||
|
0.660156,
|
||||||
|
0.193359,
|
||||||
|
0.75
|
||||||
|
],
|
||||||
|
"aspect": 1.0543,
|
||||||
|
"src": "hat_i_snapback_faded.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0046",
|
||||||
|
"title": "Double Pluggers",
|
||||||
|
"artist": "Rissole Classic",
|
||||||
|
"price": 2,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.556641,
|
||||||
|
0.660156,
|
||||||
|
0.658203,
|
||||||
|
0.743164
|
||||||
|
],
|
||||||
|
"aspect": 1.2235,
|
||||||
|
"src": "shoes_i_double_pluggers.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0047",
|
||||||
|
"title": "Chunky Boots",
|
||||||
|
"artist": "Trash & Treasure",
|
||||||
|
"price": 20,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.660156,
|
||||||
|
0.660156,
|
||||||
|
0.761719,
|
||||||
|
0.743164
|
||||||
|
],
|
||||||
|
"aspect": 1.2235,
|
||||||
|
"src": "shoes_i_chunky_boots.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0048",
|
||||||
|
"title": "Peace Sign Sneakers",
|
||||||
|
"artist": "Doof Merch",
|
||||||
|
"price": 14,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.763672,
|
||||||
|
0.660156,
|
||||||
|
0.865234,
|
||||||
|
0.742188
|
||||||
|
],
|
||||||
|
"aspect": 1.2381,
|
||||||
|
"src": "shoes_i_peace_sign_sneakers.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0049",
|
||||||
|
"title": "Platform Sneakers",
|
||||||
|
"artist": "Club Rack",
|
||||||
|
"price": 16,
|
||||||
|
"price_band": "collector",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.103516,
|
||||||
|
0.751953,
|
||||||
|
0.205078,
|
||||||
|
0.829102
|
||||||
|
],
|
||||||
|
"aspect": 1.3165,
|
||||||
|
"src": "shoes_i_platform_sneakers_womens.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0050",
|
||||||
|
"title": "Plain Tote",
|
||||||
|
"artist": "Best & Cheapest",
|
||||||
|
"price": 1,
|
||||||
|
"price_band": "bargain",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.195312,
|
||||||
|
0.660156,
|
||||||
|
0.248047,
|
||||||
|
0.75
|
||||||
|
],
|
||||||
|
"aspect": 0.587,
|
||||||
|
"src": "bag_plaintote.png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wr_0051",
|
||||||
|
"title": "Eye Shoulder Bag",
|
||||||
|
"artist": "Club Rack",
|
||||||
|
"price": 7,
|
||||||
|
"price_band": "mid",
|
||||||
|
"slot": "small",
|
||||||
|
"atlas": "wardrobe_atlas_00.webp",
|
||||||
|
"uv": [
|
||||||
|
0.25,
|
||||||
|
0.660156,
|
||||||
|
0.300781,
|
||||||
|
0.75
|
||||||
|
],
|
||||||
|
"aspect": 0.5652,
|
||||||
|
"src": "bag_bag96.png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"material": {
|
||||||
|
"alphaTest": 0.45,
|
||||||
|
"side": "double",
|
||||||
|
"roughness": 0.86
|
||||||
|
},
|
||||||
|
"source": "90sDJsim/web/world/media/doll \u2014 441 hand-prompted 1990-99 Australian garment layers; 52 selected BY NAME (ruling 3), cropped to alpha bbox, packed into one atlas. Labels are parody per the house law (AUDIT.md: no real brands ship); the parody IS the period voice."
|
||||||
|
}
|
||||||
BIN
web/assets/wardrobe/wardrobe_atlas_00.webp
Normal file
|
After Width: | Height: | Size: 241 KiB |
@ -74,6 +74,9 @@ import { createDig, binSeed } from './js/interiors/dig.js';
|
|||||||
import { createWallet } from './js/interiors/wallet.js';
|
import { createWallet } from './js/interiors/wallet.js';
|
||||||
import { collapseBuyItem } from './js/interiors/stockpack.js';
|
import { collapseBuyItem } from './js/interiors/stockpack.js';
|
||||||
import { createSell, sellOffer, sellableIn, nearCounter, SELL_DIST } from './js/interiors/sell.js';
|
import { createSell, sellOffer, sellableIn, nearCounter, SELL_DIST } from './js/interiors/sell.js';
|
||||||
|
// [R41 §41.4] the kit catalogue + its own drift check (kit41.kitAudit compares every hardcoded row
|
||||||
|
// against the live manifest and asserts the three traps Lane E flagged). Exposed for the harness.
|
||||||
|
import { KIT, POOLS, BOOTHS, CONTROL_NODES, kitAudit } from './js/interiors/kit41.js';
|
||||||
|
|
||||||
// ?stock=real — feed Lane E's GODVERSE record-sleeve pack through the stockAdapter seam.
|
// ?stock=real — feed Lane E's GODVERSE record-sleeve pack through the stockAdapter seam.
|
||||||
const STOCK_REAL = new URLSearchParams(location.search).get('stock') === 'real';
|
const STOCK_REAL = new URLSearchParams(location.search).get('stock') === 'real';
|
||||||
@ -82,7 +85,16 @@ const STOCK_REAL = new URLSearchParams(location.search).get('stock') === 'real';
|
|||||||
// demonstrable from this page, which is what makes an F gate on either of them non-vacuous.
|
// demonstrable from this page, which is what makes an F gate on either of them non-vacuous.
|
||||||
const OPSHOP_DIG = (() => { const v = new URLSearchParams(location.search).get('opshopdig'); return v != null && v !== '0'; })();
|
const OPSHOP_DIG = (() => { const v = new URLSearchParams(location.search).get('opshopdig'); return v != null && v !== '0'; })();
|
||||||
if (STOCK_REAL) for (const t of ['record', 'book', 'toy']) preloadStockPack(t); // fail-soft per type
|
if (STOCK_REAL) for (const t of ['record', 'book', 'toy']) preloadStockPack(t); // fail-soft per type
|
||||||
const realAdapterFor = (type) => (STOCK_REAL ? makeStockAdapter(getStockPack(type)) : null);
|
// [R41 §41.4 — THE WARDROBE] The op-shop pack lives under its OWN base (`assets/wardrobe/`) because it is
|
||||||
|
// not a GODVERSE crate: it is 52 named 1990s Australian garment layers off one 240 KB atlas, feeding the
|
||||||
|
// clothes rails and the shelves. Cache identity is (type, base) since R24, so it coexists with the
|
||||||
|
// town-wide packs. ONE fetch, only under ?stock=real — a default boot and ?noassets fetch nothing.
|
||||||
|
// → LANE F: the same one line in index.html turns it on in the game (LANE_C_NOTES §41).
|
||||||
|
const WARDROBE_BASE = 'assets/wardrobe/';
|
||||||
|
if (STOCK_REAL) preloadStockPack('opshop', { base: WARDROBE_BASE });
|
||||||
|
const realAdapterFor = (type) => (STOCK_REAL
|
||||||
|
? makeStockAdapter(getStockPack(type, type === 'opshop' ? WARDROBE_BASE : undefined))
|
||||||
|
: null);
|
||||||
|
|
||||||
// ── buy loop v0: in-memory wallet + cash HUD + inventory (I). F wires the same wallet on the street. ──
|
// ── buy loop v0: in-memory wallet + cash HUD + inventory (I). F wires the same wallet on the street. ──
|
||||||
const wallet = createWallet(1990);
|
const wallet = createWallet(1990);
|
||||||
@ -156,7 +168,9 @@ function rebuild() {
|
|||||||
const lot = parseLot($('lot').value);
|
const lot = parseLot($('lot').value);
|
||||||
const shop = { id: 's' + seed, type, name: type.toUpperCase() + ' ' + seed, seed, storeys: 1, lot };
|
const shop = { id: 's' + seed, type, name: type.toUpperCase() + ' ' + seed, seed, storeys: 1, lot };
|
||||||
current = buildInterior(shop, THREE, { archetype: arch, useGLB, manifest: useGLB ? glbManifest : undefined,
|
current = buildInterior(shop, THREE, { archetype: arch, useGLB, manifest: useGLB ? glbManifest : undefined,
|
||||||
stock: STOCK_REAL ? 'real' : undefined, fittingOpts: OPSHOP_DIG ? { rummageBin: { dig: true } } : undefined });
|
stock: STOCK_REAL ? 'real' : undefined,
|
||||||
|
stockBase: (STOCK_REAL && type === 'opshop') ? WARDROBE_BASE : undefined, // [R41] the wardrobe pack
|
||||||
|
fittingOpts: OPSHOP_DIG ? { rummageBin: { dig: true } } : undefined });
|
||||||
scene.add(current.group);
|
scene.add(current.group);
|
||||||
// place the player at the spawn
|
// place the player at the spawn
|
||||||
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
||||||
@ -302,14 +316,28 @@ function move(dt) {
|
|||||||
// ── draw sweep: worst-room draw count must be ≤350 (round-6 law, decision #2). Asserts so it
|
// ── draw sweep: worst-room draw count must be ≤350 (round-6 law, decision #2). Asserts so it
|
||||||
// can't regress; F mirrors this in the qa harness. Measures every type × archetype, GLB off AND on.
|
// can't regress; F mirrors this in the qa harness. Measures every type × archetype, GLB off AND on.
|
||||||
const DRAW_LAW = 350;
|
const DRAW_LAW = 350;
|
||||||
|
// [R41 §41.4] THE SWEEP WAS MEASURING A ROOM THAT WASN'T THERE. It removed every interior in the scene
|
||||||
|
// EXCEPT `current` — the live room the page is showing — so every per-type number carried a constant
|
||||||
|
// offset from whatever room the page last built (seed 1990, type = the first entry, a record shop). It
|
||||||
|
// looked harmless while nothing changed; the moment §41.4 furnished the record shop, `toy` — a type this
|
||||||
|
// round never touches — "gained" 4 draws, in both modes. The sweep now clears the scene of ALL interiors
|
||||||
|
// and puts `current` back at the end (rebuild()), so a type's number is that type's room and nothing else.
|
||||||
|
// Numbers therefore drop slightly against R39/R40 prints of this same instrument: that is the phantom
|
||||||
|
// leaving, not the rooms getting cheaper.
|
||||||
async function drawSweep(opts = {}) {
|
async function drawSweep(opts = {}) {
|
||||||
const wasCurrent = current;
|
if (current) { current.dispose(); current = null; clearDebug(); }
|
||||||
for (const s of scene.children.filter(o => o.userData && o.userData.kind === 'interior')) if (s !== (current && current.group)) scene.remove(s);
|
for (const s of scene.children.filter(o => o.userData && o.userData.kind === 'interior')) scene.remove(s);
|
||||||
let manifest = null;
|
let manifest = null;
|
||||||
if (opts.glb) { manifest = await fetch('assets/manifest.json?x=' + Date.now()).then(r => r.ok ? r.json() : null).catch(() => null); }
|
if (opts.glb) { manifest = await fetch('assets/manifest.json?x=' + Date.now()).then(r => r.ok ? r.json() : null).catch(() => null); }
|
||||||
|
// [R41] `opts.stock` measures the ?stock=real arm too — real covers/garments come off shared atlases as
|
||||||
|
// `buyMesh` meshes that batch.js deliberately leaves alone, so they are the one stock path whose draws
|
||||||
|
// scale with fittings rather than with materials. Worth its own number now that a clothes rail carries
|
||||||
|
// twelve of them. Needs the packs preloaded (the page does that under ?stock=real).
|
||||||
const measure = async (type, arch) => {
|
const measure = async (type, arch) => {
|
||||||
const r = buildInterior({ id: type, type, seed: 1990, storeys: 1 }, THREE,
|
const r = buildInterior({ id: type, type, seed: 1990, storeys: 1 }, THREE,
|
||||||
{ archetype: arch, useGLB: opts.glb, manifest });
|
{ archetype: arch, useGLB: opts.glb, manifest, noKit: opts.noKit,
|
||||||
|
stock: opts.stock ? 'real' : undefined,
|
||||||
|
stockBase: (opts.stock && type === 'opshop') ? WARDROBE_BASE : undefined });
|
||||||
scene.add(r.group); if (opts.glb) await r.glbReady;
|
scene.add(r.group); if (opts.glb) await r.glbReady;
|
||||||
camera.position.set(r.dims.W * 0.3, r.dims.H * 0.6, r.dims.D / 2 - 0.8); camera.lookAt(0, 0.9, -r.dims.D * 0.15);
|
camera.position.set(r.dims.W * 0.3, r.dims.H * 0.6, r.dims.D / 2 - 0.8); camera.lookAt(0, 0.9, -r.dims.D * 0.15);
|
||||||
renderer.info.reset(); renderer.render(scene, camera);
|
renderer.info.reset(); renderer.render(scene, camera);
|
||||||
@ -587,6 +615,8 @@ function frame() {
|
|||||||
|
|
||||||
// expose for headless verification (screenshot harness, workflow checks)
|
// expose for headless verification (screenshot harness, workflow checks)
|
||||||
window.PROCITY_C = { buildInterior, THREE, SHOP_TYPES, ARCHETYPE_KEYS, soak, drawSweep, DRAW_LAW, rebuild, get current() { return current; }, scene, camera, renderer,
|
window.PROCITY_C = { buildInterior, THREE, SHOP_TYPES, ARCHETYPE_KEYS, soak, drawSweep, DRAW_LAW, rebuild, get current() { return current; }, scene, camera, renderer,
|
||||||
|
KIT, POOLS, BOOTHS, CONTROL_NODES, WARDROBE_BASE,
|
||||||
|
kitAudit: (m) => kitAudit(m || glbManifest), // [R41] catalogue-vs-manifest drift + trap asserts
|
||||||
DIG_ON, STOCK_REAL, OPSHOP_DIG, digSoak, openDigOn, binUnderAim, get dig() { return dig; }, wallet, preloadStockPack, getStockPack, makeStockAdapter,
|
DIG_ON, STOCK_REAL, OPSHOP_DIG, digSoak, openDigOn, binUnderAim, get dig() { return dig; }, wallet, preloadStockPack, getStockPack, makeStockAdapter,
|
||||||
shelfUnderAim, showShelfCard, buyShelfOffer, shelfBuySoak,
|
shelfUnderAim, showShelfCard, buyShelfOffer, shelfBuySoak,
|
||||||
SELL_STUB, sell, trySell, sellOffer, sellableIn, nearCounter, SELL_DIST };
|
SELL_STUB, sell, trySell, sellOffer, sellableIn, nearCounter, SELL_DIST };
|
||||||
|
|||||||
@ -42,6 +42,14 @@ function flushBuckets(ctx, target, buckets, stockTag, bake) {
|
|||||||
for (const m of b.meshes) m.parent && m.parent.remove(m);
|
for (const m of b.meshes) m.parent && m.parent.remove(m);
|
||||||
const mm = new THREE.Mesh(merged, b.material);
|
const mm = new THREE.Mesh(merged, b.material);
|
||||||
mm.userData = stockTag ? { isStock: true, batched: true } : { isFrame: true, batched: true };
|
mm.userData = stockTag ? { isStock: true, batched: true } : { isFrame: true, batched: true };
|
||||||
|
// [R41 §41.4] CARRY UNANIMOUS SUB-TAGS THROUGH THE MERGE. A frame mesh can be marked for later
|
||||||
|
// selective hiding (`boothGear` — the primitive DJ gear a GLB replaces; `tillPrimitive` — the till
|
||||||
|
// that `cash_register` replaces). Merging rebuilt userData from scratch and dropped those marks, so
|
||||||
|
// the hide that depends on them silently became a no-op. Only propagate when EVERY mesh in the
|
||||||
|
// bucket carries the tag: a bucket is one material, and a mark that isn't unanimous isn't a mark.
|
||||||
|
if (!stockTag)
|
||||||
|
for (const tag of ['boothGear', 'tillPrimitive'])
|
||||||
|
if (b.meshes.every(m => m.userData && m.userData[tag])) mm.userData[tag] = true;
|
||||||
target.add(mm);
|
target.add(mm);
|
||||||
saved += b.meshes.length - 1;
|
saved += b.meshes.length - 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -349,7 +349,11 @@ function counter(ctx, o = {}, r) {
|
|||||||
// Counter-top attach points (local space, on the benchtop top surface) for GLB counter-top props
|
// Counter-top attach points (local space, on the benchtop top surface) for GLB counter-top props
|
||||||
// (cash_register on the till spot; milkshake_mixer on the free left spot). y = benchtop top.
|
// (cash_register on the till spot; milkshake_mixer on the free left spot). y = benchtop top.
|
||||||
const counterTop = { y: h + 0.03, till: { x: w / 2 - 0.35, z: 0 }, appliance: { x: -w / 2 + 0.4, z: -0.02 } };
|
const counterTop = { y: h + 0.03, till: { x: w / 2 - 0.35, z: 0 }, appliance: { x: -w / 2 + 0.4, z: -0.02 } };
|
||||||
return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 0.3, slots: [], places: [g], counterTop };
|
// [R41] `o.glbId` overrides the default `counter_till` swap for one recipe — the pub's bar is Lane E's
|
||||||
|
// `plywood_bar` (1.51 × 0.82 × 1.05), which is what a band-room bar actually looks like. Recipe field
|
||||||
|
// `counterGlb`; absent ⇒ counter_till exactly as before.
|
||||||
|
return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: h + 0.3, slots: [], places: [g], counterTop,
|
||||||
|
glbId: o.glbId || null };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Glass display case — timber base + tinted glass top, a few "treasures" inside.
|
// Glass display case — timber base + tinted glass top, a few "treasures" inside.
|
||||||
@ -510,6 +514,244 @@ function ampStack(ctx, o = {}, r) {
|
|||||||
return { group: g, footprint: { w, d }, height: h, slots: [], places: [] };
|
return { group: g, footprint: { w, d }, height: h, slots: [], places: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── R41 §41.4 — THE KIT (Lane E's 110 new interior GLBs) ─────────────────────────────────
|
||||||
|
//
|
||||||
|
// ONE builder covers all 110. It picks an id from a pool (`kit41.js POOLS`) on the fitting's own seeded
|
||||||
|
// stream, reads that id's REAL footprint/height out of the catalogue, and builds a cheap primitive
|
||||||
|
// silhouette at exactly those metres. Two things follow, and they are the whole reason it is shaped this
|
||||||
|
// way:
|
||||||
|
// • `?noassets=1` gets a real, correctly-sized, walkable shop floor with ZERO fetches (the catalogue is
|
||||||
|
// a hardcoded table, and buildInterior is synchronous — see kit41.js's header on why not "read the
|
||||||
|
// manifest");
|
||||||
|
// • the occupancy grid reserves the GLB's own footprint, so the swap in glb.js needs no rescale and the
|
||||||
|
// path/browse guarantees computed before the depot answers stay true after it does.
|
||||||
|
//
|
||||||
|
// The chosen id rides out on `fitting.glbId`, which `glb.js upgradeFitting` prefers over the legacy
|
||||||
|
// per-kind `KIND_TO_GLB` map. Draw cost when the GLB is live: ONE, for every id in the catalogue.
|
||||||
|
import { KIT, POOLS, BOOTHS, CONTROL_NODES } from './kit41.js';
|
||||||
|
|
||||||
|
// Seeded muted palettes per form family — the placeholder should read as the right KIND of object even
|
||||||
|
// before (or without) its GLB, which is what makes ?noassets a playable build rather than a grey box.
|
||||||
|
const KITCOL = {
|
||||||
|
tub: ['#2c2f36', '#d8d5cc', '#2f5aa8', '#7a5a3a', '#5a6a55'],
|
||||||
|
rack: ['#6d4a30', '#8a7a5a', '#4a4f57', '#9c8d76'],
|
||||||
|
stack: ['#3a3f47', '#6d4a30', '#7a6a55'],
|
||||||
|
board: ['#9c8d76', '#c9c2b2', '#6d4a30'],
|
||||||
|
unit: ['#b8bcc4', '#8a8d94', '#5a4632', '#c8c2b4'],
|
||||||
|
cab: ['#2a2028', '#3a2a30', '#242a34'],
|
||||||
|
table: ['#6d4a30', '#4a3524', '#8a7a5a'],
|
||||||
|
seat: ['#6a2a2a', '#2a3a5a', '#4a3a2a', '#3a4a3a'],
|
||||||
|
barrel: ['#8a8d94', '#3a4a3a', '#6a5a3a', '#b0b4ba'],
|
||||||
|
post: ['#4a4f57', '#8a3a2a', '#8a8d94'],
|
||||||
|
slab: ['#22242a', '#3a3f47', '#6d5a3a'],
|
||||||
|
speaker: ['#16161a', '#1e1e24'],
|
||||||
|
deck: ['#26262c', '#3a3a42', '#b8bcc4'],
|
||||||
|
panel: ['#7a6a4a', '#4a4f57', '#c9c2b2'],
|
||||||
|
column: ['#c9c2b2', '#a8a49a', '#8a8d94'],
|
||||||
|
trolley: ['#8a8d94', '#4a4f57'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Primitive silhouettes. Each draws into `g` at base y=0, footprint centred on (0,0), "front" = +Z.
|
||||||
|
const KITFORM = {
|
||||||
|
tub(ctx, g, w, d, h, m, r) {
|
||||||
|
const th = 0.022, lip = h * 0.78;
|
||||||
|
ctx.box(w, th, d, m, 0, th / 2, 0, g); // floor
|
||||||
|
ctx.box(th, h, d, m, -w / 2 + th / 2, h / 2, 0, g); ctx.box(th, h, d, m, w / 2 - th / 2, h / 2, 0, g);
|
||||||
|
ctx.box(w, h, th, m, 0, h / 2, -d / 2 + th / 2, g); ctx.box(w, h * 0.86, th, m, 0, h * 0.43, d / 2 - th / 2, g);
|
||||||
|
ctx.box(w - 0.05, th, d - 0.05, m, 0, lip, 0, g); // goods ride up and crest the rim
|
||||||
|
},
|
||||||
|
rack(ctx, g, w, d, h, m, r) {
|
||||||
|
const leg = ctx.mat('#4a4038', 0.7);
|
||||||
|
const tiers = h > 0.95 ? 3 : 2;
|
||||||
|
[[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([sx, sz]) =>
|
||||||
|
ctx.box(0.05, h, 0.05, leg, sx * (w / 2 - 0.04), h / 2, sz * (d / 2 - 0.04), g));
|
||||||
|
for (let i = 0; i < tiers; i++) ctx.box(w, 0.035, d, m, 0, 0.14 + i * ((h - 0.16) / Math.max(1, tiers - 1 || 1)), 0, g);
|
||||||
|
ctx.box(w, h * 0.5, 0.03, m, 0, h * 0.25, -d / 2 + 0.015, g); // back panel
|
||||||
|
},
|
||||||
|
stack(ctx, g, w, d, h, m, r) {
|
||||||
|
const n = 4, ch = h / n, th = 0.02;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const dd = d * (1 - i * 0.14);
|
||||||
|
ctx.box(w, th, dd, m, 0, i * ch + th / 2, (d - dd) / 2 * -0.5, g);
|
||||||
|
ctx.box(w, ch * 0.9, th, m, 0, i * ch + ch * 0.45, -dd / 2, g);
|
||||||
|
ctx.box(th, ch * 0.9, dd, m, -w / 2, i * ch + ch * 0.45, (d - dd) / 2 * -0.5, g);
|
||||||
|
ctx.box(th, ch * 0.9, dd, m, w / 2, i * ch + ch * 0.45, (d - dd) / 2 * -0.5, g);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
board(ctx, g, w, d, h, m, r) {
|
||||||
|
const t = Math.max(0.03, Math.min(h, 0.05));
|
||||||
|
ctx.box(w, t, Math.max(d, 0.22), m, 0, t / 2, 0.08, g); // the plank
|
||||||
|
if (h > 0.4) { ctx.box(w, h, 0.03, m, 0, h / 2, -0.02, g); ctx.box(w, t, Math.max(d, 0.22), m, 0, h - t / 2, 0.08, g); }
|
||||||
|
[-1, 1].forEach(s => ctx.box(0.04, Math.min(h, 0.24), 0.2, ctx.mat('#4a4038', 0.7), s * (w / 2 - 0.08), -0.06, 0.02, g));
|
||||||
|
},
|
||||||
|
unit(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
ctx.box(w + 0.03, 0.04, d + 0.03, ctx.mat('#3a3730', 0.7), 0, h, 0, g); // capping
|
||||||
|
ctx.box(w * 0.8, h * 0.5, 0.02, ctx.mat('#2a2a30', 0.6), 0, h * 0.42, d / 2 + 0.01, g);
|
||||||
|
},
|
||||||
|
cab(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
const lit = ctx.mat('#e8c05a', 0.4, { emissive: new ctx.THREE.Color('#4a3a10') });
|
||||||
|
ctx.box(w * 0.86, h * 0.34, 0.03, lit, 0, h * 0.62, d / 2 + 0.015, g); // lit glass
|
||||||
|
ctx.box(w * 0.7, 0.1, 0.05, ctx.mat('#1a1a1e', 0.5), 0, h * 0.34, d / 2 + 0.02, g);
|
||||||
|
},
|
||||||
|
table(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, 0.05, d, m, 0, h, 0, g);
|
||||||
|
const leg = ctx.mat('#4a4038', 0.7);
|
||||||
|
if (Math.abs(w - d) < 0.25) ctx.cyl(0.05, h, leg, 0, h / 2, 0, g); // round bistro: centre post
|
||||||
|
else [[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([sx, sz]) =>
|
||||||
|
ctx.box(0.06, h, 0.06, leg, sx * (w / 2 - 0.1), h / 2, sz * (d / 2 - 0.1), g));
|
||||||
|
if (Math.abs(w - d) < 0.25) ctx.cyl(w * 0.28, 0.04, leg, 0, 0.02, 0, g); // and a foot
|
||||||
|
},
|
||||||
|
seat(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h * 0.5, d, m, 0, h * 0.25, 0, g); // base
|
||||||
|
ctx.box(w, h * 0.55, d * 0.22, m, 0, h * 0.62, -d / 2 + d * 0.11, g); // back
|
||||||
|
ctx.box(w, 0.1, d * 0.8, m, 0, h * 0.5, d * 0.06, g); // cushion
|
||||||
|
},
|
||||||
|
barrel(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.cyl(Math.min(w, d) / 2, h, m, 0, h / 2, 0, g);
|
||||||
|
ctx.cyl(Math.min(w, d) / 2 * 0.94, 0.03, ctx.mat('#3a3730', 0.6), 0, h, 0, g);
|
||||||
|
},
|
||||||
|
post(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.cyl(Math.min(0.06, Math.min(w, d) / 2), h, m, 0, h / 2, 0, g);
|
||||||
|
ctx.cyl(Math.min(w, d) / 2, h * 0.16, m, 0, h - h * 0.08, 0, g); // head
|
||||||
|
ctx.cyl(Math.min(w, d) / 2 * 0.9, 0.03, m, 0, 0.015, 0, g); // foot
|
||||||
|
},
|
||||||
|
slab(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
ctx.box(w + 0.02, 0.03, d + 0.02, ctx.mat('#b8bcc4', 0.4, { metalness: 0.6 }), 0, h * 0.7, 0, g);
|
||||||
|
},
|
||||||
|
speaker(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
const gr = ctx.mat('#0c0c10', 0.6);
|
||||||
|
ctx.box(w - 0.08, h * 0.3, 0.02, gr, 0, h * 0.3, d / 2, g);
|
||||||
|
ctx.box(w - 0.12, h * 0.14, 0.02, gr, 0, h * 0.78, d / 2, g);
|
||||||
|
},
|
||||||
|
deck(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
ctx.cyl(Math.min(w, d) * 0.32, 0.012, ctx.mat('#b8bcc4', 0.3, { metalness: 0.7 }), -w * 0.06, h + 0.006, 0, g);
|
||||||
|
ctx.box(w * 0.1, 0.012, d * 0.5, ctx.mat('#1a1a1e', 0.5), w * 0.36, h + 0.006, 0, g);
|
||||||
|
},
|
||||||
|
panel(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, Math.max(d, 0.03), m, 0, h / 2, 0, g);
|
||||||
|
ctx.box(w * 0.86, h * 0.7, 0.01, ctx.mat('#d9cfae', 0.8), 0, h * 0.52, Math.max(d, 0.03) / 2 + 0.006, g);
|
||||||
|
},
|
||||||
|
column(ctx, g, w, d, h, m, r) {
|
||||||
|
ctx.box(w, h, d, m, 0, h / 2, 0, g);
|
||||||
|
ctx.box(w + 0.08, 0.1, d + 0.08, m, 0, 0.05, 0, g);
|
||||||
|
ctx.box(w + 0.08, 0.1, d + 0.08, m, 0, h - 0.05, 0, g);
|
||||||
|
},
|
||||||
|
trolley(ctx, g, w, d, h, m, r) {
|
||||||
|
for (let i = 0; i < 3; i++) ctx.box(w, 0.03, d, m, 0, 0.14 + i * ((h - 0.16) / 2), 0, g);
|
||||||
|
[[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([sx, sz]) => {
|
||||||
|
ctx.box(0.04, h, 0.04, m, sx * (w / 2 - 0.03), h / 2, sz * (d / 2 - 0.03), g);
|
||||||
|
ctx.cyl(0.05, 0.03, ctx.mat('#2a2a30', 0.5), sx * (w / 2 - 0.03), 0.05, sz * (d / 2 - 0.03), g);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Where stock sits on each form (local coords), so a kit fitting is FULL, not an empty shell. Every slot
|
||||||
|
// is `pool:true` — the R39 trick: pooled white faces merge into the room's existing stock buckets, so a
|
||||||
|
// heaped tub costs ~0 extra draws (an un-pooled tinted fill cost one draw per item).
|
||||||
|
function kitSlots(form, w, d, h, kind, count) {
|
||||||
|
if (!kind) return [];
|
||||||
|
// A 12" SLEEVE IS 0.31 m SQUARE — a constant of the physical world, not a fraction of whatever tub it
|
||||||
|
// is standing in. Deriving it from the tub height (h * 1.3) put 0.42 m "records" in a 0.32 m crate and
|
||||||
|
// they read as posters; `recordBin`/`crate` have used 0.28–0.32 since v1 and this now matches them.
|
||||||
|
const SLEEVE_H = 0.30;
|
||||||
|
if (form === 'tub')
|
||||||
|
return [kind === 'sleeve'
|
||||||
|
? { kind: 'sleeve', x: 0, y: h * 0.72, z: 0, faceW: Math.min(0.31, w * 0.86), run: d * 0.84, height: SLEEVE_H, count, tilt: -0.32 }
|
||||||
|
: { kind: 'box', x: 0, y: h * 0.78, z: 0, ry: 0, run: w - 0.06, depth: d - 0.06, height: h * 0.8, count, lean: false, scatter: true, pool: true }];
|
||||||
|
if (form === 'rack' || form === 'trolley') {
|
||||||
|
// TIER HEIGHTS ARE FRACTIONS OF h, NOT AN EVEN LADDER, AND THEY SIT *IN* THE TUBS. Most of the rack
|
||||||
|
// pool is `3dstore` TUB RACKS — open tubs on a frame — so goods parked at y = h stand ON TOP of the
|
||||||
|
// tub rims and the tub itself reads as an empty wireframe box (it is thin-walled and open; at a
|
||||||
|
// grazing angle you see straight through it). 0.82h puts the sleeves inside the upper tub with the
|
||||||
|
// top third cresting the rim, which is what a record tub actually looks like, and it fills the hole.
|
||||||
|
const ys = form === 'trolley' ? [h * 0.92, h * 0.55, h * 0.18]
|
||||||
|
: (h > 0.95 ? [h * 0.86, h * 0.55, h * 0.22] : [h * 0.82, h * 0.28]);
|
||||||
|
return ys.map(y => (kind === 'sleeve'
|
||||||
|
? { kind: 'sleeve', x: 0, y, z: 0, faceW: Math.min(0.31, w * 0.9), run: d * 0.78, height: SLEEVE_H, count, tilt: -0.3 }
|
||||||
|
: { kind, x: 0, y: y + 0.02, z: 0, ry: 0, run: w - 0.1, depth: d * 0.68, height: 0.26, count, lean: kind === 'spine', pool: true }));
|
||||||
|
}
|
||||||
|
if (form === 'stack') {
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < 4; i++) out.push({ kind: 'sleeve', x: 0, y: (h / 4) * i + h / 4 * 0.62, z: 0, faceW: Math.min(0.31, w * 0.86), run: d * 0.6, height: SLEEVE_H * 0.85, count: Math.max(3, (count / 2) | 0), tilt: -0.28 });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
if (form === 'board')
|
||||||
|
return [{ kind: kind === 'sleeve' ? 'box' : kind, x: 0, y: 0.06, z: 0.06, ry: 0, run: w - 0.14, depth: 0.2, height: 0.28, count, lean: false, pool: true }];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build one kit fitting. `o.pool` = an id list from kit41 POOLS (required — no pool ⇒ a plain shelf).
|
||||||
|
// o.dig true ⇒ userData.kind 'bin' (the dig fires on it). DEFAULT FALSE: R39 held "what an op shop
|
||||||
|
// yields" for John, so only recipes that already dig (record, stall) ever pass it.
|
||||||
|
// o.stock 'sleeve'|'box'|'spine'|'garment' — what stock.js fills it with (null ⇒ empty prop)
|
||||||
|
// o.wall true ⇒ wall-mounted (no floor footprint stamped); o.mountY = base height
|
||||||
|
// o.maxRun clamp: ids whose longest floor dimension exceeds this are filtered out of the pool BEFORE
|
||||||
|
// the seeded pick (TRAP 3, generalised — a 4.6 m board in a 6 m room).
|
||||||
|
function kitProp(ctx, o = {}, r) {
|
||||||
|
const g = new ctx.THREE.Group();
|
||||||
|
const pool = (o.pool || []).filter(id => KIT[id] && (!o.maxRun || Math.max(KIT[id][0], KIT[id][1]) <= o.maxRun));
|
||||||
|
const id = pool.length ? pool[(r() * pool.length) | 0] : null;
|
||||||
|
const row = id ? KIT[id] : [0.6, 0.4, 0.6, 'unit'];
|
||||||
|
// WALL FITTINGS ARE X-MAJOR BY HOUSE CONVENTION (pegboard/barredWindow/artFrame all are, and
|
||||||
|
// placeAtWall's slot.ry is what turns local +X along the wall). Several kit ids are authored Z-major
|
||||||
|
// (`shelf_board_short` is 0.48 × 1.95), so a wall row is transposed here — and glb.js's autoYaw then
|
||||||
|
// reads the same swapped footprint and gives the GLB the matching quarter turn. Floor props are NOT
|
||||||
|
// transposed: the placer's own ry orients them.
|
||||||
|
const swap = !!o.wall && row[1] > row[0];
|
||||||
|
const [w, d, h, form] = swap ? [row[1], row[0], row[2], row[3]] : row;
|
||||||
|
const pal = KITCOL[form] || KITCOL.unit;
|
||||||
|
const m = ctx.mat(pal[(r() * pal.length) | 0], form === 'speaker' || form === 'deck' ? 0.6 : 0.82,
|
||||||
|
form === 'barrel' || form === 'post' ? { metalness: 0.25 } : {});
|
||||||
|
(KITFORM[form] || KITFORM.unit)(ctx, g, w, d, h, m, r);
|
||||||
|
const slots = kitSlots(form, w, d, h, o.stock, o.items ?? 8);
|
||||||
|
const diggable = o.dig === true && (form === 'tub' || form === 'rack' || form === 'stack');
|
||||||
|
g.userData = diggable ? { kind: 'bin', interactable: true } : { kind: o.uk || 'prop' };
|
||||||
|
const fit = {
|
||||||
|
group: g, footprint: { w: w + 0.08, d: d + 0.08 }, height: h, slots,
|
||||||
|
places: diggable ? [g] : [], glbId: id,
|
||||||
|
};
|
||||||
|
if (o.wall) { fit.wallMounted = true; fit.mountY = o.mountY ?? 1.45; }
|
||||||
|
return fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A DJ booth: a primitive bench with several GLBs standing ON it (`fitting.glbParts`, the one multi-GLB
|
||||||
|
// shape in the lane — DJ gear is desktop kit, a 1200 is 0.11 m tall). R41 PLACES the rigged models and
|
||||||
|
// records their control-node names; NOTHING DRIVES THEM this round (that is D/F's later work), and this
|
||||||
|
// builder deliberately ships no animation hook so nobody mistakes a placed node for a live one.
|
||||||
|
function djBooth(ctx, o = {}, r) {
|
||||||
|
const spec = BOOTHS[o.booth] || BOOTHS.venue;
|
||||||
|
const g = new ctx.THREE.Group();
|
||||||
|
const { w, d, h } = spec;
|
||||||
|
const body = ctx.mat(r() < 0.5 ? '#3a3128' : '#2a2a30', 0.8);
|
||||||
|
ctx.box(w, h, d, body, 0, h / 2, 0, g); // the bench
|
||||||
|
ctx.box(w + 0.06, 0.05, d + 0.06, ctx.mat('#1c1a18', 0.7), 0, h, 0, g); // bench top edge
|
||||||
|
ctx.box(w - 0.1, 0.28, d * 0.55, ctx.mat('#1a1a1e', 0.6), 0, h * 0.32, -d * 0.1, g); // rack space under
|
||||||
|
const nodes = {};
|
||||||
|
// primitive stand-ins on the bench top, drawn straight into `g` on ONE shared material so batch.js
|
||||||
|
// merges them (a child Group per part would have escaped the batcher and cost a draw each).
|
||||||
|
const gear = ctx.mat('#26262c', 0.6), plate = ctx.mat('#b8bcc4', 0.3, { metalness: 0.7 });
|
||||||
|
for (const p of spec.parts) {
|
||||||
|
const [pw, pd, ph] = KIT[p.id] || [0.3, 0.3, 0.1];
|
||||||
|
// `boothGear` marks the stand-in that a GLB part REPLACES. The bench underneath is deliberately not
|
||||||
|
// marked: a booth's GLBs are the gear only, so hiding the whole primitive (what a single-GLB swap
|
||||||
|
// does) left a 1200 and a mixer hovering at 0.92 m over bare floorboards. Measured on screen, R41.
|
||||||
|
ctx.box(pw, ph, pd, gear, p.x, h + ph / 2, p.z, g).userData.boothGear = true;
|
||||||
|
ctx.cyl(Math.min(pw, pd) * 0.3, 0.012, plate, p.x - pw * 0.06, h + ph + 0.006, p.z, g).userData.boothGear = true;
|
||||||
|
if (CONTROL_NODES[p.id]) nodes[p.id] = CONTROL_NODES[p.id];
|
||||||
|
}
|
||||||
|
g.userData = { kind: 'prop', djBooth: o.booth || 'venue', controlNodes: nodes };
|
||||||
|
return {
|
||||||
|
group: g, footprint: { w: w + 0.12, d: d + 0.12 }, height: h + 0.25, slots: [], places: [],
|
||||||
|
// consumed by glb.js: one GLB per part, planted on the bench top (y = h).
|
||||||
|
glbParts: spec.parts.map(p => ({ id: p.id, x: p.x, y: h, z: p.z, ry: p.ry || 0 })),
|
||||||
|
controlNodes: nodes,
|
||||||
|
};
|
||||||
|
}
|
||||||
export const FITTINGS = {
|
export const FITTINGS = {
|
||||||
recordBin, crate, crateStack, listeningCorner, rummageBin,
|
recordBin, crate, crateStack, listeningCorner, rummageBin,
|
||||||
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
|
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
|
||||||
@ -517,6 +759,13 @@ export const FITTINGS = {
|
|||||||
trestleTable, counter, glassCase,
|
trestleTable, counter, glassCase,
|
||||||
pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame,
|
pegboard, fridge, magazineRack, armchair, barredWindow, returnsSlot, escalator, artFrame,
|
||||||
stage, paSpeaker, ampStack,
|
stage, paSpeaker, ampStack,
|
||||||
|
// [R41 §41.4] the kit. Registered under several NAMES that all build the same way, because
|
||||||
|
// layout.js keys each spec's seeded stream on `fit-<kind>-<i>` — two specs sharing a kind in one recipe
|
||||||
|
// would share a stream and therefore pick the SAME id. One name per pool per recipe keeps them distinct.
|
||||||
|
kitBin: kitProp, kitRack: kitProp, kitWall: kitProp, kitWindow: kitProp, kitSleeve: kitProp,
|
||||||
|
kitTrolley: kitProp, kitColumn: kitProp,
|
||||||
|
kitSeat: kitProp, kitFloor: kitProp, kitVenue: kitProp, kitHot: kitProp, kitGear: kitProp,
|
||||||
|
djBooth,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a fitting by kind. Unknown kinds fall back to a wall shelf (never crash).
|
// Build a fitting by kind. Unknown kinds fall back to a wall shelf (never crash).
|
||||||
|
|||||||
@ -66,48 +66,122 @@ export function preloadManifest(url = 'assets/manifest.json') {
|
|||||||
}
|
}
|
||||||
export function manifestReady() { return _manifestP && typeof _manifestP.then === 'function' ? _manifestP : Promise.resolve(null); }
|
export function manifestReady() { return _manifestP && typeof _manifestP.then === 'function' ? _manifestP : Promise.resolve(null); }
|
||||||
|
|
||||||
// Upgrade one placed fitting in-place, if the manifest maps its kind. No-op when disabled/unavailable.
|
// [R41 §41.4] AUTO-FACING for the kit. The 110 new fittings carry no facing convention
|
||||||
// `manifest` is the resolved object (or null). ctx is the room build context (for the _disposed guard).
|
// (manifest.conventions.facing: "Lane B/C rotate directional props"), and several of them are the SAME
|
||||||
// Returns a promise that settles when the swap is done (or immediately when there is nothing to do), so
|
// MESH ROTATED 90° from a sibling — so a hand-tuned yaw table would be 110 rows of guesswork. Instead the
|
||||||
// callers can await a room's GLB upgrades (buildInterior exposes this as room.glbReady).
|
// yaw is MEASURED: the primitive placeholder was built at the manifest footprint, so if the GLB's own
|
||||||
|
// footprint is major-axis-swapped relative to the slot the grid reserved, it needs a quarter turn. Square
|
||||||
|
// props (aspect within 10%) are orientation-agnostic and get 0 — a rule, not a table.
|
||||||
|
function autoYaw(entry, fitting) {
|
||||||
|
const [ew, ed] = entry.footprint || [0, 0];
|
||||||
|
const fw = fitting.footprint.w, fd = fitting.footprint.d;
|
||||||
|
if (!ew || !ed || !fw || !fd) return 0;
|
||||||
|
if (Math.abs(ew - ed) / Math.max(ew, ed) < 0.1) return 0; // square: nothing to align
|
||||||
|
if (Math.abs(fw - fd) / Math.max(fw, fd) < 0.1) return 0; // slot is square: either way fits
|
||||||
|
return (ew > ed) === (fw > fd) ? 0 : Math.PI / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load + seat one GLB inside a fitting group. Shared by the whole-fitting swap and the multi-part booth.
|
||||||
|
// `yaw` is decided by the caller (legacy kinds keep their hand-tuned KIND_TO_YAW; kit ids get autoYaw).
|
||||||
|
// `at` (optional) = { x, y, z } local placement — the booth's bench-top slots. Without it the GLB takes the
|
||||||
|
// fitting's own origin and is clamped to the footprint the occupancy grid reserved.
|
||||||
|
const refOf = (entry, manifest) => (manifest.localBase ? `${manifest.localBase}${entry.file}` : `depot:${entry.file}`);
|
||||||
|
|
||||||
|
function seatGLB(ctx, fitting, entry, manifest, kind, yaw, at) {
|
||||||
|
return loadGLB(refOf(entry, manifest)).then(gltf => placeGLB(ctx, fitting, gltf, kind, yaw, at));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instantiate a loaded gltf into the fitting group. SPLIT OUT FROM THE FETCH ON PURPOSE (R41): the
|
||||||
|
// multi-part booths must attach their parts in DECLARATION order, not in whichever order the network
|
||||||
|
// finished — see upgradeFitting.
|
||||||
|
function placeGLB(ctx, fitting, gltf, kind, yaw, at) {
|
||||||
|
if (!gltf || ctx._disposed || !fitting.group.parent) return null;
|
||||||
|
const THREE = ctx.THREE;
|
||||||
|
const inst = skeletonClone(gltf.scene); // clone-safe even if skinned
|
||||||
|
inst.rotation.y = yaw || 0; // face before measuring
|
||||||
|
// Scale: GLBs are authored in metres (glb_law), so keep REAL-WORLD scale — only shrink if the prop
|
||||||
|
// would overflow the footprint the occupancy grid reserved (never inflate a small prop to fill a slot).
|
||||||
|
// A bench-top part is never clamped: it is already smaller than the bench by construction.
|
||||||
|
const bb = new THREE.Box3().setFromObject(inst);
|
||||||
|
const size = new THREE.Vector3(); bb.getSize(size);
|
||||||
|
const glbW = Math.max(size.x, size.z) || 1;
|
||||||
|
const s = at ? 1 : Math.min(1, Math.max(fitting.footprint.w, fitting.footprint.d) / glbW);
|
||||||
|
inst.scale.setScalar(s);
|
||||||
|
inst.position.set(at ? at.x : 0, (at ? at.y : 0) - bb.min.y * s, at ? at.z : 0); // plant on the surface
|
||||||
|
inst.userData = { glbUpgrade: true, kind };
|
||||||
|
fitting.group.add(inst);
|
||||||
|
return inst;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upgrade one placed fitting in-place, if the manifest maps its kind (or the fitting names its own id).
|
||||||
|
// No-op when disabled/unavailable. `manifest` is the resolved object (or null). ctx is the room build
|
||||||
|
// context (for the _disposed guard). Returns a promise that settles when the swap is done (or immediately
|
||||||
|
// when there is nothing to do), so callers can await a room's GLB upgrades (room.glbReady).
|
||||||
// Resolution: production uses `depot:<file>` (loaders.js → depot CDN / local-depot). A manifest may set
|
// Resolution: production uses `depot:<file>` (loaders.js → depot CDN / local-depot). A manifest may set
|
||||||
// `localBase` (e.g. "assets/_local_glb/") to load straight from a served local dir — used for offline
|
// `localBase` (e.g. "assets/_local_glb/") to load straight from a served local dir — used for offline
|
||||||
// GLB validation before the depot is reachable.
|
// GLB validation before the depot is reachable.
|
||||||
export function upgradeFitting(ctx, fitting, kind, manifest) {
|
export function upgradeFitting(ctx, fitting, kind, manifest) {
|
||||||
if (!manifest || !manifest.fittings) return Promise.resolve();
|
if (!manifest || !manifest.fittings) return Promise.resolve();
|
||||||
const id = KIND_TO_GLB[kind];
|
|
||||||
|
// [R41] MULTI-PART fittings (the DJ booths): several GLBs standing on one primitive bench. The bench
|
||||||
|
// frame is hidden exactly as a single swap hides it, so the booth reads as gear-on-a-desk either way.
|
||||||
|
if (fitting.glbParts && fitting.glbParts.length) {
|
||||||
|
const parts = fitting.glbParts
|
||||||
|
.map(p => ({ p, e: manifest.fittings[p.id] }))
|
||||||
|
.filter(o => o.e && o.e.file);
|
||||||
|
if (!parts.length) return Promise.resolve();
|
||||||
|
// FETCH IN PARALLEL, ATTACH IN DECLARATION ORDER. Attaching inside each load's own `.then` made the
|
||||||
|
// booth's children land in whichever order the network finished, which is not a function of the seed:
|
||||||
|
// two runs of the same 240 rooms differed on exactly the three types that have a booth (record, pawn,
|
||||||
|
// pub), and on nothing else. Same objects, same transforms, different child order — enough to break
|
||||||
|
// "byte-equal across two builds", which is the law this lane is held to. Load is concurrent; the
|
||||||
|
// second pass is a plain in-order loop, so it is deterministic by construction, not by luck.
|
||||||
|
return Promise.all(parts.map(o => loadGLB(refOf(o.e, manifest)))).then(gltfs => {
|
||||||
|
if (ctx._disposed) return;
|
||||||
|
let any = false;
|
||||||
|
for (let i = 0; i < parts.length; i++)
|
||||||
|
if (placeGLB(ctx, fitting, gltfs[i], parts[i].p.id, parts[i].p.ry || 0, parts[i].p)) any = true;
|
||||||
|
// Hide the GEAR stand-ins only — the bench they stand on IS the fitting and has no GLB of its own.
|
||||||
|
if (any) for (const c of fitting.group.children) if (c.userData && c.userData.boothGear) c.visible = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// [R41] a fitting may name its OWN manifest id (`fitting.glbId`, chosen on its seeded stream from a
|
||||||
|
// kit41 POOL). That is what turns one primitive builder into 110 distinct shop fittings. The legacy
|
||||||
|
// per-kind map is the fallback for the pre-R41 fittings, which name no id.
|
||||||
|
const id = fitting.glbId || KIND_TO_GLB[kind];
|
||||||
const entry = id && manifest.fittings[id];
|
const entry = id && manifest.fittings[id];
|
||||||
if (!entry || !entry.file) return Promise.resolve();
|
if (!entry || !entry.file) return Promise.resolve();
|
||||||
|
|
||||||
const ref = manifest.localBase ? `${manifest.localBase}${entry.file}` : `depot:${entry.file}`;
|
// Facing: a pre-R41 kind keeps its hand-tuned yaw EXACTLY (KIND_TO_YAW[kind] || 0 — the R4/R6 on-screen
|
||||||
return loadGLB(ref).then(gltf => {
|
// validation); only a kit id, which has no tuned row and could be either half of a rotated pair, is
|
||||||
if (!gltf || ctx._disposed || !fitting.group.parent) return; // room rebuilt / disposed mid-flight
|
// auto-faced. Legacy rooms are therefore byte-identical on screen.
|
||||||
const THREE = ctx.THREE;
|
const yaw = fitting.glbId ? autoYaw(entry, fitting) : (KIND_TO_YAW[kind] || 0);
|
||||||
const inst = skeletonClone(gltf.scene); // clone-safe even if skinned
|
return seatGLB(ctx, fitting, entry, manifest, fitting.glbId ? id : kind, yaw).then(inst => {
|
||||||
// Facing: rotate the directional prop to match the primitive the layout placed (before measuring).
|
if (inst) hidePrimitive(fitting);
|
||||||
inst.rotation.y = KIND_TO_YAW[kind] || 0;
|
|
||||||
// Scale: GLBs are authored in metres (glb_law), so keep REAL-WORLD scale — only shrink if the prop
|
|
||||||
// would overflow the footprint the occupancy grid reserved (never inflate a small prop to fill a slot).
|
|
||||||
const bb = new THREE.Box3().setFromObject(inst);
|
|
||||||
const size = new THREE.Vector3(); bb.getSize(size);
|
|
||||||
const want = fitting.footprint;
|
|
||||||
const glbW = Math.max(size.x, size.z) || 1;
|
|
||||||
const s = Math.min(1, Math.max(want.w, want.d) / glbW);
|
|
||||||
inst.scale.setScalar(s);
|
|
||||||
inst.position.y -= bb.min.y * s; // plant on the floor (glb_law minY≈0)
|
|
||||||
// hide the primitive FRAME (fixture) but keep the procedural STOCK visible on top of the detailed
|
|
||||||
// GLB shelf/counter. Post-batch (batch.js) the children are merged meshes tagged isStock / isFrame;
|
|
||||||
// pre-batch they're individual meshes (stock tagged isStock, frame untagged). Hide everything that
|
|
||||||
// isn't stock and isn't the GLB we're adding.
|
|
||||||
for (const c of fitting.group.children) {
|
|
||||||
if (c.userData && (c.userData.isStock || c.userData.glbUpgrade)) continue;
|
|
||||||
c.visible = false;
|
|
||||||
}
|
|
||||||
inst.userData = { glbUpgrade: true, kind };
|
|
||||||
fitting.group.add(inst);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the primitive FRAME (fixture) but keep the procedural STOCK visible on top of the detailed GLB
|
||||||
|
// shelf/counter. Post-batch (batch.js) the children are merged meshes tagged isStock / isFrame; pre-batch
|
||||||
|
// they're individual meshes (stock tagged isStock, frame untagged). Hide everything that isn't stock and
|
||||||
|
// isn't a GLB we added.
|
||||||
|
//
|
||||||
|
// [R41 §41.4 — A PRE-EXISTING BUG THIS ROUND WALKED INTO] `buyMesh` was missing from that list. R9's
|
||||||
|
// buy-anywhere shelves (stockpack.buildBuyableShelf) are merged, per-item-addressable meshes tagged
|
||||||
|
// `{ noBatch, buyMesh }` and NOT `isStock` — because batch.js already skips them on `noBatch` — so on any
|
||||||
|
// fitting whose kind has a GLB (bookshelf, cubeShelf, clothesRack) the swap turned every real,
|
||||||
|
// buyable item in the room INVISIBLE. It never showed up because `?stock=real` is opt-in and R9 measured
|
||||||
|
// it with GLBs off; R41 found it the moment 52 real garments went on a clothes rail and the rail came
|
||||||
|
// back empty. Fixed here rather than by tagging them isStock, because the tag is what tells batch.js
|
||||||
|
// how to treat a mesh and these must stay unbatched.
|
||||||
|
function hidePrimitive(fitting) {
|
||||||
|
for (const c of fitting.group.children) {
|
||||||
|
if (c.userData && (c.userData.isStock || c.userData.buyMesh || c.userData.glbUpgrade)) continue;
|
||||||
|
c.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Place counter-top GLB props (cash_register, milkshake_mixer) onto a counter fitting's benchtop.
|
// Place counter-top GLB props (cash_register, milkshake_mixer) onto a counter fitting's benchtop.
|
||||||
// Additive: does NOT hide the counter primitive frame (upgradeFitting does that when counter is GLB-live).
|
// Additive: does NOT hide the counter primitive frame (upgradeFitting does that when counter is GLB-live).
|
||||||
// `counterHasGlb`: when the counter itself upgrades to counter_till (which carries its OWN modelled till),
|
// `counterHasGlb`: when the counter itself upgrades to counter_till (which carries its OWN modelled till),
|
||||||
|
|||||||
@ -200,7 +200,9 @@ export function buildInterior(shop, THREE, opts) {
|
|||||||
// [R39] opts.fittingOpts = { <fittingKind>: {…} } — per-build overrides of a recipe's own fitting
|
// [R39] opts.fittingOpts = { <fittingKind>: {…} } — per-build overrides of a recipe's own fitting
|
||||||
// opts (`spec.o`). Pure data, no stream reads; omitted ⇒ every recipe's declared defaults, i.e.
|
// opts (`spec.o`). Pure data, no stream reads; omitted ⇒ every recipe's declared defaults, i.e.
|
||||||
// byte-identical to pre-R39. The one live use is `{ rummageBin: { dig: true } }` — see §39.
|
// byte-identical to pre-R39. The one live use is `{ rummageBin: { dig: true } }` — see §39.
|
||||||
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm, fittingOpts: opts.fittingOpts });
|
// [R41] opts.noKit — the control arm for §41.4's 110 new fittings (see layout.js). Diagnostics only.
|
||||||
|
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm, fittingOpts: opts.fittingOpts,
|
||||||
|
noKit: opts.noKit });
|
||||||
|
|
||||||
// DRAW BATCHING (round 6, ≤350 draws/room law): merge the many same-material stock/fixture meshes
|
// DRAW BATCHING (round 6, ≤350 draws/room law): merge the many same-material stock/fixture meshes
|
||||||
// inside each fitting group into a few merged geometries. Fitting groups (and their userData) are
|
// inside each fitting group into a few merged geometries. Fitting groups (and their userData) are
|
||||||
@ -242,6 +244,8 @@ export function buildInterior(shop, THREE, opts) {
|
|||||||
placement: lay.placement,
|
placement: lay.placement,
|
||||||
pathOK: lay.pathOK,
|
pathOK: lay.pathOK,
|
||||||
carved: lay.carved,
|
carved: lay.carved,
|
||||||
|
pulls: lay.pulls, // [R41] { pulled, restored, lost } — what the door→counter path
|
||||||
|
// guarantee cost this room. lost > 0 means a real blocker.
|
||||||
glbReady, // Promise — resolves when opts.useGLB hero-prop swaps have settled
|
glbReady, // Promise — resolves when opts.useGLB hero-prop swaps have settled
|
||||||
buildMs,
|
buildMs,
|
||||||
_debug: { grid: lay.grid, spawnCell: lay.spawnCell, targetCell: lay.targetCell, ctx },
|
_debug: { grid: lay.grid, spawnCell: lay.spawnCell, targetCell: lay.targetCell, ctx },
|
||||||
|
|||||||
290
web/js/interiors/kit41.js
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
@ -101,7 +101,9 @@ function placeCounter(ctx, recipe, W, D, r) {
|
|||||||
} else { // 'corner' — back-east, facing into the room
|
} else { // 'corner' — back-east, facing into the room
|
||||||
x = W / 2 - 0.55; z = -D / 4; ry = -Math.PI / 2; front = { x: x - 0.95, z };
|
x = W / 2 - 0.55; z = -D / 4; ry = -Math.PI / 2; front = { x: x - 0.95, z };
|
||||||
}
|
}
|
||||||
const f = buildFitting('counter', ctx, { w: cw }, r);
|
// [R41] `recipe.counterGlb` overrides the counter's GLB swap for one type (the venues' `plywood_bar`).
|
||||||
|
// Pure data — it reads no stream, so a recipe without it is byte-identical to pre-R41.
|
||||||
|
const f = buildFitting('counter', ctx, { w: cw, glbId: recipe.counterGlb }, r);
|
||||||
f.group.position.set(x, 0, z); f.group.rotation.y = ry;
|
f.group.position.set(x, 0, z); f.group.rotation.y = ry;
|
||||||
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
||||||
const stand = keeperStand(x, z, front, W, D);
|
const stand = keeperStand(x, z, front, W, D);
|
||||||
@ -315,11 +317,23 @@ function makeWallSlots(W, D, r) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── free-standing candidate positions per zone ────────────────────────────────────────
|
// ── free-standing candidate positions per zone ────────────────────────────────────────
|
||||||
function zoneCandidates(zone, W, D, doorCx, r) {
|
function zoneCandidates(zone, W, D, doorCx, r, stage) {
|
||||||
const out = [];
|
const out = [];
|
||||||
if (zone === 'window') {
|
if (zone === 'window') {
|
||||||
const z = D / 2 - 1.4;
|
const z = D / 2 - 1.4;
|
||||||
for (let x = -W / 2 + 1.1; x <= doorCx - 1.2; x += 1.3) out.push({ x, z, ry: Math.PI });
|
for (let x = -W / 2 + 1.1; x <= doorCx - 1.2; x += 1.3) out.push({ x, z, ry: Math.PI });
|
||||||
|
} else if (zone === 'stage') {
|
||||||
|
// [R41 §41.4] THE STAGE FLANKS — venue-only. Before this, a PA stack or a crowd barrier drawn from the
|
||||||
|
// venue pool went into `aisle`, whose candidate grid spans the whole room, so the pub's gear landed
|
||||||
|
// wherever there was space: a 1.97 m crowd barrier ended up across the doorway. Gear belongs BESIDE
|
||||||
|
// the stage. Deliberately NOT in front of it: the strip from `stage.frontZ + 0.6` to `D/2 − 1.6` is
|
||||||
|
// where placeWatchPoints stands the crowd, and furniture there is furniture instead of an audience.
|
||||||
|
// No stage (a non-venue recipe that asks for this zone) ⇒ no candidates ⇒ the spec's fallbacks run.
|
||||||
|
if (!stage) return out;
|
||||||
|
const fx = Math.min(W / 2 - 0.7, stage.w / 2 + 0.75);
|
||||||
|
for (const sx of [-1, 1])
|
||||||
|
for (let z = stage.z - stage.d / 2; z <= stage.frontZ + 0.9; z += 0.8)
|
||||||
|
out.push({ x: sx * fx, z, ry: sx > 0 ? -Math.PI / 2 : Math.PI / 2 });
|
||||||
} else if (zone === 'aisle') {
|
} else if (zone === 'aisle') {
|
||||||
for (let z = D / 2 - 3.0; z >= -D / 2 + 1.6; z -= 1.7)
|
for (let z = D / 2 - 3.0; z >= -D / 2 + 1.6; z -= 1.7)
|
||||||
for (let x = -W / 2 + 1.3; x <= W / 2 - 1.3; x += 1.9) out.push({ x, z, ry: 0 });
|
for (let x = -W / 2 + 1.3; x <= W / 2 - 1.3; x += 1.9) out.push({ x, z, ry: 0 });
|
||||||
@ -331,7 +345,7 @@ function zoneCandidates(zone, W, D, doorCx, r) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── main entry ────────────────────────────────────────────────────────────────────────
|
// ── main entry ────────────────────────────────────────────────────────────────────────
|
||||||
export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts }) {
|
export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts, noKit }) {
|
||||||
const { W, D } = dims;
|
const { W, D } = dims;
|
||||||
const doorCx = shell.door.x;
|
const doorCx = shell.door.x;
|
||||||
const grid = makeGrid(W, D);
|
const grid = makeGrid(W, D);
|
||||||
@ -367,7 +381,13 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
// 2) FITTINGS from the recipe mix (skip the counter entry — already placed).
|
// 2) FITTINGS from the recipe mix (skip the counter entry — already placed).
|
||||||
const area = W * D;
|
const area = W * D;
|
||||||
const wallSlots = makeWallSlots(W, D, rWall);
|
const wallSlots = makeWallSlots(W, D, rWall);
|
||||||
for (const spec of recipe.fittings) {
|
// [R41 §41.4] `opts.noKit` is the KIT'S OWN CONTROL ARM, and it is exact rather than approximate: every
|
||||||
|
// R41 spec is appended LAST in its recipe, so dropping them here leaves each surviving spec the same
|
||||||
|
// draws off the same `lay-count`/`lay-place` streams in the same order — a `noKit` room is byte-identical
|
||||||
|
// to the pre-R41 room, which is what makes "the kit cost N draws" a measurement instead of a claim.
|
||||||
|
// Diagnostics only (like opts.noBatch); nothing in the game sets it.
|
||||||
|
const specs = noKit ? recipe.fittings.filter(s => !isKitSpec(s)) : recipe.fittings;
|
||||||
|
for (const spec of specs) {
|
||||||
if (spec.kind === 'counter') continue;
|
if (spec.kind === 'counter') continue;
|
||||||
// count scales with clutter × area, clamped to [min,max]
|
// count scales with clutter × area, clamped to [min,max]
|
||||||
const density = recipe.clutter * (area / 70);
|
const density = recipe.clutter * (area / 70);
|
||||||
@ -384,9 +404,9 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
if (spec.zone === 'wall' || f.wallMounted) {
|
if (spec.zone === 'wall' || f.wallMounted) {
|
||||||
ok = placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec);
|
ok = placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec);
|
||||||
} else if (spec.zone === 'counter') {
|
} else if (spec.zone === 'counter') {
|
||||||
ok = placeNearCounter(ctx, grid, f, counter, rPlace, placed, spec, W, D, doorCx);
|
ok = placeNearCounter(ctx, grid, f, counter, rPlace, placed, spec, W, D, doorCx, stageInfo);
|
||||||
} else {
|
} else {
|
||||||
ok = placeFree(ctx, grid, f, spec.zone, W, D, doorCx, rPlace, placed, spec);
|
ok = placeFree(ctx, grid, f, spec.zone, W, D, doorCx, rPlace, placed, spec, stageInfo);
|
||||||
}
|
}
|
||||||
// [R39] OPT-IN placement fallbacks, for a fitting a recipe promises will BE there (the op shop's
|
// [R39] OPT-IN placement fallbacks, for a fitting a recipe promises will BE there (the op shop's
|
||||||
// rummage bin). Default is unchanged: no fallback list ⇒ a fitting that can't find its zone is
|
// rummage bin). Default is unchanged: no fallback list ⇒ a fitting that can't find its zone is
|
||||||
@ -395,7 +415,15 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
// rooms. The chain runs on the SAME `rPlace` stream and only ever fires for a spec that opted in
|
// rooms. The chain runs on the SAME `rPlace` stream and only ever fires for a spec that opted in
|
||||||
// AND already failed, so no other fitting's draws move.
|
// AND already failed, so no other fitting's draws move.
|
||||||
if (!ok && spec.fallbackZones)
|
if (!ok && spec.fallbackZones)
|
||||||
for (const z of spec.fallbackZones) { ok = placeFree(ctx, grid, f, z, W, D, doorCx, rPlace, placed, spec); if (ok) break; }
|
for (const z of spec.fallbackZones) {
|
||||||
|
// [R41] 'wall' in a fallback chain has to route to placeAtWall — the wall is a SLOT POOL, not a
|
||||||
|
// candidate grid, so `zoneCandidates('wall')` returns [] and a 'wall' fallback was a silent
|
||||||
|
// no-op. No pre-R41 spec lists it, so this only ever adds a recovery that used to be missing.
|
||||||
|
ok = z === 'wall'
|
||||||
|
? placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec)
|
||||||
|
: placeFree(ctx, grid, f, z, W, D, doorCx, rPlace, placed, spec, stageInfo);
|
||||||
|
if (ok) break;
|
||||||
|
}
|
||||||
if (!ok && spec.anywhere) ok = placeAnywhere(ctx, grid, f, W, D, rPlace, placed, spec);
|
if (!ok && spec.anywhere) ok = placeAnywhere(ctx, grid, f, W, D, rPlace, placed, spec);
|
||||||
if (!ok) { /* no room — density self-caps; drop the fitting (its resources free at room end) */ }
|
if (!ok) { /* no room — density self-caps; drop the fitting (its resources free at room end) */ }
|
||||||
}
|
}
|
||||||
@ -409,18 +437,46 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
const removable = () => placed
|
const removable = () => placed
|
||||||
.filter(p => p.priority < 8 && p.kind !== 'counter' && !p.noStamp) // wall-mounted don't block the floor
|
.filter(p => p.priority < 8 && p.kind !== 'counter' && !p.noStamp) // wall-mounted don't block the floor
|
||||||
.sort((a, b) => a.priority - b.priority);
|
.sort((a, b) => a.priority - b.priority);
|
||||||
|
const pulled = [];
|
||||||
while (!pathOK) {
|
while (!pathOK) {
|
||||||
const pool = removable();
|
const pool = removable();
|
||||||
if (!pool.length) break;
|
if (!pool.length) break;
|
||||||
const victim = pool[0];
|
const victim = pool[0];
|
||||||
placed.splice(placed.indexOf(victim), 1);
|
const at = placed.indexOf(victim);
|
||||||
|
placed.splice(at, 1);
|
||||||
if (victim.fitting.group.parent) victim.fitting.group.parent.remove(victim.fitting.group);
|
if (victim.fitting.group.parent) victim.fitting.group.parent.remove(victim.fitting.group);
|
||||||
victim.removed = true;
|
victim.removed = true;
|
||||||
|
pulled.push({ v: victim, at });
|
||||||
rebuildOcc(grid, placed);
|
rebuildOcc(grid, placed);
|
||||||
pathOK = reaches(grid, spawnCell, targetCell);
|
pathOK = reaches(grid, spawnCell, targetCell);
|
||||||
}
|
}
|
||||||
if (!pathOK) { carveCorridor(grid, spawnCell, targetCell); carved = true; pathOK = reaches(grid, spawnCell, targetCell); }
|
if (!pathOK) { carveCorridor(grid, spawnCell, targetCell); carved = true; pathOK = reaches(grid, spawnCell, targetCell); }
|
||||||
|
|
||||||
|
// 3b) [R41 §41.4] PUT BACK EVERYTHING THAT WASN'T THE BLOCKER.
|
||||||
|
// The pull loop above takes the LOWEST-PRIORITY survivor each pass, not the one standing in the
|
||||||
|
// corridor — so it clears the cheap furniture first and only reaches the actual blocker by attrition.
|
||||||
|
// Measured: `pub/wide/4242` came back with a bar, a stage and NOTHING ELSE — no tables, no PA, no R41
|
||||||
|
// kit — because a wide pub puts the bar beside the stage and leaves one ~1 m gap to squeeze through,
|
||||||
|
// and restoring that gap cost every innocent fitting in the room. That was already true before this
|
||||||
|
// round; ten more fittings per pub just made it impossible to miss.
|
||||||
|
// The fix is additive and self-checking: walk the pulled list in reverse, put each fitting back at its
|
||||||
|
// original index, and keep it ONLY if door→counter still reaches. A room that pulled nothing does
|
||||||
|
// nothing here (byte-identical); a room that pulled N can only end with MORE fittings than before,
|
||||||
|
// never fewer, and the path guarantee is re-proved on every single restore rather than assumed.
|
||||||
|
let restored = 0;
|
||||||
|
if (pathOK && pulled.length) {
|
||||||
|
for (let i = pulled.length - 1; i >= 0; i--) {
|
||||||
|
const { v, at } = pulled[i];
|
||||||
|
placed.splice(Math.min(at, placed.length), 0, v);
|
||||||
|
v.removed = false;
|
||||||
|
rebuildOcc(grid, placed);
|
||||||
|
if (reaches(grid, spawnCell, targetCell)) { restored++; continue; }
|
||||||
|
placed.splice(placed.indexOf(v), 1);
|
||||||
|
v.removed = true;
|
||||||
|
rebuildOcc(grid, placed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 4) add SURVIVORS to the room + fill them with visual stock. Collect `places` here (NOT at
|
// 4) add SURVIVORS to the room + fill them with visual stock. Collect `places` here (NOT at
|
||||||
// placement time) so interactables belong only to fittings that actually made it into the room —
|
// placement time) so interactables belong only to fittings that actually made it into the room —
|
||||||
// a fitting pulled by the path loop must not leave a phantom interactable in the returned list.
|
// a fitting pulled by the path loop must not leave a phantom interactable in the returned list.
|
||||||
@ -453,6 +509,7 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
return {
|
return {
|
||||||
placed: placed.filter(p => !p.removed),
|
placed: placed.filter(p => !p.removed),
|
||||||
places, decor: decor.meshes, grid, pathOK, carved,
|
places, decor: decor.meshes, grid, pathOK, carved,
|
||||||
|
pulls: { pulled: pulled.length, restored, lost: pulled.length - restored }, // [R41] path-loop cost
|
||||||
spawnCell, targetCell, placement, browsePoints,
|
spawnCell, targetCell, placement, browsePoints,
|
||||||
watchPoints, // venue: audience poses {x,z,ry,dance} facing the stage, cap by kind (pub/band_room 8, rsl 12); else []
|
watchPoints, // venue: audience poses {x,z,ry,dance} facing the stage, cap by kind (pub/band_room 8, rsl 12); else []
|
||||||
stage: stageInfo, // venue: { x,z,w,d,deckY,frontZ,bandPoses } (else null)
|
stage: stageInfo, // venue: { x,z,w,d,deckY,frontZ,bandPoses } (else null)
|
||||||
@ -465,8 +522,8 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts })
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── placement strategies ──────────────────────────────────────────────────────────────
|
// ── placement strategies ──────────────────────────────────────────────────────────────
|
||||||
function placeFree(ctx, grid, f, zone, W, D, doorCx, r, placed, spec) {
|
function placeFree(ctx, grid, f, zone, W, D, doorCx, r, placed, spec, stage) {
|
||||||
const cands = zoneCandidates(zone, W, D, doorCx, r);
|
const cands = zoneCandidates(zone, W, D, doorCx, r, stage);
|
||||||
for (const c of cands) {
|
for (const c of cands) {
|
||||||
const ry = c.ry + (r() - 0.5) * 0.4;
|
const ry = c.ry + (r() - 0.5) * 0.4;
|
||||||
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
||||||
@ -526,13 +583,19 @@ function placeAtWall(ctx, grid, f, wallSlots, r, placed, spec) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeNearCounter(ctx, grid, f, counter, r, placed, spec, W, D, doorCx) {
|
function placeNearCounter(ctx, grid, f, counter, r, placed, spec, W, D, doorCx, stage) {
|
||||||
// cases beside the till, on the goods side of the counter — try BOTH sides + a couple offsets, then
|
// cases beside the till, on the goods side of the counter — try BOTH sides + a couple offsets, then
|
||||||
// fall back to a free-standing aisle spot so a min≥1 fitting (e.g. a glass case) reliably lands.
|
// fall back to a free-standing aisle spot so a min≥1 fitting (e.g. a glass case) reliably lands.
|
||||||
const ry = counter.ry;
|
const ry = counter.ry;
|
||||||
|
// [R41] `spec.gap` — how far along the bench to stand the neighbour. The hardcoded 1.4 m is the gap a
|
||||||
|
// glass case needs and is KEPT as the default, so every pre-R41 spec places byte-identically. It is far
|
||||||
|
// too small for a 1.55 m DJ booth beside a 2.2 m counter: their padded rects overlap by ~0.8 m, every
|
||||||
|
// candidate fails, and the record shop's hero prop landed in 1 room out of 15 (measured). An opt-in
|
||||||
|
// number, not a retuned constant — the fitting that needs more room asks for more room.
|
||||||
|
const gap = spec.gap || 1.4;
|
||||||
const cands = counter.ry === 0
|
const cands = counter.ry === 0
|
||||||
? [{ x: counter.x - 1.4, z: counter.z }, { x: counter.x + 1.4, z: counter.z }, { x: counter.x - 1.4, z: counter.z - 0.7 }, { x: counter.x + 1.4, z: counter.z - 0.7 }]
|
? [{ x: counter.x - gap, z: counter.z }, { x: counter.x + gap, z: counter.z }, { x: counter.x - gap, z: counter.z - 0.7 }, { x: counter.x + gap, z: counter.z - 0.7 }]
|
||||||
: [{ x: counter.x, z: counter.z - 1.4 }, { x: counter.x, z: counter.z + 1.4 }, { x: counter.x - 0.7, z: counter.z - 1.4 }, { x: counter.x - 0.7, z: counter.z + 1.4 }];
|
: [{ x: counter.x, z: counter.z - gap }, { x: counter.x, z: counter.z + gap }, { x: counter.x - 0.7, z: counter.z - gap }, { x: counter.x - 0.7, z: counter.z + gap }];
|
||||||
shuffle(r, cands);
|
shuffle(r, cands);
|
||||||
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
||||||
for (const side of cands) {
|
for (const side of cands) {
|
||||||
@ -544,7 +607,7 @@ function placeNearCounter(ctx, grid, f, counter, r, placed, spec, W, D, doorCx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// counter is boxed in — put it in the aisle instead (still front-of-room-ish)
|
// counter is boxed in — put it in the aisle instead (still front-of-room-ish)
|
||||||
return placeFree(ctx, grid, f, 'aisle', W, D, doorCx, r, placed, spec);
|
return placeFree(ctx, grid, f, 'aisle', W, D, doorCx, r, placed, spec, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── wall decor (art + signage) ─────────────────────────────────────────────────────────
|
// ── wall decor (art + signage) ─────────────────────────────────────────────────────────
|
||||||
@ -627,3 +690,5 @@ function carveCorridor(grid, startIdx, targetIdx) {
|
|||||||
clear(tx, tz);
|
clear(tx, tz);
|
||||||
}
|
}
|
||||||
const round = (v) => Math.round(v * 1000) / 1000;
|
const round = (v) => Math.round(v * 1000) / 1000;
|
||||||
|
// The R41 kit's specs, by construction: every one of them is a `kit*` fitting or a `djBooth`.
|
||||||
|
const isKitSpec = (s) => s.kind === 'djBooth' || /^kit[A-Z]/.test(s.kind);
|
||||||
|
|||||||
@ -86,6 +86,13 @@ function garmentPool(ctx, r, n = 8) {
|
|||||||
return ctx._garmentPool;
|
return ctx._garmentPool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Which pack items a slot may draw from. A multi-slot pack (R41's wardrobe) hands the caller an already
|
||||||
|
// filtered `pool`; a single-slot pack (record/book/toy) hands back `pack.items` by reference, and those
|
||||||
|
// keep the pre-R41 behaviour exactly — one seeded atlas bucket, one r() draw, one draw call.
|
||||||
|
function slotItems(pack, src, r) {
|
||||||
|
return (src && src.pool && src.pool !== pack.items) ? src.pool : pack.atlasPool(r);
|
||||||
|
}
|
||||||
|
|
||||||
// ── fill entry point ─────────────────────────────────────────────────────────────────
|
// ── fill entry point ─────────────────────────────────────────────────────────────────
|
||||||
export function fill(ctx, fitting, opts, r) {
|
export function fill(ctx, fitting, opts, r) {
|
||||||
for (const slot of fitting.slots || []) fillSlot(ctx, fitting.group, slot, opts, r);
|
for (const slot of fitting.slots || []) fillSlot(ctx, fitting.group, slot, opts, r);
|
||||||
@ -118,12 +125,13 @@ function binFan(ctx, parent, slot, opts, r) {
|
|||||||
const step = run / Math.max(1, n);
|
const step = run / Math.max(1, n);
|
||||||
const src = opts.adapter && opts.adapter(opts.shop, 'sleeve');
|
const src = opts.adapter && opts.adapter(opts.shop, 'sleeve');
|
||||||
const pack = src && src.real ? src.pack : null;
|
const pack = src && src.real ? src.pack : null;
|
||||||
|
const packItems = pack ? (src.pool || pack.items) : null; // [R41] slot-filtered when the pack has slots
|
||||||
const pool = pack ? null : facePool(ctx, 'card', r);
|
const pool = pack ? null : facePool(ctx, 'card', r);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const z = -run / 2 + (i + 0.5) * step;
|
const z = -run / 2 + (i + 0.5) * step;
|
||||||
let sl;
|
let sl;
|
||||||
if (pack) {
|
if (pack) {
|
||||||
sl = realSleeveMesh(ctx, pack, pick(r, pack.items), fw, h); // isStock + shared atlas material
|
sl = realSleeveMesh(ctx, pack, pick(r, packItems), fw, h); // isStock + shared atlas material
|
||||||
sl.position.set(slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z);
|
sl.position.set(slot.x + (r() - 0.5) * 0.02, slot.y + h / 2, slot.z + z);
|
||||||
parent.add(sl);
|
parent.add(sl);
|
||||||
} else {
|
} else {
|
||||||
@ -174,7 +182,7 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
|||||||
if (kind === 'spine') { // books / VHS: thin spines edge-out
|
if (kind === 'spine') { // books / VHS: thin spines edge-out
|
||||||
const step = (run - 0.04) / n;
|
const step = (run - 0.04) / n;
|
||||||
if (pack) { // real book spines → one buyable, addressable mesh
|
if (pack) { // real book spines → one buyable, addressable mesh
|
||||||
const poolItems = pack.atlasPool(r); // single atlas → the whole slot merges to one draw
|
const poolItems = slotItems(pack, src, r); // single atlas → the whole slot merges to one draw
|
||||||
const pls = [];
|
const pls = [];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const bh = clear * (0.8 + r() * 0.2);
|
const bh = clear * (0.8 + r() * 0.2);
|
||||||
@ -213,12 +221,16 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
|||||||
const bw = Math.min((run - 0.02) / n, kind === 'snack' ? 0.12 : 0.28);
|
const bw = Math.min((run - 0.02) / n, kind === 'snack' ? 0.12 : 0.28);
|
||||||
const step = run / n;
|
const step = run / n;
|
||||||
if (pack) { // real toy boxes → one buyable, addressable mesh
|
if (pack) { // real toy boxes → one buyable, addressable mesh
|
||||||
const poolItems = pack.atlasPool(r); // single atlas → the whole slot merges to one draw
|
const poolItems = slotItems(pack, src, r); // single atlas → the whole slot merges to one draw
|
||||||
const pls = [];
|
const pls = [];
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const bh = clear * (kind === 'snack' ? 0.6 : 0.62 + r() * 0.28);
|
const bh = clear * (kind === 'snack' ? 0.6 : 0.62 + r() * 0.28);
|
||||||
const x = slot.x - run / 2 + (i + 0.5) * step;
|
const x = slot.x - run / 2 + (i + 0.5) * step;
|
||||||
pls.push({ item: pick(r, poolItems), x, y: slot.y + bh / 2, z: slot.z + depth / 2 - 0.01, w: bw * 0.88, h: bh });
|
const it = pick(r, poolItems);
|
||||||
|
// [R41] a cut-out item (a bucket hat, a pair of thongs) takes its OWN width from its atlas cell;
|
||||||
|
// a cover-art item has no `aspect` and keeps the fixed carton width it always had.
|
||||||
|
const iw = it.aspect ? Math.min(bw * 0.98, bh * it.aspect) : bw * 0.88;
|
||||||
|
pls.push({ item: it, x, y: slot.y + bh / 2, z: slot.z + depth / 2 - 0.01, w: iw, h: bh });
|
||||||
}
|
}
|
||||||
const meshes = buildBuyableShelf(ctx, pack, pls);
|
const meshes = buildBuyableShelf(ctx, pack, pls);
|
||||||
if (meshes.length) { for (const m of meshes) parent.add(m); return; } // else fall through, fail-soft
|
if (meshes.length) { for (const m of meshes) parent.add(m); return; } // else fall through, fail-soft
|
||||||
@ -235,8 +247,30 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Garments hung along a rail.
|
// Garments hung along a rail.
|
||||||
|
//
|
||||||
|
// [R41 §41.4 — ROUTE R5, THE WARDROBE, ZERO GEOMETRY] With the op-shop pack loaded (`?stock=real`), every
|
||||||
|
// garment on this rail is a real 1990s Australian layer off ONE shared atlas — so a rail of twelve is one
|
||||||
|
// draw, exactly like the record bins, and the op shop's clothes finally have names the buy card can show.
|
||||||
|
// The garment's WIDTH comes from its own atlas cell aspect, not a constant: a pair of phat pants is wider
|
||||||
|
// than a ribbed singlet, and forcing both into a 0.34 m plane was the tell that gave the old placeholder
|
||||||
|
// away. No pack ⇒ the pooled procedural canvas, unchanged, byte-for-byte (this is the ?noassets path).
|
||||||
function hang(ctx, parent, slot, opts, r) {
|
function hang(ctx, parent, slot, opts, r) {
|
||||||
const n = slot.count || 10, run = slot.run || 1.4, h = slot.height || 0.52;
|
const n = slot.count || 10, run = slot.run || 1.4, h = slot.height || 0.52;
|
||||||
|
const src = opts.adapter && opts.adapter(opts.shop, 'garment');
|
||||||
|
const pack = src && src.real ? src.pack : null;
|
||||||
|
if (pack) {
|
||||||
|
const items = src.pool || pack.items;
|
||||||
|
const pls = [];
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const it = pick(r, items);
|
||||||
|
const gh = h * (0.9 + r() * 0.2);
|
||||||
|
const gw = gh * (it.aspect || 0.66);
|
||||||
|
pls.push({ item: it, x: slot.x - run / 2 + run * (i / (n - 1 || 1)), y: slot.y - 0.05 - gh / 2,
|
||||||
|
z: slot.z + (r() - 0.5) * 0.03, w: gw, h: gh, ry: (r() - 0.5) * 0.15 });
|
||||||
|
}
|
||||||
|
const meshes = buildBuyableShelf(ctx, pack, pls); // addressable → the shelf buy card reads it
|
||||||
|
if (meshes.length) { for (const m of meshes) parent.add(m); return; } // else fall through, fail-soft
|
||||||
|
}
|
||||||
const pool = garmentPool(ctx, r);
|
const pool = garmentPool(ctx, r);
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const gx = slot.x - run / 2 + (run) * (i / (n - 1 || 1));
|
const gx = slot.x - run / 2 + (run) * (i / (n - 1 || 1));
|
||||||
|
|||||||
@ -52,12 +52,21 @@ export function getStockPack(type, base = DEFAULT_STOCK_BASE) { return _resolved
|
|||||||
async function buildPack(type, idx, base, THREE) {
|
async function buildPack(type, idx, base, THREE) {
|
||||||
const names = idx.atlases || [];
|
const names = idx.atlases || [];
|
||||||
const materials = {};
|
const materials = {};
|
||||||
|
// [R41 §41.4] `idx.material` — an OPTIONAL per-pack material block. The record/book/toy atlases are
|
||||||
|
// opaque cover art and carry none, so they build exactly the material they always did. The wardrobe
|
||||||
|
// atlas is 52 CUT-OUT garments on transparent ground: without alphaTest every tee is a rectangle, and
|
||||||
|
// without DoubleSide the back of a garment hanging on a rail is a hole. One field, no fork.
|
||||||
|
const mk = idx.material || null;
|
||||||
await Promise.all(names.map(async (name) => {
|
await Promise.all(names.map(async (name) => {
|
||||||
const url = /^depot:|^https?:/.test(name) ? name : base + name;
|
const url = /^depot:|^https?:/.test(name) ? name : base + name;
|
||||||
let tex;
|
let tex;
|
||||||
try { tex = await _texLoader.loadAsync(url); } catch (e) { tex = null; }
|
try { tex = await _texLoader.loadAsync(url); } catch (e) { tex = null; }
|
||||||
if (tex) { tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; }
|
if (tex) { tex.colorSpace = THREE.SRGBColorSpace; tex.anisotropy = 4; }
|
||||||
materials[name] = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.72, map: tex || null });
|
materials[name] = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xffffff, roughness: (mk && mk.roughness) || 0.72, map: tex || null,
|
||||||
|
...(mk && mk.alphaTest ? { transparent: true, alphaTest: mk.alphaTest } : {}),
|
||||||
|
...(mk && mk.side === 'double' ? { side: THREE.DoubleSide } : {}),
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
const first = names[0];
|
const first = names[0];
|
||||||
// Items grouped by atlas — so a buyable shelf can draw all its covers from ONE atlas and merge to a
|
// Items grouped by atlas — so a buyable shelf can draw all its covers from ONE atlas and merge to a
|
||||||
@ -66,11 +75,20 @@ async function buildPack(type, idx, base, THREE) {
|
|||||||
for (const name of names) byAtlas[name] = [];
|
for (const name of names) byAtlas[name] = [];
|
||||||
for (const it of idx.items) (byAtlas[it.atlas] || byAtlas[first] || (byAtlas[first] = [])).push(it);
|
for (const it of idx.items) (byAtlas[it.atlas] || byAtlas[first] || (byAtlas[first] = [])).push(it);
|
||||||
const nonEmpty = names.filter(n => byAtlas[n] && byAtlas[n].length);
|
const nonEmpty = names.filter(n => byAtlas[n] && byAtlas[n].length);
|
||||||
|
// [R41] Items may declare which STOCK SLOT they serve (`it.slot`). The wardrobe pack holds two kinds in
|
||||||
|
// one atlas — garments that hang on a rail and small goods that sit on a shelf — because one atlas is
|
||||||
|
// one material is ONE DRAW for every piece of clothing in the room. Packs whose items carry no `slot`
|
||||||
|
// get a single bucket keyed by the pack's own canonical slot, i.e. exactly their old behaviour.
|
||||||
|
const bySlot = {};
|
||||||
|
for (const it of idx.items) (bySlot[it.slot || SLOT_FOR[type] || 'sleeve'] ||= []).push(it);
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
items: idx.items,
|
items: idx.items,
|
||||||
atlases: nonEmpty.length ? nonEmpty : names,
|
atlases: nonEmpty.length ? nonEmpty : names,
|
||||||
itemsByAtlas: byAtlas,
|
itemsByAtlas: byAtlas,
|
||||||
|
bySlot,
|
||||||
|
// items serving one stock-slot kind (falls back to the whole pack, so a caller never gets nothing)
|
||||||
|
slotPool(slotKind) { const p = bySlot[slotKind]; return p && p.length ? p : idx.items; },
|
||||||
// shared material for an item's atlas (batching groups sleeves by this)
|
// shared material for an item's atlas (batching groups sleeves by this)
|
||||||
material(item) { return materials[item.atlas] || materials[first]; },
|
material(item) { return materials[item.atlas] || materials[first]; },
|
||||||
// items from a seeded single atlas (buyable shelves use this to stay one-mesh/one-draw)
|
// items from a seeded single atlas (buyable shelves use this to stay one-mesh/one-draw)
|
||||||
@ -80,16 +98,24 @@ async function buildPack(type, idx, base, THREE) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Each pack type feeds exactly one stock slot kind so a record shop's pack never lands on a bric-a-brac
|
// Each pack type feeds the stock slot kinds it belongs in, so a record shop's pack never lands on a
|
||||||
// shelf: record→sleeve (bins), book→spine (bookshelves), toy→box (cube shelves).
|
// bric-a-brac shelf: record→sleeve (bins), book→spine (bookshelves), toy→box (cube shelves).
|
||||||
const SLOT_FOR = { record: 'sleeve', book: 'spine', toy: 'box' };
|
// [R41 §41.4] `opshop` serves TWO: garment (the clothes rails) and box (the shelves and bargain tubs) —
|
||||||
|
// the 441-layer wardrobe library is tops/bottoms AND hats/shoes/bags, and an op shop sells both off the
|
||||||
|
// same floor. Declared as an array; the three pre-R41 packs keep their single string.
|
||||||
|
const SLOT_FOR = { record: 'sleeve', book: 'spine', toy: 'box', opshop: ['garment', 'box'] };
|
||||||
|
|
||||||
// The stockAdapter for ?stock=real. Returns the pack POOL for its matching slot kind; consumers
|
// The stockAdapter for ?stock=real. Returns the pack POOL for its matching slot kind; consumers
|
||||||
// (stock.binFan / stock.shelfLine / dig.js) pick a seeded subset. Other kinds → null → procedural.
|
// (stock.binFan / stock.shelfLine / stock.hang / dig.js) pick a seeded subset. Other kinds → null →
|
||||||
|
// procedural. `pool` is the slot-filtered item list; a single-slot pack returns its whole item list, so
|
||||||
|
// the three pre-R41 packs are handed byte-identical data.
|
||||||
export function makeStockAdapter(pack) {
|
export function makeStockAdapter(pack) {
|
||||||
if (!pack || !pack.items || !pack.items.length) return null;
|
if (!pack || !pack.items || !pack.items.length) return null;
|
||||||
const want = SLOT_FOR[pack.type] || 'sleeve';
|
const want = SLOT_FOR[pack.type] || 'sleeve';
|
||||||
return (shop, slotKind) => (slotKind === want ? { real: true, pack } : null);
|
const wants = Array.isArray(want) ? want : [want];
|
||||||
|
return (shop, slotKind) => (wants.includes(slotKind)
|
||||||
|
? { real: true, pack, pool: wants.length > 1 ? pack.slotPool(slotKind) : pack.items }
|
||||||
|
: null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build a leaning cover PLANE for one pack item, UV-remapped to its atlas rect, tagged isStock so
|
// Build a leaning cover PLANE for one pack item, UV-remapped to its atlas rect, tagged isStock so
|
||||||
@ -115,6 +141,7 @@ function itemQuad(THREE, pack, pl) {
|
|||||||
a.setXY(0, u0, v1); a.setXY(1, u1, v1); a.setXY(2, u0, v0); a.setXY(3, u1, v0);
|
a.setXY(0, u0, v1); a.setXY(1, u1, v1); a.setXY(2, u0, v0); a.setXY(3, u1, v0);
|
||||||
if (pl.rz) g.rotateZ(pl.rz);
|
if (pl.rz) g.rotateZ(pl.rz);
|
||||||
if (pl.rx) g.rotateX(pl.rx);
|
if (pl.rx) g.rotateX(pl.rx);
|
||||||
|
if (pl.ry) g.rotateY(pl.ry); // [R41] hanging garments swing a few degrees on the rail
|
||||||
g.translate(pl.x, pl.y, pl.z);
|
g.translate(pl.x, pl.y, pl.z);
|
||||||
return g;
|
return g;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,13 @@
|
|||||||
// stockKind : what stock.js renders into this shop's carriers
|
// stockKind : what stock.js renders into this shop's carriers
|
||||||
// signFlavour : seeded inside signage strings, themed
|
// signFlavour : seeded inside signage strings, themed
|
||||||
|
|
||||||
|
// [R41 §41.4] The kit pools live in kit41.js; recipes name a pool, never an asset id, so a manifest change
|
||||||
|
// is one edit in one file. Every kit spec below is APPENDED LAST inside its recipe on purpose: layout.js
|
||||||
|
// draws counts from `lay-count` and placements from `lay-place` in array order, so a spec added at the END
|
||||||
|
// leaves every pre-existing fitting in that recipe the SAME draws, and therefore the same placement. (The
|
||||||
|
// R39 lesson, applied at ten times the size.)
|
||||||
|
import { POOLS } from './kit41.js';
|
||||||
|
|
||||||
export const ARCH = { cosy: 'cosy', gallery: 'gallery', wide: 'wide', hall: 'hall', pokey: 'pokey' };
|
export const ARCH = { cosy: 'cosy', gallery: 'gallery', wide: 'wide', hall: 'hall', pokey: 'pokey' };
|
||||||
|
|
||||||
const RECIPES = {
|
const RECIPES = {
|
||||||
@ -44,6 +51,22 @@ const RECIPES = {
|
|||||||
{ kind: 'wallShelf', zone: 'wall', min: 1, max: 3, priority: 3 },
|
{ kind: 'wallShelf', zone: 'wall', min: 1, max: 3, priority: 3 },
|
||||||
{ kind: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
|
{ kind: 'crateStack', zone: 'wall', min: 1, max: 2, priority: 2 }, // back-room dressing (crate_stack GLB)
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: the 3dstore shop floor. Real record-shop fittings, not a scatter: bins to flick through in
|
||||||
|
// the aisle, tiered racks behind them, boards on the wall, a window ledge, sleeves pinned up. The
|
||||||
|
// record shop already digs (R39), so its bins mint `kind:'bin'` — this is the one type where more
|
||||||
|
// bins is unambiguously more game.
|
||||||
|
{ kind: 'kitBin', zone: 'aisle', min: 2, max: 4, priority: 4, fallbackZones: ['centre'],
|
||||||
|
o: { pool: POOLS.record_bins, dig: true, stock: 'sleeve', items: 12 } },
|
||||||
|
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 3, priority: 4, fallbackZones: ['centre'],
|
||||||
|
o: { pool: POOLS.shop_racks, dig: true, stock: 'sleeve', items: 9 } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 3,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||||||
|
{ kind: 'kitWindow', zone: 'window', min: 0, max: 1, priority: 2, o: { pool: POOLS.window_dress } },
|
||||||
|
{ kind: 'kitSleeve', zone: 'wall', min: 1, max: 2, priority: 1,
|
||||||
|
o: { pool: POOLS.sleeve_wall, wall: true, mountY: 1.72 } }, // TRAP 2: vinyl_sleeve, wall only
|
||||||
|
// THE HERO: the rigged 1200 + a rigged TTM-54i behind the counter. 8 draws, one room, once.
|
||||||
|
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 6, gap: 2.35,
|
||||||
|
fallbackZones: ['aisle', 'centre'], anywhere: true, o: { booth: 'record_hero' } },
|
||||||
],
|
],
|
||||||
stockKind: 'sleeves',
|
stockKind: 'sleeves',
|
||||||
signFlavour: ['NEW ARRIVALS', 'VINYL $5 EA', 'SORRY NO REFUNDS', '2ND HAND CDs', 'JAZZ · SOUL · FUNK'],
|
signFlavour: ['NEW ARRIVALS', 'VINYL $5 EA', 'SORRY NO REFUNDS', '2ND HAND CDs', 'JAZZ · SOUL · FUNK'],
|
||||||
@ -72,6 +95,14 @@ const RECIPES = {
|
|||||||
// diggable. WHAT IT YIELDS IS JOHN'S CALL — LANE_C_PUB §10. Do not flip the default in passing.
|
// diggable. WHAT IT YIELDS IS JOHN'S CALL — LANE_C_PUB §10. Do not flip the default in passing.
|
||||||
{ kind: 'rummageBin', zone: 'aisle', min: 1, max: 1, priority: 4,
|
{ kind: 'rummageBin', zone: 'aisle', min: 1, max: 1, priority: 4,
|
||||||
fallbackZones: ['centre', 'window'], anywhere: true, o: { dig: false } },
|
fallbackZones: ['centre', 'window'], anywhere: true, o: { dig: false } },
|
||||||
|
// ── R41: bargain tubs + a wooden rack + a wall board. `dig` STAYS OFF — R39 measured the whole
|
||||||
|
// question ("op shops now dig RECORDS") and held the contents for John; nothing here reopens it.
|
||||||
|
{ kind: 'kitBin', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre', 'window'],
|
||||||
|
o: { pool: POOLS.bargain_tubs, stock: 'box', items: 10 } },
|
||||||
|
{ kind: 'kitRack', zone: 'wall', min: 1, max: 2, priority: 3, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.shop_racks, stock: 'box', items: 8 } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||||||
],
|
],
|
||||||
stockKind: 'garments',
|
stockKind: 'garments',
|
||||||
signFlavour: ['ALL CLOTHES $2', 'BAG SALE SAT', 'BRIC-A-BRAC', 'PROCEEDS TO CHARITY', 'HALF PRICE'],
|
signFlavour: ['ALL CLOTHES $2', 'BAG SALE SAT', 'BRIC-A-BRAC', 'PROCEEDS TO CHARITY', 'HALF PRICE'],
|
||||||
@ -110,6 +141,13 @@ const RECIPES = {
|
|||||||
{ kind: 'spinnerRack', zone: 'centre', min: 1, max: 2, priority: 3 },
|
{ kind: 'spinnerRack', zone: 'centre', min: 1, max: 2, priority: 3 },
|
||||||
{ kind: 'armchair', zone: 'window', min: 1, max: 1, priority: 2 },
|
{ kind: 'armchair', zone: 'window', min: 1, max: 1, priority: 2 },
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: the returns trolley (3dstore `library_trolley`) + a rack of overflow + wall boards.
|
||||||
|
{ kind: 'kitTrolley', zone: 'aisle', min: 1, max: 1, priority: 3, fallbackZones: ['centre'],
|
||||||
|
o: { pool: POOLS.library, stock: 'spine', items: 9 } },
|
||||||
|
{ kind: 'kitRack', zone: 'wall', min: 1, max: 2, priority: 2, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.shop_racks, stock: 'spine', items: 9 } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 2,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.55, stock: 'spine', items: 8, maxRun: 3.0 } },
|
||||||
],
|
],
|
||||||
stockKind: 'spines',
|
stockKind: 'spines',
|
||||||
signFlavour: ['ALL BOOKS $2', 'FICTION A–M', 'RARE & COLLECTABLE', 'NO HAGGLING (haggle at till)', 'POETRY ↑'],
|
signFlavour: ['ALL BOOKS $2', 'FICTION A–M', 'RARE & COLLECTABLE', 'NO HAGGLING (haggle at till)', 'POETRY ↑'],
|
||||||
@ -130,6 +168,12 @@ const RECIPES = {
|
|||||||
{ kind: 'returnsSlot', zone: 'counter', min: 1, max: 1, priority: 4 },
|
{ kind: 'returnsSlot', zone: 'counter', min: 1, max: 1, priority: 4 },
|
||||||
{ kind: 'pegboard', zone: 'wall', min: 1, max: 2, priority: 2 },
|
{ kind: 'pegboard', zone: 'wall', min: 1, max: 2, priority: 2 },
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: the ex-rental bargain tub by the door + wall boards + the window display.
|
||||||
|
{ kind: 'kitBin', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre', 'window'],
|
||||||
|
o: { pool: POOLS.bargain_tubs, stock: 'box', items: 10 } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 1, max: 2, priority: 2,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 6, maxRun: 3.0 } },
|
||||||
|
{ kind: 'kitWindow', zone: 'window', min: 0, max: 1, priority: 2, o: { pool: POOLS.window_dress } },
|
||||||
],
|
],
|
||||||
stockKind: 'spines',
|
stockKind: 'spines',
|
||||||
signFlavour: ['NEW RELEASES', 'OVERNIGHT $3', 'BE KIND REWIND', 'WEEKLY $2', 'DROP RETURNS HERE'],
|
signFlavour: ['NEW RELEASES', 'OVERNIGHT $3', 'BE KIND REWIND', 'WEEKLY $2', 'DROP RETURNS HERE'],
|
||||||
@ -150,6 +194,14 @@ const RECIPES = {
|
|||||||
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 1, priority: 2 }, // magazine_rack GLB
|
{ kind: 'magazineRack', zone: 'wall', min: 1, max: 1, priority: 2 }, // magazine_rack GLB
|
||||||
{ kind: 'barredWindow', zone: 'window', min: 1, max: 1, priority: 3 },
|
{ kind: 'barredWindow', zone: 'window', min: 1, max: 1, priority: 3 },
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: unredeemed pledges. Flight cases and a sub on the floor; an 808 and an SP-1200 on the
|
||||||
|
// bench behind the grille — statics, 2 draws, because a pawnbroker's gear is not a working booth.
|
||||||
|
{ kind: 'kitGear', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre'],
|
||||||
|
o: { pool: POOLS.pawn_gear } },
|
||||||
|
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.1,
|
||||||
|
fallbackZones: ['aisle', 'centre'], anywhere: true, o: { booth: 'pawn' } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.5, stock: 'box', items: 5, maxRun: 3.0 } },
|
||||||
],
|
],
|
||||||
stockKind: 'mixed',
|
stockKind: 'mixed',
|
||||||
signFlavour: ['WE BUY ANYTHING', 'CASH LOANS', 'GOLD · TOOLS · GEAR', 'ID REQUIRED', 'UNREDEEMED PLEDGES'],
|
signFlavour: ['WE BUY ANYTHING', 'CASH LOANS', 'GOLD · TOOLS · GEAR', 'ID REQUIRED', 'UNREDEEMED PLEDGES'],
|
||||||
@ -170,6 +222,14 @@ const RECIPES = {
|
|||||||
{ kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 3 }, // lolly/treat display case (glass_case GLB)
|
{ kind: 'glassCase', zone: 'counter', min: 1, max: 1, priority: 3 }, // lolly/treat display case (glass_case GLB)
|
||||||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
|
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 3, priority: 4 },
|
||||||
{ kind: 'cubeShelf', zone: 'aisle', min: 0, max: 1, priority: 2 },
|
{ kind: 'cubeShelf', zone: 'aisle', min: 0, max: 1, priority: 2 },
|
||||||
|
// ── R41: the hot box. `bain_marie` + `pie_warmer` are the two props that make a milk bar a milk bar
|
||||||
|
// (this is also PROCITY's cafe — there is no separate `cafe` type in SHOP_TYPE_IDS).
|
||||||
|
{ kind: 'kitHot', zone: 'wall', min: 1, max: 2, priority: 5, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.milkbar_hot } },
|
||||||
|
{ kind: 'kitFloor', zone: 'aisle', min: 1, max: 3, priority: 2, fallbackZones: ['window'],
|
||||||
|
o: { pool: POOLS.milkbar_floor } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 1,
|
||||||
|
o: { pool: POOLS.wall_service, wall: true, mountY: 2.35 } }, // the rattling air-con
|
||||||
],
|
],
|
||||||
stockKind: 'snacks',
|
stockKind: 'snacks',
|
||||||
signFlavour: ['MILK · BREAD · SMOKES', 'PIES HOT', 'PAPERS IN', 'PHONECARDS', 'NO SCHOOL KIDS AT LUNCH'],
|
signFlavour: ['MILK · BREAD · SMOKES', 'PIES HOT', 'PAPERS IN', 'PHONECARDS', 'NO SCHOOL KIDS AT LUNCH'],
|
||||||
@ -190,6 +250,14 @@ const RECIPES = {
|
|||||||
{ kind: 'metalShelf', zone: 'wall', min: 2, max: 4, priority: 3 },
|
{ kind: 'metalShelf', zone: 'wall', min: 2, max: 4, priority: 3 },
|
||||||
{ kind: 'escalator', zone: 'centre', min: 1, max: 1, priority: 5 }, // the grand-hall centrepiece
|
{ kind: 'escalator', zone: 'centre', min: 1, max: 1, priority: 5 }, // the grand-hall centrepiece
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: SPEND CAREFULLY HERE. dept is the worst room in the game (GLB-on 188 pre-R41, i.e. it owns
|
||||||
|
// the whole 162-draw margin), so it gets the fewest additions of any type: columns for the hall and
|
||||||
|
// one rack row.
|
||||||
|
{ kind: 'kitColumn', zone: 'centre', min: 0, max: 2, priority: 3, o: { pool: POOLS.columns } },
|
||||||
|
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 2, priority: 3,
|
||||||
|
o: { pool: POOLS.shop_racks, stock: 'box', items: 8 } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 2,
|
||||||
|
o: { pool: POOLS.wall_boards, wall: true, mountY: 1.6, stock: 'box', items: 6, maxRun: 3.0 } },
|
||||||
],
|
],
|
||||||
stockKind: 'mixed',
|
stockKind: 'mixed',
|
||||||
signFlavour: ['MANCHESTER ↑', 'MENSWEAR', 'HABERDASHERY', 'GROUND FLOOR', 'STAFF ONLY'],
|
signFlavour: ['MANCHESTER ↑', 'MENSWEAR', 'HABERDASHERY', 'GROUND FLOOR', 'STAFF ONLY'],
|
||||||
@ -208,6 +276,16 @@ const RECIPES = {
|
|||||||
{ kind: 'crate', zone: 'centre', min: 2, max: 5, priority: 3 },
|
{ kind: 'crate', zone: 'centre', min: 2, max: 5, priority: 3 },
|
||||||
{ kind: 'clothesRack', zone: 'wall', min: 0, max: 2, priority: 2 },
|
{ kind: 'clothesRack', zone: 'wall', min: 0, max: 2, priority: 2 },
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 8 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 8 },
|
||||||
|
// ── R41: this closes a MEASURED HOLE, not just a dressing gap. R39 counted 48 stall rooms and found
|
||||||
|
// 21 of them (44%) with no crate at all — the recipe's `crate` is zone 'centre' and the 3–6 trestles
|
||||||
|
// take the aisle first, so RED TICKET DAY could name a stall you cannot dig in. These bins carry the
|
||||||
|
// full fallback chain (aisle → centre → window → anywhere), so a market stall always has something
|
||||||
|
// to riffle. Re-measured after the change; the number is in C-progress.md §41.4.
|
||||||
|
{ kind: 'kitBin', zone: 'aisle', min: 2, max: 4, priority: 4,
|
||||||
|
fallbackZones: ['centre', 'window'], anywhere: true,
|
||||||
|
o: { pool: POOLS.record_bins, dig: true, stock: 'sleeve', items: 12 } },
|
||||||
|
{ kind: 'kitRack', zone: 'aisle', min: 1, max: 2, priority: 3, fallbackZones: ['centre'],
|
||||||
|
o: { pool: POOLS.shop_racks, stock: 'box', items: 8 } },
|
||||||
],
|
],
|
||||||
stockKind: 'boxes',
|
stockKind: 'boxes',
|
||||||
signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'],
|
signFlavour: ['$1 EACH', 'CASH ONLY', 'FIRST IN BEST DRESSED', 'MAKE ME AN OFFER', 'FRESH TODAY'],
|
||||||
@ -229,10 +307,33 @@ const RECIPES = {
|
|||||||
// watchMax = crowd cap (== max watchPoints); deckH = deck height; deckDepth/deckWidthFrac size the
|
// watchMax = crowd cap (== max watchPoints); deckH = deck height; deckDepth/deckWidthFrac size the
|
||||||
// deck. pub values == the R12 constants, so the pub stage is byte-identical (bar the new drums pose).
|
// deck. pub values == the R12 constants, so the pub stage is byte-identical (bar the new drums pose).
|
||||||
venueSpec: { watchMax: 8, deckH: 0.32, deckDepth: 2.0, deckWidthFrac: 0.58 },
|
venueSpec: { watchMax: 8, deckH: 0.32, deckDepth: 2.0, deckWidthFrac: 0.58 },
|
||||||
|
// [R41] the bar is Lane E's `plywood_bar`, not the shop `counter_till`. Same fitting, same keeper
|
||||||
|
// stand, same service strip — only the model changes.
|
||||||
|
counterGlb: 'plywood_bar',
|
||||||
fittings: [
|
fittings: [
|
||||||
{ kind: 'trestleTable', zone: 'wall', min: 1, max: 3, priority: 2 }, // pub tables against the walls
|
{ kind: 'trestleTable', zone: 'wall', min: 1, max: 3, priority: 2 }, // pub tables against the walls
|
||||||
{ kind: 'metalShelf', zone: 'wall', min: 0, max: 1, priority: 2 }, // bar back-shelf (bottles)
|
{ kind: 'metalShelf', zone: 'wall', min: 0, max: 1, priority: 2 }, // bar back-shelf (bottles)
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41 — THE PUB FINALLY GETS FURNISHED. Before this round the whole room was a bar, a stage, two
|
||||||
|
// PA cabs and up to three trestles: the gig venue with almost nothing in it. Seating hugs the WALLS
|
||||||
|
// and the floor props sit at priority 2 so the door→bar path loop pulls them FIRST — the crowd's
|
||||||
|
// standing room (watchPoints) is computed after placement and must survive furnishing. Both numbers
|
||||||
|
// (draws AND watchPoints per venue) are re-measured in C-progress.md §41.4.
|
||||||
|
// ORDER MATTERS INSIDE THIS BLOCK (and only inside it — every pre-R41 spec is above and untouched):
|
||||||
|
// the venue kit goes FIRST because the stage flanks and the side-wall slots beside the stage are
|
||||||
|
// the same 0.4 m cells. Seating placed first took them, and the gear landed 5 times in 15 rooms;
|
||||||
|
// gear first, and it lands every time while the seating simply takes the next wall slot along.
|
||||||
|
{ kind: 'kitVenue', zone: 'stage', min: 2, max: 3, priority: 2, fallbackZones: ['wall', 'aisle'],
|
||||||
|
o: { pool: POOLS.venue_kit } },
|
||||||
|
{ kind: 'kitSeat', zone: 'wall', min: 3, max: 6, priority: 3, fallbackZones: ['aisle', 'centre'],
|
||||||
|
o: { pool: POOLS.pub_seating } },
|
||||||
|
{ kind: 'kitFloor', zone: 'wall', min: 3, max: 6, priority: 2, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.pub_floor } },
|
||||||
|
{ kind: 'kitWall', zone: 'wall', min: 0, max: 1, priority: 1,
|
||||||
|
o: { pool: POOLS.wall_service, wall: true, mountY: 2.4 } },
|
||||||
|
// the DJ rig beside the bar — STATICS (E: "a static copy is 1 draw and looks the same standing still")
|
||||||
|
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.2,
|
||||||
|
fallbackZones: ['stage', 'wall', 'aisle', 'centre'], anywhere: true, o: { booth: 'venue' } },
|
||||||
],
|
],
|
||||||
stockKind: 'snacks',
|
stockKind: 'snacks',
|
||||||
signFlavour: ['LIVE MUSIC FRI', 'HAPPY HOUR 5–6', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
|
signFlavour: ['LIVE MUSIC FRI', 'HAPPY HOUR 5–6', 'NO THONGS AFTER 7', 'COLD BEER ON TAP', 'BANDS OUT BACK'],
|
||||||
@ -251,9 +352,18 @@ const RECIPES = {
|
|||||||
counterPos: 'corner', // a plank bar in the corner (keeper = barkeep)
|
counterPos: 'corner', // a plank bar in the corner (keeper = barkeep)
|
||||||
venue: true,
|
venue: true,
|
||||||
venueSpec: { watchMax: 8, deckH: 0.2, deckDepth: 1.6, deckWidthFrac: 0.62 }, // barely a riser, wide + shallow
|
venueSpec: { watchMax: 8, deckH: 0.2, deckDepth: 1.6, deckWidthFrac: 0.62 }, // barely a riser, wide + shallow
|
||||||
|
counterGlb: 'plywood_bar', // a plank bar is literally what this recipe says it is
|
||||||
fittings: [
|
fittings: [
|
||||||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf (tinnies)
|
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf (tinnies)
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: PA, sub, flight cases, a smoke machine and a distro box shoved against the wall — the tin
|
||||||
|
// shed's furniture is the gear. No seating: you stand and you sweat (the recipe's own words).
|
||||||
|
{ kind: 'kitVenue', zone: 'stage', min: 2, max: 4, priority: 3, fallbackZones: ['wall', 'aisle'],
|
||||||
|
o: { pool: POOLS.venue_kit } },
|
||||||
|
{ kind: 'kitFloor', zone: 'wall', min: 2, max: 3, priority: 2, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.pub_floor } },
|
||||||
|
{ kind: 'djBooth', zone: 'counter', min: 1, max: 1, priority: 4, gap: 2.2,
|
||||||
|
fallbackZones: ['stage', 'wall', 'aisle'], o: { booth: 'venue' } },
|
||||||
],
|
],
|
||||||
stockKind: 'snacks',
|
stockKind: 'snacks',
|
||||||
signFlavour: ['ALL AGES SUN', 'PAY THE BAND', 'NO STAGE DIVING', 'MERCH OUT BACK', '$5 ON THE DOOR'],
|
signFlavour: ['ALL AGES SUN', 'PAY THE BAND', 'NO STAGE DIVING', 'MERCH OUT BACK', '$5 ON THE DOOR'],
|
||||||
@ -271,10 +381,19 @@ const RECIPES = {
|
|||||||
counterPos: 'corner', // the members' bar
|
counterPos: 'corner', // the members' bar
|
||||||
venue: true,
|
venue: true,
|
||||||
venueSpec: { watchMax: 12, deckH: 0.4, deckDepth: 2.4, deckWidthFrac: 0.5 }, // a raised stage at one end
|
venueSpec: { watchMax: 12, deckH: 0.4, deckDepth: 2.4, deckWidthFrac: 0.5 }, // a raised stage at one end
|
||||||
|
counterGlb: 'plywood_bar',
|
||||||
fittings: [
|
fittings: [
|
||||||
{ kind: 'trestleTable', zone: 'wall', min: 2, max: 4, priority: 2 }, // bistro tables round the edge
|
{ kind: 'trestleTable', zone: 'wall', min: 2, max: 4, priority: 2 }, // bistro tables round the edge
|
||||||
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf
|
{ kind: 'metalShelf', zone: 'wall', min: 1, max: 2, priority: 2 }, // bar back-shelf
|
||||||
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
{ kind: 'counter', zone: 'counter', min: 1, max: 1, priority: 9 },
|
||||||
|
// ── R41: banquettes and bistro tables round the edge, the pokies and the meat-raffle drum the
|
||||||
|
// signage has been promising since R13. The 12-strong crowd cap is why everything hugs a wall.
|
||||||
|
{ kind: 'kitVenue', zone: 'stage', min: 1, max: 3, priority: 2, fallbackZones: ['wall', 'aisle'],
|
||||||
|
o: { pool: POOLS.venue_kit } },
|
||||||
|
{ kind: 'kitSeat', zone: 'wall', min: 3, max: 6, priority: 3, fallbackZones: ['aisle', 'centre'],
|
||||||
|
o: { pool: POOLS.pub_seating } },
|
||||||
|
{ kind: 'kitFloor', zone: 'wall', min: 3, max: 5, priority: 2, fallbackZones: ['aisle'],
|
||||||
|
o: { pool: POOLS.rsl_floor } },
|
||||||
],
|
],
|
||||||
stockKind: 'snacks',
|
stockKind: 'snacks',
|
||||||
signFlavour: ['MEMBERS & GUESTS', 'BISTRO OPEN', 'BINGO THURS', 'MEAT RAFFLE 7PM', 'LEST WE FORGET'],
|
signFlavour: ['MEMBERS & GUESTS', 'BISTRO OPEN', 'BINGO THURS', 'MEAT RAFFLE 7PM', 'LEST WE FORGET'],
|
||||||
|
|||||||