PROCITY/docs/LANES/LANE_C_NOTES.md
m3ultra fbc8ffa1c8 Lane C round-8: buy loop v0 + book/toy real stock (?stock=real)
C1 — buy loop v0 (new wallet.js): runtime-only economy — seeded cash
(~$60–200), buy()/broke-gate/in-memory bag, onChange for a cash chip.
World gen untouched (purchases never re-enter the plan/seeded build).
Test page: #cashChip HUD + I-toggle inventory + dig BUY IT button
(pull → bag, cash down, bin depletes); digSoak buys ≤2/visit.

C2 — book spines + toy boxes wired to ?stock=real. stockpack.js keys
each pack to a slot (record→sleeve, book→spine, toy→box); stock.js
shelfLine builds real atlas-UV cover planes for book/toy shelves
(static, no dig), fail-soft per pack. E's packs live: record 350/6,
book 311/4, toy 273/5 (items/atlases).

C3 — validated: draws ≤350 (record 41, book 42, toy 51), determinism
0 fails, leak-free (0 geo/tex delta ×20; buy-soak 12 visits/5 bought,
leakGeo0/tex0), reload resets wallet, flag-off + ?noassets untouched,
qa.sh --strict GREEN 5/5. Shot: docs/shots/laneC/buyloop_r8.jpg.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 04:33:00 +10:00

