Lane C R39 §39.3: the op shop gets a bin — the verb table, and the margin was never six
THE TABLE (LANE_C_PUB §10, the deliverable): what a player can actually do in each of the 12 shop types, counted in 48 rooms per type. `record` digs (4.6 bins/room) and is the only room where anything can be sold. `stall` digs — in 27 of 48 rooms; the other 21 (44%) have NO crate, a measured correction to the round's binding fact #5. The other ten types offer looking. `?stock=real` is opt-in, so the default boot has zero `buyMesh` anywhere; garments/snacks/magazines/treasures are unbuyable in any mode; `interactable:true` on cases/fridges/returns/arcade is read by nothing. THE MECHANISM: `rummageBin` — a castored galvanised dump tub, rim 0.76 m, heaped with stickered bric-a-brac and a hand-lettered "EVERYTHING $2" card. 72 rooms: placed 72/72, reachable 72/72 (0.77–1.15 m), 0 carves, pathOK 72/72, deterministic 72/72, and the rest of the room byte-identical to pre-R39 72/72 (the spec is appended last). Armed end-to-end: aim → dig → 16 offers → BUY → bin depletes. Default: 0 bins, dig never opens. THE CONTENTS ARE NOT SHIPPED. `o.dig` defaults false, so the tub is furniture and the economy is untouched; `opts.fittingOpts.rummageBin.dig` (or `?opshopdig=1`) arms it. Held for John with the numbers: 44 record + 36 stall vs 147 op shops across 23 towns. THE BUDGET: `C-progress.md:841`'s GLB-on 344 is R9-era and stale — today's tree measured 319 before I touched it, so the margin was 31, not six. The bin costs +5 GLB-off / +10 GLB-on at opshop/hall; every other type byte-identical. Margin now 21. Also: `slot.pool` stops the scatter fill minting a material per item, so 12 goods cost less than 9 did. FILED, NOT FIXED: Lane E's bookshelf GLB ships transmission:1, which triggers three.js's transmission pre-pass and issues every draw in the room twice — opshop/hall 191→104, book/hall 101→51. Roughly half the interior budget. E's asset, so E/F rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c2e44821c7
commit
eeba920d68
@ -3,7 +3,99 @@
|
||||
*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.*
|
||||
|
||||
Last updated: 2026-07-18 (round 30) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-08-03 (round 39) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-08-03 (round 39, v9-alpha THE CITY IS THE GAME) — §39.3 the op shop can't be dug
|
||||
|
||||
Fable's finding, verified before commissioning: `kind:'bin'` — the thing the dig fires on — is minted at
|
||||
exactly `fittings.js` `recordBin`/`crate` and used by exactly two recipes, `record` and market `stall`.
|
||||
PROCITY is built on thriftgod's op-shop DNA and **you cannot dig in an op shop.** Brief: measure what a
|
||||
player can actually do in every shop type, build the op shop a bin, **flag the contents, do not decide
|
||||
them**, and spend against the GLB-path interior margin.
|
||||
|
||||
### 1. THE TABLE (LANE_C_PUB §10) — the deliverable, and it is uncomfortable
|
||||
Built by reading the recipes and the fitting kinds, then counting interactables in **48 rooms per type**
|
||||
(6 archetypes x 8 seeds). Full table + the five structural facts in the contract; the short version:
|
||||
- **`record`** digs (mean **4.6** bins/room, 0/48 empty) and is **the only room in the game where
|
||||
anything can be sold**. **`stall`** digs — in 27 of 48 rooms; the other **21 (44%) have no crate at
|
||||
all** (mean 0.92), because its `crate` is `zone:'centre'` and the 3–6 trestles take the aisle first.
|
||||
**That is a measured correction to the round's binding fact #5** and it means RED TICKET DAY's roster,
|
||||
which counts a stall by type, can name a stall you cannot dig in.
|
||||
- **The other ten types offer nothing but looking.** No bins, and pull-and-buy is dark: `?stock=real` is
|
||||
opt-in (`index.html:222`), so **on the boot every player gets there is not one `buyMesh` in the game**.
|
||||
Even armed, `SLOT_FOR` has three entries and the pack is keyed by SHOP TYPE, so only book spines in a
|
||||
`book` shop and toy boxes in a `toy` shop can ever be bought — the video store's identical spine
|
||||
shelves cannot, because no `video` pack exists.
|
||||
- **Four stock kinds can never be bought in any mode**: `garment`, `snack`, `magazine`, `treasure` —
|
||||
`hang()`/`treasures()` never call `buildBuyableShelf`. **The op shop's clothes are unbuyable by
|
||||
construction.**
|
||||
- **`interactable: true` is not a verb.** `interior_mode` routes exactly three things (aimed `bin`,
|
||||
counter, `buyMesh`). `case`, `fridge`, `returns` and the arcade cabinet set the flag and **nothing
|
||||
reads them** — 8 dead glass cases in a dept-store hall, 4 dead fridges in a milk bar.
|
||||
- **Sell** is `item.type === shopType`, and only two call sites stamp a type: a dig pull is **always
|
||||
`'record'` wherever it happens**, a shelf buy is the shop's type. Hence: one sale point, default boot.
|
||||
|
||||
### 2. THE MECHANISM — `rummageBin`, shipped, contents gated
|
||||
A shallow galvanised dump tub (0.95 x 0.70 x 0.28) with flared sides on a castored steel frame, rim at
|
||||
**0.76 m** — you rummage standing — heaped with stickered mixed goods and a hand-lettered card taped to
|
||||
the front ("EVERYTHING $2"). Not a record crate recoloured: `crate` is 0.55 x 0.45 and deep because it
|
||||
is sized to stand LPs in. Shot: [r39_opshop_rummage_bin.png](docs/shots/laneC/r39_opshop_rummage_bin.png).
|
||||
|
||||
Three new **opt-in, pure-data** seams (absent ⇒ pre-R39 byte-identical): `recipe.fittings[i].o`,
|
||||
`buildInterior opts.fittingOpts[kind]`, and `.fallbackZones` / `.anywhere` placement fallbacks. The last
|
||||
one is not decoration — appended last, the bin's zone is the fullest it will ever be and it landed in
|
||||
only **11 of 24** rooms without them.
|
||||
|
||||
**72 rooms (6 archetypes x 6 seeds x GLB on/off):** placed **72/72** · reachable **72/72** (nearest
|
||||
spawn-reachable walkable cell **0.77–1.15 m** from the bin, vs the dig's 2.5/3.2 m reach) · **0 carves** ·
|
||||
`pathOK` 72/72 · determinism 72/72 · **the rest of the room byte-identical to pre-R39 72/72** (the spec is
|
||||
appended LAST, so the shared `lay-count`/`lay-place` streams hand every existing fitting the same draws).
|
||||
End-to-end armed: aim → dig opens → **16 offers** → card → **BUY** debits the wallet → bin depletes to 15.
|
||||
Negative control (default): tub present, **0 bins**, aim finds nothing, dig never opens. 0 console errors.
|
||||
Soak 60 rooms: **leak geo 0 / tex 0**, determinism identical, avg 3.4 ms / worst 16.5 ms.
|
||||
|
||||
### 3. SPENT AGAINST THE GLB PATH — and the margin was not six
|
||||
**`C-progress.md:841`'s "GLB-on 344" is R9-era and stale.** Re-measured on today's tree with the lane's
|
||||
own instrument before touching anything: **GLB-off 156 · GLB-on 319, both at `opshop/hall`.** So the
|
||||
margin was **31**, not six. After the bin:
|
||||
|
||||
| `drawSweep` | pre-R39 | R39 | Δ |
|
||||
|---|---|---|---|
|
||||
| GLB-off worst | 156 opshop/hall | **161** opshop/hall | +5 |
|
||||
| GLB-on worst | 319 opshop/hall | **329** opshop/hall | +10 |
|
||||
| the other 11 types, both modes | — | — | **0, byte-identical** |
|
||||
|
||||
**Margin against ≤350: 21 draws.** Stated at the worst room the bin lands in, not an average.
|
||||
A free win found while costing it: the scatter fill minted a *tinted* material per item (9 goods = 9
|
||||
draws in a room with no other scatter). New opt-in `slot.pool` keeps every face on the room's shared
|
||||
pooled textures at white — the trick the spine/box rows already use — so the pile merges into buckets
|
||||
the room already has. **12 goods now cost less than 9 did.** Existing scatter callers draw the same
|
||||
values in the same order; proven by the 11 untouched types' sweep numbers.
|
||||
|
||||
### 4. THE BIGGER FINDING — half the interior budget is a pre-pass (→ E/F, NOT shipped)
|
||||
Lane E's `bookshelf` GLB ships **`transmission: 1`** on `mtl_10218_Bookshelves_v1`. One transmissive
|
||||
material makes three.js run its transmission pre-pass — a second full render of the opaque scene — so
|
||||
**every draw in that room is issued and counted twice.** Measured by zeroing it and re-rendering:
|
||||
`opshop`/hall **191 → 104 (−46%)** · `opshop`/wide 142 → 80 · `book`/hall **101 → 51 (−50%)** ·
|
||||
`dept`/hall 116 → 116 (the control, no bookshelf). Those are the two worst GLB-on types in the sweep.
|
||||
**→ E:** re-emit without `KHR_materials_transmission`. **→ F:** a `transmission = 0` scrub in
|
||||
`glb.js upgradeFitting` is the cheap fence. **I have not shipped either** — it changes how another
|
||||
lane's asset renders. Either turns a 21-draw margin into ~100.
|
||||
|
||||
### 5. HELD FOR JOHN — the contents (LANE_C_PUB §10.6)
|
||||
Not "op shops now dig", but "op shops now dig **records**". Across the 23 shipped caches (44 record ·
|
||||
36 stall · **147 opshop**): crate-shops/town **0–11 → 2–25**, towns with none **3 → 0**, diggable bins
|
||||
**~235 → ~382 (+63%)**, under-band gems/day ~113 → ~183. **A** (parody records) costs zero code and is
|
||||
what the market stall already does, but an op-shop find is stamped `type:'record'` and bowral/darwin/
|
||||
daylesford have no record shop — they could dig and have nowhere to sell. **B** (books/toys via the
|
||||
existing packs) needs an `opshop` pack that cannot exist today *and* a second dig presentation, because
|
||||
`dig.js` builds a crate of 0.31 m square sleeves. **C** (a bric-a-brac class) costs most and is the only
|
||||
one that closes the sell hole, since type = type would make op shops buy their own bric-a-brac back.
|
||||
**Recommendation: play arm A behind the flag for one session, then rule; C is the destination; B is not
|
||||
worth doing alone.** Also held, same reason: the one-line `fallbackZones` fix that would give market
|
||||
stalls the crates their recipe already promises.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -32,6 +32,11 @@
|
||||
> **v7.0-alpha amendment (2026-07-18, R30 ledger #2):** **§9 added — the sell-counter contract** (the
|
||||
> no-pump law made structural: `offer(p) = min(p−1, max(1, floor(p·0.5)))` — strictly below the buy side
|
||||
> for every p ≥ 1, by construction). §0–§8 unchanged.
|
||||
> **v9.0-alpha amendment (2026-08-03, R39 §39.3):** **§10 added — WHAT YOU CAN ACTUALLY DO IN A SHOP:**
|
||||
> the honest per-type verb table, the five structural facts under it, a measured correction to the
|
||||
> round's binding fact #5 (**44% of market-stall rooms contain no crate**), the op shop's own
|
||||
> **rummage bin** (shipped; contents gated, held for John), and the finding that **one asset's
|
||||
> `transmission: 1` is costing ~46–50% of the interior draw budget**. §0–§9 unchanged.
|
||||
|
||||
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||||
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
|
||||
@ -547,3 +552,192 @@ const sell = createSell();
|
||||
sell.open({ shopType, shopName, items, getCash, onSell, onClose, emitters }) // → bool (false = nothing sellable)
|
||||
sell.close(); sell.dispose(); sell.active // mirrors dig.js
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. WHAT YOU CAN ACTUALLY DO IN A SHOP (v9-alpha, R39 §39.3) → everyone
|
||||
|
||||
> **This section is a measurement, not a design.** Fable's brief asked for the honest per-type
|
||||
> inventory of player verbs, built by reading the recipes and the fitting kinds and then counting
|
||||
> them in built rooms — not from memory. It is uncomfortable. It is also, as far as I can tell, the
|
||||
> first time anyone has written down what this game currently offers, shop type by shop type.
|
||||
|
||||
### 10.1 THE TABLE — the DEFAULT BOOT (`web/index.html`, no flags), 12 types
|
||||
|
||||
Counts are from built rooms: **6 archetypes × 8 seeds = 48 rooms per type**, headless.
|
||||
|
||||
| type | dig a bin? | pull-and-buy off a shelf? | sell at the counter? | anything else |
|
||||
|---|---|---|---|---|
|
||||
| `record` | **YES** — 2–8 bins/room, mean **4.6**, 0/48 rooms empty | no — records are the dig, by design | **YES** — the only shop in the game where anything can be sold | — |
|
||||
| `opshop` | **no** *(R39: it now HAS a bin; contents unruled — §10.4)* | no — there is no `opshop` stock pack, and none is possible today | no — nothing in the game is ever `type:'opshop'` | look at clothes you cannot buy |
|
||||
| `book` | no | **only under `?stock=real`** — ~37 buy meshes / ~412 items per room | only for items already `type:'book'`, which also needs `?stock=real` | — |
|
||||
| `toy` | no | **only under `?stock=real`** — ~34 meshes / ~62 items per room | ditto, `type:'toy'` | a glass case (interactable, no consumer) |
|
||||
| `video` | no | **no** — `vhsAisle` has `'spine'` slots, but the pack is resolved by SHOP TYPE and there is no `video` pack | no | returns slot + arcade cabinet: both `interactable:true`, **both dead** |
|
||||
| `pawn` | no | no | no | glass cases + pegboard — dressing only |
|
||||
| `milkbar` | no | no | no | fridge + glass case: `interactable:true`, **dead** |
|
||||
| `dept` | no | no | no | glass cases, escalator — dressing only |
|
||||
| `stall` | **YES, but** — **21 of 48 rooms have ZERO crates**; mean **0.92**, max 3 | no | **no** — a stall dig records `type:'record'`; the stall is `type:'stall'`, so the matcher fails | — |
|
||||
| `pub` | no | no | no | on a gig night: pay the cover, watch the band |
|
||||
| `band_room` | no | no | no | ditto |
|
||||
| `rsl` | no | no | no | ditto |
|
||||
|
||||
Every type additionally: walk in, walk out, look, a keeper standing at the counter, up to 3 browser
|
||||
rigs, a seeded radio/room-tone, and (with a wantlist) a ~45% keeper rumour on the banner.
|
||||
|
||||
### 10.2 The five structural facts behind that table (verified in the code, R39)
|
||||
|
||||
1. **`?stock=real` is opt-in** — `index.html:222` `params.get('stock') === 'real'`, and
|
||||
`interior_mode.js:360` gates the adapter on it. No adapter ⇒ `stock.js` never calls
|
||||
`buildBuyableShelf` ⇒ **on the boot every player gets, there is not one `buyMesh` anywhere in the
|
||||
game.** The R9 buy-anywhere path is real, tested and shipped, and it is *dark by default*.
|
||||
2. **The pack is chosen by SHOP TYPE, and `SLOT_FOR` has three entries** (`stockpack.js:85`:
|
||||
`record→sleeve · book→spine · toy→box`). `buildInterior` asks `getStockPack(recipe.key)`. So the
|
||||
only shelves that can *ever* be buyable are book spines in a `book` shop and toy boxes in a `toy`
|
||||
shop — the video store's identical spine shelves cannot be, because no `video` pack exists.
|
||||
3. **Four stock kinds can never be bought in any mode**: `garment`, `snack`, `magazine`, `treasure`.
|
||||
`hang()` and `treasures()` never call `buildBuyableShelf`, and no pack type maps to them. **The op
|
||||
shop's actual stock — the clothes — is unbuyable by construction.**
|
||||
4. **`sellableIn` is `item.type === shopType`** (`sell.js:37`, fail-closed) and only two call sites
|
||||
ever stamp a `type`: `interior_mode.js:206` (a dig pull — **always `'record'`, wherever it
|
||||
happened**) and `:251` (a shelf buy — `currentShop.type`). Therefore on a default boot the sell
|
||||
card can open in exactly one kind of room: a record shop.
|
||||
5. **`interactable: true` is not a verb.** `interior_mode`'s E routes exactly three things: an aimed
|
||||
`kind:'bin'`, the counter (via `nearCounter`), and a `buyMesh`. `case`, `fridge`, `returns` and
|
||||
the arcade cabinet all set `interactable: true` and **nothing reads them** — 5 glass cases per
|
||||
pawn room, 8 per dept room, 4 fridges per milk bar, all inert. That flag is currently a comment.
|
||||
|
||||
### 10.3 The correction to the binding fact (R39, measured — the honest direction)
|
||||
|
||||
The round's binding fact #5 says *"only record shops and market stalls have a diggable crate."* True
|
||||
of the **recipes**. Not true of the **rooms**: the stall's `crate` is `zone:'centre'`, and the stall's
|
||||
3–6 trestle tables take the aisle first, so the centre candidates are blocked. **21 of 48 stall rooms
|
||||
(44%) contain no crate at all**; mean 0.92 per room against the record shop's 4.6.
|
||||
So RED TICKET DAY's roster, which counts a stall as a crate-shop by type, **overcounts**: a roster can
|
||||
name a stall you cannot dig in. One line fixes it (`fallbackZones` / `anywhere` on the stall's crate
|
||||
spec — the machinery landed this round), but it adds crates to the world, so it is **held with the
|
||||
contents question** and not shipped.
|
||||
|
||||
### 10.4 THE RUMMAGE BIN — the op shop's own floor bin (SHIPPED, contents gated)
|
||||
|
||||
`fittings.js rummageBin` + the `opshop` recipe. A shallow galvanised dump tub (0.95 × 0.70 × 0.28)
|
||||
with flared sides on a castored steel frame, rim at **0.76 m** — you rummage standing, not crouched —
|
||||
heaped with stickered mixed goods and a hand-lettered card taped to the front panel
|
||||
("EVERYTHING $2"). Deliberately not a record crate recoloured: `crate` is 0.55 × 0.45 and deep
|
||||
because it is sized to stand LPs in, and nothing stands up in this.
|
||||
|
||||
```js
|
||||
// the shipped default — furniture. kind:'prop', no bin, no dig prompt, no `places` entry.
|
||||
buildInterior(shop, THREE)
|
||||
// armed — kind:'bin', the dig fires. ONE opt. (interior_test.html: ?opshopdig=1)
|
||||
buildInterior(shop, THREE, { fittingOpts: { rummageBin: { dig: true } } })
|
||||
```
|
||||
|
||||
**New, generic, opt-in seams (all pure data, no stream reads, absent ⇒ pre-R39 byte-identical):**
|
||||
- `recipe.fittings[i].o` — opts handed to the fitting builder.
|
||||
- `buildInterior opts.fittingOpts[kind]` — per-build override of the above. F's gate handle.
|
||||
- `recipe.fittings[i].fallbackZones` / `.anywhere` — placement fallbacks for a fitting a recipe
|
||||
*promises* will be there. Without them the bin landed in only **11 of 24** archetype×seed rooms,
|
||||
because it is appended last and its zone is the fullest it will ever be. With them: **72/72**.
|
||||
|
||||
**Guarantees (72 rooms = 6 archetypes × 6 seeds × GLB on/off, headless):**
|
||||
- placed **72/72** · **0 carves** · `pathOK` **72/72** · determinism **72/72**
|
||||
- **reachable 72/72** — the nearest walkable cell that is flood-reachable from the spawn sits
|
||||
**0.77–1.15 m** from the bin, against the dig's 2.5 m proximity / 3.2 m aimed reach
|
||||
- **the rest of the room is byte-identical to pre-R39 in 72/72.** The spec is appended LAST, so the
|
||||
shared `lay-count` / `lay-place` streams hand every existing op-shop fitting the same draws it
|
||||
always had; only the tub is added.
|
||||
- end-to-end, armed: aim resolves → dig opens → **16 offers** → pull card → **BUY** debits the wallet
|
||||
→ the bin depletes to 15. Negative control, default: tub present, **0 bins**, aim finds nothing,
|
||||
the dig never opens. Zero console errors on both arms.
|
||||
|
||||
**Draw cost — measured on the GLB path, which is the default boot (`index.html:349`).**
|
||||
|
||||
| `drawSweep` (the ≤350 law's own instrument) | pre-R39 | R39 | Δ |
|
||||
|---|---|---|---|
|
||||
| GLB-**off** worst | 156 (opshop/hall) | **161** (opshop/hall) | **+5** |
|
||||
| GLB-**on** worst | 319 (opshop/hall) | **329** (opshop/hall) | **+10** |
|
||||
| every other type, both modes | — | — | **0 — byte-identical, 11/11** |
|
||||
|
||||
**Margin against the ≤350 law: 21 draws.** Per-room the tub costs **+3…+9** GLB-off and **+6…+18**
|
||||
GLB-on (the doubling is not the tub — see §10.5). It is the worst-case room it lands in, which is why
|
||||
this is stated against `opshop/hall` and not an average. Soak: 60 rooms, **leak geo 0 / tex 0**,
|
||||
determinism identical, avg 3.4 ms / worst 16.5 ms.
|
||||
|
||||
**A free win found while costing it (`stock.js`, shipped).** The scatter fill minted a *tinted*
|
||||
material per item, so a pile of 9 goods in a room with no other scatter cost **9 draws** — one bucket
|
||||
each. New opt-in `slot.pool` keeps every face on the room's shared pooled textures at white, exactly
|
||||
as the spine and shelf-box rows already do, so the pile merges into buckets the room already has.
|
||||
The tub went from 9 goods / 12 draws to **12 goods / 3–9 draws**. Existing scatter callers
|
||||
(`trestleTable`, `pegboard`) draw the same values in the same order and are byte-identical — proven
|
||||
by all 11 other types' `drawSweep` numbers not moving.
|
||||
|
||||
### 10.5 THE INTERIOR BUDGET IS ~2× WHAT IT NEEDS TO BE — one asset material (→ Lanes E/F)
|
||||
|
||||
Costing the tub turned up something worth more than the tub. **Lane E's `bookshelf` GLB ships
|
||||
`transmission: 1`** on `mtl_10218_Bookshelves_v1`. One transmissive material in a scene makes
|
||||
three.js run its transmission pre-pass — **a second full render of the opaque scene into a render
|
||||
target** — so *every* draw in that room is issued and counted twice.
|
||||
|
||||
Measured, seed 1990, isolated room, by setting `transmission = 0` on that material and re-rendering:
|
||||
|
||||
| room | GLB-on draws | with `transmission: 0` | saved |
|
||||
|---|---|---|---|
|
||||
| `opshop`/hall (the game's worst interior) | 191 | **104** | **−87 (−46%)** |
|
||||
| `opshop`/wide | 142 | **80** | −62 (−44%) |
|
||||
| `book`/hall | 101 | **51** | **−50 (−50%)** |
|
||||
| `dept`/hall (no bookshelf) | 116 | 116 | 0 — the control |
|
||||
|
||||
`bookshelf` is in the `opshop` (1–2) and `book` (5–8) recipes, which are the two worst GLB-on types
|
||||
in the sweep (329 and 243). **Roughly half the interior draw budget is being spent on a pre-pass
|
||||
nobody asked for, triggered by a flag on one asset**, and a bookshelf rendering as glass is almost
|
||||
certainly not the intent. **→ E:** re-emit without `KHR_materials_transmission`. **→ F:** the cheap
|
||||
runtime fence is a `transmission = 0` scrub in `glb.js upgradeFitting`; I have NOT shipped it, because
|
||||
it changes how another lane's asset renders and that is not mine to decide in passing. Either fix
|
||||
turns a 21-draw margin into a ~100-draw one.
|
||||
|
||||
### 10.6 THE CONTENTS — HELD FOR JOHN. Do not flip the default without this ruling.
|
||||
|
||||
The mechanism is shipped and gated. **What is IN the bin is the question**, because it is not "op
|
||||
shops now dig", it is "op shops now dig **records**". Measured against the 23 shipped town caches
|
||||
(1,236 shops: **44 record · 36 stall · 147 opshop**):
|
||||
|
||||
| | today | with the op-shop bin armed |
|
||||
|---|---|---|
|
||||
| crate-**shops** per town | 0–11 | **2–25** |
|
||||
| towns with **none** | **3** (bowral, darwin, daylesford) | **0** |
|
||||
| diggable **bins**, all 23 towns¹ | ~235 | ~382 (**+63%**) |
|
||||
| under-band gems/day, all towns² | ~113 | ~183 |
|
||||
|
||||
¹ record 4.6 bins/room × 44 + stall 0.92 × 36 + opshop 1.0 × 147. ² `dig.js:48` — 10% gem roll on a
|
||||
collector/grail band ⇒ 3% per sleeve × 16 sleeves/bin.
|
||||
|
||||
**A · parody records (what the mechanism inherits).** **Cost: zero code** — `proceduralSleeves`
|
||||
already serves everything, and *the market stall already does exactly this*, so this is a change of
|
||||
scale, not of kind. BIBLE bands untouched (`record` bands exist). RED TICKET DAY gets a roster in all
|
||||
23 towns. **The catch:** an op-shop find is stamped `type:'record'`, so it can only be sold in a
|
||||
record shop — and bowral, darwin and daylesford have **zero** record shops. Those three towns would
|
||||
be able to dig and have nowhere to sell. The first pull from an armed bin, seed 42, was a `grail`-band
|
||||
record asking **$72**, in the Salvos bric-a-brac tub. That is either the op-shop fantasy made
|
||||
mechanical or the moment records stop being special; it is a taste call, not a measurement.
|
||||
|
||||
**B · books and toys through the existing pull-and-buy pack.** Not free. (i) The pack is resolved by
|
||||
shop type, so an op shop asks for `stock_opshop_index.json`, which does not exist — it needs either an
|
||||
E-emitted pack or a recipe-declares-its-pack-types change in C. (ii) **The dig scene is
|
||||
record-shaped**: `dig.js` builds a crate of 0.31 m square sleeves and a card that says
|
||||
artist/title/condition. A paperback in it renders as a square LP sleeve. So B costs a second dig
|
||||
presentation. In its favour: the BIBLE already has `book` and `toy` bands (`bible.js:28-29`), so the
|
||||
price fences hold with **zero new numbers**, and the sell path is clean once F stamps the item's own
|
||||
type instead of the hardcoded `'record'`.
|
||||
|
||||
**C · a new bric-a-brac class.** Highest cost — a new BIBLE band row, a parody generator or pack, a
|
||||
card treatment, gates. Also the only option that *fixes* A's asymmetry: `sellableIn` is type = type,
|
||||
so bric-a-brac would sell **in op shops**, which is both correct and diegetic, and the op shop becomes
|
||||
its own loop instead of a second-rate record shop.
|
||||
|
||||
**My recommendation.** Ship the flag (done), and **let John play arm A for one session before anyone
|
||||
decides** — it is zero code and it answers the only question that matters, which is whether digging an
|
||||
op shop is fun. Then: **C is the destination**, because it is what stops "records" being the answer to
|
||||
every question in this game and it closes the three-town sell hole. **B is not worth doing alone** —
|
||||
its cost lands in the wrong place (a record-shaped dig scene showing a book), and its good half (the
|
||||
band table, the type stamp) is inherited by C anyway. If John wants RED TICKET DAY running in all 23
|
||||
towns *this round*, A does it today for nothing, and the three-town sell hole is the price.
|
||||
|
||||
BIN
docs/shots/laneC/r39_opshop_room.png
Normal file
BIN
docs/shots/laneC/r39_opshop_room.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 886 KiB |
BIN
docs/shots/laneC/r39_opshop_rummage_bin.png
Normal file
BIN
docs/shots/laneC/r39_opshop_rummage_bin.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@ -77,6 +77,10 @@ import { createSell, sellOffer, sellableIn, nearCounter, SELL_DIST } from './js/
|
||||
|
||||
// ?stock=real — feed Lane E's GODVERSE record-sleeve pack through the stockAdapter seam.
|
||||
const STOCK_REAL = new URLSearchParams(location.search).get('stock') === 'real';
|
||||
// [R39 §39.3] `?opshopdig=1` arms the op shop's rummage bin as a real `kind:'bin'` (the mechanism is
|
||||
// built and measured; WHAT IS IN IT is John's call, so the default stays furniture). Both arms are
|
||||
// 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'; })();
|
||||
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);
|
||||
|
||||
@ -151,7 +155,8 @@ function rebuild() {
|
||||
const arch = archSel.value === 'auto' ? undefined : archSel.value;
|
||||
const lot = parseLot($('lot').value);
|
||||
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, stock: STOCK_REAL ? 'real' : undefined });
|
||||
current = buildInterior(shop, THREE, { archetype: arch, useGLB, manifest: useGLB ? glbManifest : undefined,
|
||||
stock: STOCK_REAL ? 'real' : undefined, fittingOpts: OPSHOP_DIG ? { rummageBin: { dig: true } } : undefined });
|
||||
scene.add(current.group);
|
||||
// place the player at the spawn
|
||||
camera.position.set(current.spawn.x, 1.6, current.spawn.z);
|
||||
@ -582,7 +587,7 @@ function frame() {
|
||||
|
||||
// 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,
|
||||
DIG_ON, STOCK_REAL, 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,
|
||||
SELL_STUB, sell, trySell, sellOffer, sellableIn, nearCounter, SELL_DIST };
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
// PROCITY Lane C — parametric fittings kit. Ported from 90sDJsim/web/world/fittings.js (the kit was
|
||||
// "written to be lifted") and extended with the fittings the CITY_SPEC registry needs: cube/metal/
|
||||
// book/VHS shelving, glass case, fridge, magazine rack, spinner rack, armchair, escalator prop,
|
||||
// returns slot, barred window, listening corner, crate.
|
||||
// returns slot, barred window, listening corner, crate, rummage bin (the op shop's own floor bin, R39).
|
||||
//
|
||||
// Every builder is `(ctx, o, r) → Fitting` where:
|
||||
// ctx : the shared Ctx (tracked box/mat/cyl for leak-free disposal, canvas textures)
|
||||
@ -92,6 +92,86 @@ function crateStack(ctx, o = {}, r) {
|
||||
return { group: g, footprint: { w: w + 0.1, d: d + 0.1 }, height: n * ch, slots: [], places: [] };
|
||||
}
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────────────────
|
||||
// OP SHOP
|
||||
//
|
||||
// [R39 — THE OP SHOP CAN'T BE DUG] Until this round `kind:'bin'` — the thing the dig fires on — was
|
||||
// minted in exactly two places (recordBin, crate) and used by exactly two recipes (record, stall).
|
||||
// The op shop, the type this game is built out of, had clothes racks, shelves, a trestle and a
|
||||
// crateStack its own comment calls back-room dressing: **no floor bin at all.** This is the op shop's
|
||||
// own bin, and it is deliberately NOT a record crate recoloured:
|
||||
// • WIDER THAN DEEP and shallow (0.95 × 0.70 × 0.28 tub) — a dump bin, not a sleeve bin. `crate` is
|
||||
// 0.55 × 0.45 and deep, because it is sized to stand LPs up in; nothing stands up in this.
|
||||
// • RIM AT 0.76 m — you rummage standing with your elbows in, not crouched over a floor crate.
|
||||
// • FLARED SIDES on a castored steel frame — the church-hall / charity-shop dump tub silhouette.
|
||||
// • A HAND-LETTERED CARD taped to the front panel ("EVERYTHING $2") — the one thing no record bin
|
||||
// in this game has, and the thing that makes it read op shop from across the room.
|
||||
// CONTENTS ARE NOT DECIDED HERE. `o.dig` (default **false**) is what mints `kind:'bin'`; with it off
|
||||
// this is honest op-shop furniture and the game's economy is untouched. See LANE_C_PUB §10: what a
|
||||
// dig in an op shop yields is a John taste call, and turning it on ships "op shops dig RECORDS".
|
||||
const RUMMAGE_CARD = ['EVERYTHING $2', 'BRIC-A-BRAC 50¢', '$2 EACH', 'FILL A BAG $5', 'ANY 3 FOR $5'];
|
||||
const RUMMAGE_TUB = ['#8d949a', '#6e7f86', '#7c8a6a', '#9a8f7a']; // galvo, faded teal, sage, cream-grey
|
||||
function rummageBin(ctx, o = {}, r) {
|
||||
const g = new ctx.THREE.Group();
|
||||
const w = o.w || 0.95, d = o.d || 0.70;
|
||||
const legH = 0.48, tubH = 0.28, th = 0.02, flare = 0.24; // rim = legH + tubH = 0.76 m
|
||||
const tub = ctx.mat(RUMMAGE_TUB[(r() * RUMMAGE_TUB.length) | 0], 0.6, { metalness: 0.25 });
|
||||
const frame = MAT.metal(ctx);
|
||||
// castored steel frame: four legs + a lower cross rail (the rail is where the card is wired on)
|
||||
[[-1, -1], [1, -1], [-1, 1], [1, 1]].forEach(([sx, sz]) => {
|
||||
ctx.box(0.04, legH, 0.04, frame, sx * (w / 2 - 0.07), legH / 2, sz * (d / 2 - 0.07), g);
|
||||
ctx.cyl(0.035, 0.03, frame, sx * (w / 2 - 0.07), 0.015, sz * (d / 2 - 0.07), g); // castor
|
||||
});
|
||||
ctx.box(w - 0.1, 0.03, 0.03, frame, 0, 0.2, d / 2 - 0.07, g);
|
||||
ctx.box(w - 0.1, 0.03, 0.03, frame, 0, 0.2, -(d / 2 - 0.07), g);
|
||||
// the tub: outer floor + four panels flared out at the rim, plus a raised inner plate so the goods
|
||||
// ride up and crest the rim (the same trick `crate` uses for its sleeves — a full bin, not a hole)
|
||||
ctx.box(w, th, d, tub, 0, legH, 0, g);
|
||||
ctx.box(w - 0.06, th, d - 0.06, tub, 0, legH + 0.10, 0, g);
|
||||
const side = (bw, bh, bd, x, y, z, rx, rz) => {
|
||||
const m = ctx.box(bw, bh, bd, tub, x, y, z, g); m.rotation.x = rx; m.rotation.z = rz; return m;
|
||||
};
|
||||
side(w, tubH, th, 0, legH + tubH / 2, d / 2, flare, 0);
|
||||
side(w, tubH, th, 0, legH + tubH / 2, -d / 2, -flare, 0);
|
||||
side(th, tubH, d, -w / 2, legH + tubH / 2, 0, 0, -flare);
|
||||
side(th, tubH, d, w / 2, legH + tubH / 2, 0, 0, flare);
|
||||
// the hand-lettered card, taped to the front panel — the op-shop tell. Sits ON the flared panel's
|
||||
// own face: rotating a +Z plane by `flare` about X tilts its normal to (0, −sin, cos), so the card
|
||||
// is offset along that normal rather than along +Z, or it z-fights the panel it is taped to.
|
||||
const card = ctx.plane(0.52, 0.20, cardMat(ctx, RUMMAGE_CARD[(r() * RUMMAGE_CARD.length) | 0]), g);
|
||||
card.position.set(0, legH + tubH / 2 - Math.sin(flare) * 0.018, d / 2 + Math.cos(flare) * 0.018);
|
||||
card.rotation.x = flare;
|
||||
// mixed goods, tipped in — the trestle-table scatter, not a fan of sleeves. `pool` keeps every face
|
||||
// on the room's shared pooled textures, so a full tub is ~0 extra draws (see stock.js shelfLine).
|
||||
const slots = [{ kind: 'box', x: 0, y: legH + 0.11, z: 0, ry: 0, run: w - 0.16, depth: d - 0.16,
|
||||
height: 0.22, count: o.items ?? 12, lean: false, scatter: true, pool: true }];
|
||||
// `dig` is the CONTENTS gate, not a geometry switch — off ⇒ kind 'prop' ⇒ no bin, no dig prompt,
|
||||
// no `places` entry, and the room is byte-identical to a room with any other prop in this slot.
|
||||
const diggable = o.dig === true;
|
||||
g.userData = diggable ? { kind: 'bin', interactable: true } : { kind: 'prop' };
|
||||
return { group: g, footprint: { w: w + 0.16, d: d + 0.16 }, height: legH + tubH,
|
||||
slots, places: diggable ? [g] : [] };
|
||||
}
|
||||
|
||||
// A biro-on-cardboard price card (the op shop's own signage voice — not the shop's printed signs).
|
||||
// The font SHRINKS TO FIT: a fixed size clipped "EVERYTHING $2" to "VERYTHING $" on screen, which is
|
||||
// the one thing on this prop the player can actually read.
|
||||
function cardMat(ctx, text) {
|
||||
const W = 192, H = 72, PAD = 14;
|
||||
const cv = document.createElement('canvas'); cv.width = W; cv.height = H;
|
||||
const x = cv.getContext('2d');
|
||||
x.fillStyle = '#d9c9a4'; x.fillRect(0, 0, W, H); // cardboard
|
||||
x.strokeStyle = 'rgba(90,70,40,.45)'; x.lineWidth = 2; x.strokeRect(4, 4, W - 8, H - 8);
|
||||
const s = String(text);
|
||||
let px = 30;
|
||||
do { x.font = `bold ${px}px "Comic Sans MS",cursive,Arial`; px -= 1; }
|
||||
while (px > 9 && x.measureText(s).width > W - PAD * 2);
|
||||
x.fillStyle = '#1c2f6a'; x.textAlign = 'center'; x.textBaseline = 'middle';
|
||||
x.fillText(s, W / 2, H / 2 + 1);
|
||||
const m = ctx.mat('#ffffff', 0.85); m.map = ctx.canvasTexture(cv); m.needsUpdate = true;
|
||||
return m;
|
||||
}
|
||||
|
||||
// A listening corner: a low bench + a boxy hi-fi + headphones on a stand.
|
||||
function listeningCorner(ctx, o = {}, r) {
|
||||
const g = new ctx.THREE.Group();
|
||||
@ -431,7 +511,7 @@ function ampStack(ctx, o = {}, r) {
|
||||
}
|
||||
|
||||
export const FITTINGS = {
|
||||
recordBin, crate, crateStack, listeningCorner,
|
||||
recordBin, crate, crateStack, listeningCorner, rummageBin,
|
||||
wallShelf, metalShelf, cubeShelf, bookshelf, vhsAisle, spinnerRack, arcadeCabinet,
|
||||
clothesRack,
|
||||
trestleTable, counter, glassCase,
|
||||
|
||||
@ -42,7 +42,11 @@
|
||||
// • type : any CITY_SPEC type or thriftgod alias (see theme.js); defaults to 'opshop'
|
||||
// • lot : { w, d } metres — room adapts to it; if absent, archetype ranges decide the size
|
||||
// • storeys: ceiling height driver (1 → 3.2m … up to 4.5m)
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, stock?: 'real', stockBase? }
|
||||
// opts: { archetype?: 'cosy'|'gallery'|'wide'|'hall'|'pokey', stockAdapter?, stock?: 'real', stockBase?,
|
||||
// fittingOpts? }
|
||||
// • fittingOpts (R39): { [fittingKind]: {…} } — per-build override of a recipe's own fitting opts.
|
||||
// `{ rummageBin: { dig: true } }` turns the op shop's rummage bin into a DIGGABLE bin (§39). Omit
|
||||
// ⇒ recipe defaults ⇒ byte-identical to pre-R39.
|
||||
// • stockBase (v5.0): which real-stock pack `stock:'real'` resolves. Omit → the town-wide packs
|
||||
// (`assets/models/`, today's behaviour). Pass `assets/stock_godverse/<godverseShopId>/` for a shop
|
||||
// with its own real crate. The pack cache is keyed by (type, base) — see stockpack.js + LANE_C_PUB §7.
|
||||
@ -193,7 +197,10 @@ export function buildInterior(shop, THREE, opts) {
|
||||
if (pack) adapter = makeStockAdapter(pack);
|
||||
else warnOnce(`stock=real: no pack for "${recipe.key}" @${opts.stockBase || 'default base'} (preload it) — using parody canvas`);
|
||||
}
|
||||
const lay = layout(ctx, { recipe, dims, shell, adapter, shop: norm });
|
||||
// [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.
|
||||
// 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 });
|
||||
|
||||
// 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
|
||||
|
||||
@ -331,7 +331,7 @@ function zoneCandidates(zone, W, D, doorCx, r) {
|
||||
}
|
||||
|
||||
// ── main entry ────────────────────────────────────────────────────────────────────────
|
||||
export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
export function layout(ctx, { recipe, dims, shell, adapter, shop, fittingOpts }) {
|
||||
const { W, D } = dims;
|
||||
const doorCx = shell.door.x;
|
||||
const grid = makeGrid(W, D);
|
||||
@ -374,7 +374,12 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
let n = Math.round(frange(rCount, spec.min, spec.max + 0.99) * Math.min(1.3, density));
|
||||
n = Math.max(spec.min, Math.min(spec.max, n));
|
||||
for (let i = 0; i < n; i++) {
|
||||
const f = buildFitting(spec.kind, ctx, {}, ctx.stream(`fit-${spec.kind}-${i}`));
|
||||
// [R39] fitting opts: recipe-declared (`spec.o`) with a per-build override (`opts.fittingOpts`,
|
||||
// keyed by fitting kind) on top. PURE DATA — reads no stream, so a spec that declares none is
|
||||
// byte-identical to pre-R39 and an override can never shift another fitting's seeded pick.
|
||||
const fo = fittingOpts && fittingOpts[spec.kind];
|
||||
const o = spec.o || fo ? { ...(spec.o || {}), ...(fo || {}) } : {};
|
||||
const f = buildFitting(spec.kind, ctx, o, ctx.stream(`fit-${spec.kind}-${i}`));
|
||||
let ok = false;
|
||||
if (spec.zone === 'wall' || f.wallMounted) {
|
||||
ok = placeAtWall(ctx, grid, f, wallSlots, rPlace, placed, spec);
|
||||
@ -383,6 +388,15 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
} else {
|
||||
ok = placeFree(ctx, grid, f, spec.zone, W, D, doorCx, rPlace, placed, spec);
|
||||
}
|
||||
// [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
|
||||
// dropped, exactly as before, and the density self-caps. Measured: the bin is appended last, so
|
||||
// its zone is the fullest it will ever be — `aisle` alone landed it in only 11/24 archetype×seed
|
||||
// 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.
|
||||
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; }
|
||||
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) */ }
|
||||
}
|
||||
}
|
||||
@ -466,6 +480,30 @@ function placeFree(ctx, grid, f, zone, W, D, doorCx, r, placed, spec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// [R39] Last resort for an opt-in spec (`anywhere: true`): the first free rect on a seeded sweep of
|
||||
// the open floor. Respects `reserved` through areaFree, so it can never land in the door corridor or
|
||||
// the strip in front of the till. Deterministic (the cell list is built in grid order, then shuffled
|
||||
// on `rPlace`). Never called unless the spec asked for it AND every zone already failed.
|
||||
function placeAnywhere(ctx, grid, f, W, D, r, placed, spec) {
|
||||
const cells = [];
|
||||
for (let cz = 1; cz < grid.rows - 1; cz++) for (let cx = 1; cx < grid.cols - 1; cx++) {
|
||||
if (grid.occ[cz * grid.cols + cx] !== 0 || grid.reserved[cz * grid.cols + cx]) continue;
|
||||
cells.push([(cx + 0.5) * grid.cw - W / 2, (cz + 0.5) * grid.cd - D / 2]);
|
||||
}
|
||||
shuffle(r, cells);
|
||||
for (const [x, z] of cells) {
|
||||
const ry = (r() - 0.5) * 0.4;
|
||||
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
||||
const rect = rectCells(grid, x, z, hw, hd);
|
||||
if (!areaFree(grid, rect)) continue;
|
||||
f.group.position.set(x, 0, z); f.group.rotation.y = ry;
|
||||
stampRect(grid, rect);
|
||||
placed.push({ fitting: f, x, z, ry, hw, hd, rect, priority: spec.priority, kind: spec.kind });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function placeAtWall(ctx, grid, f, wallSlots, r, placed, spec) {
|
||||
const backOff = f.wallMounted ? 0.06 : f.footprint.d / 2 + 0.05;
|
||||
for (const slot of wallSlots) {
|
||||
|
||||
@ -152,8 +152,16 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
||||
if (scatter) { // tables / pegboards: random small goods
|
||||
for (let i = 0; i < n; i++) {
|
||||
const s = 0.09 + r() * 0.11;
|
||||
const m = ctx.mat(pick(r, CARD), 0.85);
|
||||
if (r() < 0.5) { m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; }
|
||||
// [R39] `slot.pool` — keep every face on a POOLED texture at white, exactly as the spine and
|
||||
// shelf-box rows already do, so a scattered pile MERGES into the room's existing stock buckets
|
||||
// instead of minting one bucket (= one draw) per item. Measured on the rummage bin: 9 tinted
|
||||
// scatter items cost 9 draws in a room with no other scatter; pooled they cost 0–1. Absent ⇒
|
||||
// the pre-R39 tinted mix, and the stream shape is unchanged either way (both branches draw
|
||||
// tint, then the coin, then the face only when mapped) — trestle/pegboard are byte-identical.
|
||||
const tint = pick(r, CARD);
|
||||
const mapped = r() < 0.5 || !!slot.pool;
|
||||
const m = ctx.mat(slot.pool ? '#ffffff' : tint, 0.85);
|
||||
if (mapped) { m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true; }
|
||||
const b = ctx.box(s, slot.wall ? s * 1.4 : s, slot.wall ? 0.03 : s, m,
|
||||
slot.x + (r() - 0.5) * (run - s), slot.y + (slot.wall ? (r() - 0.5) * (clear - s) : s / 2),
|
||||
slot.z + (slot.wall ? 0.03 : (r() - 0.5) * (depth - s)), parent);
|
||||
|
||||
@ -16,9 +16,13 @@
|
||||
// floorBias : biased pick from carpet/lino/board floor names
|
||||
// clutter : 0..1 density multiplier (some shops crammed, some sparse)
|
||||
// counterPos : 'door' | 'forward' | 'corner' | 'back' — where the till lives
|
||||
// fittings : the mix. Each {kind, zone, min, max, priority}. Counts scale with clutter+area.
|
||||
// fittings : the mix. Each {kind, zone, min, max, priority, o?}. Counts scale with clutter+area.
|
||||
// priority: higher = more essential; flood-fill removes low-priority first if the
|
||||
// door→counter path is ever blocked. Counter is always highest.
|
||||
// o (R39, optional): opts handed to the fitting builder. Pure data, reads no stream.
|
||||
// A per-build override lives at buildInterior opts.fittingOpts[kind].
|
||||
// fallbackZones / anywhere (R39, optional): opt-in placement fallbacks for a fitting
|
||||
// the recipe PROMISES will be there. Absent ⇒ pre-R39 behaviour (drop it).
|
||||
// stockKind : what stock.js renders into this shop's carriers
|
||||
// signFlavour : seeded inside signage strings, themed
|
||||
|
||||
@ -60,6 +64,14 @@ const RECIPES = {
|
||||
{ kind: 'trestleTable', zone: 'aisle', min: 1, max: 2, priority: 3 },
|
||||
{ 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 },
|
||||
// [R39 — THE OP SHOP CAN'T BE DUG] the op shop's own floor bin. APPENDED LAST on purpose: the
|
||||
// recipe loop draws counts from `lay-count` and placements from `lay-place` in array order, so a
|
||||
// spec added at the end leaves every existing op-shop fitting's draws — and therefore its
|
||||
// placement — byte-identical, and only adds the tub. `dig:false` is the CONTENTS gate: off, this
|
||||
// is furniture (kind 'prop'); `{ rummageBin:{dig:true} }` in opts.fittingOpts makes every op shop
|
||||
// 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,
|
||||
fallbackZones: ['centre', 'window'], anywhere: true, o: { dig: false } },
|
||||
],
|
||||
stockKind: 'garments',
|
||||
signFlavour: ['ALL CLOTHES $2', 'BAG SALE SAT', 'BRIC-A-BRAC', 'PROCEEDS TO CHARITY', 'HALF PRICE'],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user