132 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# LANE C — cross-lane notes (PROCITY-C)
## → Lane F: buy loop v0 + book/toy packs — two warn-level smokes (round-8 C1/C2 landed)
**Buy loop** is a runtime-only wallet in `web/js/interiors/wallet.js` — pure, headless-testable, no DOM:
```js
import { createWallet } from './web/js/interiors/wallet.js';
const w = createWallet(1990);
const start = w.start(); // seeded ~$60200 (deterministic per seed)
w.buy({ title:'X', artist:'Y', price: 25 }); // true; cash -= 25; bag +1
w.buy({ price: start }); // false when it exceeds remaining cash (broke) — no state change
// assert: w.count() === 1 && w.cash() === start - 25 && w.canBuy(w.cash()+1) === false
```
- Deterministic start per seed; `buy` returns `false` unchanged when broke; `onChange(fn)` fires on every
buy (so F's street shell can bind ONE wallet to a cash chip across street + interiors).
- The buy **UI** (cash chip, inventory panel, dig BUY button) is **test-page-local** (`interior_test.html`).
The reusable economy is `wallet.js`; render it wherever. World gen is untouched — the wallet never writes
back into the plan or the seeded room build, so goldens/draw-counts are unaffected by purchases.
**Book/toy packs** now feed `?stock=real` too (E shipped `stock_book_*` / `stock_toy_*`). Smoke:
- Boot `?stock=real`, enter a **book** or **toy** shop, assert real stock present: same test as records —
`let n=0; room.group.traverse(o=>{ if(o.userData?.isStock && o.material?.map) n++ }); assert n>0`. Book
spines land on bookshelves, toy boxes on cube shelves (**static shelf stock, no dig** for those).
- **Fail-soft per pack**: if a pack's index/atlas is missing, that type falls back to parody canvas + one
`console.warn`; records/other types unaffected. `?noassets` untouched.
- **drawSweep still ≤350**: record 41, book 42, toy 51 (each atlas = one batched draw; packs are 46 atlases).
## → Lane F: `?stock=real` smoke for the strict harness (round-7 C1 landed)
`?stock=real` is wired (record shops; book/toy fail-soft if E ships those packs). Smoke recipe:
- Preload once: `import { preloadStockPack } from interiors.js; await preloadStockPack('record')`.
- Boot `?stock=real`, enter a record shop, **assert real sleeves present**: a real sleeve is a `Mesh`
with `userData.isStock`, geometry `PlaneGeometry`/`BufferGeometry` (batched), on a shared **atlas**
material (`material.map` = the pack atlas, not a per-item canvas). Simplest assert: after build,
`let n=0; room.group.traverse(o=>{ if(o.userData?.isStock && o.material?.map) n++ }); assert n>0` — with
the pack loaded the record bins' sleeves batch to **one** atlas mesh (parody canvas would be ~6 card
meshes). Or open the dig (`?dig=1&stock=real`) and assert a pulled sleeve's panel title is a pack title.
- **Fail-soft**: pack missing → parody canvas + one `console.warn` (no throw). `?noassets` unaffected.
- **drawSweep still ≤350** with `?stock=real` (record 59 draws — the atlas batches all sleeves).
## → Lane E: stock-pack contract CONFIRMED (round-7 C1) — build the real pack to this, unchanged
Your proposed index schema is good — **confirmed as-is, no field changes**. Building `?stock=real`
against your staged 24-sleeve sample now (`web/assets/models/stock_record_{index.json,atlas_00.webp}`);
the real covers drop in unchanged. The contract C consumes:
```jsonc
// stock_<type>_index.json (type ∈ record | book | toy ; record is the acceptance bar)
{
"version": 1,
"atlas_px": 2048,
"cell": 256, // informational; C reads uv, not cell
"atlases": ["stock_record_atlas_00.webp", ...], // 1+; may be depot: or assets/models/ resolved
"items": [
{ "id": "rec_0000",
"title": "Neon Suburbs", // parody-transformed (no-real-trademarks law applies to metadata)
"artist": "Trev Wollemi",
"price": 39, // integer dollars — C shows "$39"
"price_band": "collector", // bargain|standard|collector|grail — C may tint the sticker
"atlas": "stock_record_atlas_00.webp", // which entry in `atlases`
"uv": [0.0, 0.0, 0.125, 0.125] } // [u0,v0,u1,v1] cover rect, ORIGIN TOP-LEFT
]
}
```
Agreements / who-does-what:
- **UV origin is top-left (image-natural) — C flips V for WebGL** (`v_gl = 1 - v`). Keep your index
image-natural; the consumer handles the flip. (Sample verified against this.)
- **Resolution**: C loads `stock_<type>_index.json` from a base (default `assets/models/`, i.e. your
staged path — same-origin, works offline). Atlas filenames in the index resolve against the same base;
if you publish atlases to the depot, give me a `"depot": true` flag or depot-prefixed names and I'll
route through the `depot:` loader. For now the sample loads local.
- **Seeded pick per bin is mine** (same `shop.seed` → same crate contents). I only need *enough* items —
24 repeats across bins (fine for testing); the 300 pack fixes variety.
- **Batching**: because every sleeve samples one shared atlas material (per-item UV baked into geometry),
all real sleeves in a room merge to **one draw** — the ≤350 law holds. Please keep it **one atlas per
pack** where you can (or few); each extra atlas = one more draw.
- **Fail-soft**: index/atlas missing or `?stock` off → parody canvas (warn once). `?noassets` untouched.
- **Per-type packs**: name them `stock_book_*`, `stock_toy_*` if you build them; I key by shop type and
fail-soft per type, so record-only is fine — book/toy are a bonus.
You flagged needing a Postgres DSN (from John/Fable) to swap synthetic covers for real Discogs images —
that's external and **does not block my consumer**; ping me only if the schema needs to change.
## → Lane F: interior ≤350-draw assertion for the harness (round-6 C1)
C1 batching landed — worst room is now **313 draws GLB-on** / 152 GLB-off (was 1,245). The test page
exposes the assertion for your harness:
- `window.PROCITY_C.drawSweep({ glb: true|false })``{ worst, worstAt, pass, law: 350, perType }`.
Builds every shop type × archetype at seed 1990, renders each, returns the worst `renderer.info.render.calls`.
It's already folded into the interior soak's PASS/FAIL. For your smoke/soak gate, call it with
`glb:true` (the tighter case) and assert `pass` (`worst <= 350`). Headless-safe, ~1s.
- The draw law is a Lane-C constant (`DRAW_LAW = 350` in interior_test.html) so it's one number to bump
if the budget ever changes. Ping me if you'd rather it read from a shared config.
## → Fable / Lane F: v1.1 is ready (round-5 C1 done)
**C1 committed.** Lane E's re-exports are mapped + validated in `web/js/interiors/glb.js`:
- `record_crate` (fixed) → record bins/crates upgrade to plastic crates with the procedural sleeves.
- `counter → counter_till` → the real ~1.6 m counter-with-till drops in; footprint-fit + floor-plant
unchanged; **keeper-stand pose still valid** (it's a room-local pose, not tied to the counter geometry).
- **No double till**: my separate `cash_register` counter-top attach is dropped whenever `counter_till`
is live (that GLB has its own till). `milkshake_mixer` still on the milk-bar bench.
- GLB soak: `0 throws / 0 path / 0 determinism / leak-free / worst 10.4 ms`, **0 rooms with a visible
primitive till**. Primitive path byte-unchanged (C1 touched only `glb.js`). `qa.sh --strict` green.
**→ You can tag v1.1** per ROUND5 §Lane C acceptance ("bins + counter GLB-live, no double till, soak
green → F tags v1.1"). Evidence: [`docs/shots/laneC/glb_v1_1_r5.jpg`](../shots/laneC/glb_v1_1_r5.jpg),
report in [`LANE_C_GLB_VALIDATION.md`](LANE_C_GLB_VALIDATION.md).
## → Lane F: input/mode hook I need for `?dig=1` (round-5 C2)
The crate-riffle (dig) is interior-mode-only and needs the shell to tell an interior when the player
interacts with a fitting. Proposed seam (you own the shell/`interior_mode.js`):
- On the player pressing the interact key while aimed at a fitting whose `userData.kind === 'bin'`
(record bins expose `places:[group]` with `userData = { kind:'bin', interactable:true }`), call a
hook the interior optionally provides: **`room.onInteract?.(hitObject)`** — where `room` is the object
returned by `buildInterior(...)`. If it returns truthy, the shell should suppress normal movement /
pointer-lock until the interior calls back that the dig closed (I'll expose `room.digActive` +
fire a `procity:digClosed` event, or a callback you pass in — your call on the exact contract).
- Gate the whole thing on `?dig=1` (I read the flag inside the interiors lib; when off, `onInteract`
is absent and nothing changes — flag-off boot stays byte-identical).
I'm building the dig against my own test page (`web/interior_test.html`) with a direct click handler in
the meantime, so it's testable without the shell. Tell me if you'd prefer a different hook shape and
I'll match it. — PROCITY-C