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>
This commit is contained in:
parent
f952f513d0
commit
fbc8ffa1c8
@ -3,7 +3,49 @@
|
|||||||
*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-07-14 (round 7) · owner: PROCITY-C · reviewer: Fable
|
Last updated: 2026-07-15 (round 8) · owner: PROCITY-C · reviewer: Fable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Update 2026-07-15 (round 8) — the town comes alive: buy loop v0 + book/toy real stock
|
||||||
|
|
||||||
|
Round-8 §Lane C, theme "the town comes alive". Two deliverables + a verify pass.
|
||||||
|
|
||||||
|
**C1 — buy loop v0** (new `web/js/interiors/wallet.js`). A shallow, **runtime-only** economy: seeded
|
||||||
|
starting cash, purchases, an in-memory bag. Reload resets; world generation is **untouched** (purchases
|
||||||
|
never enter the plan or the seeded room build — the wallet is a separate object the shell reads, never
|
||||||
|
writes back into gen).
|
||||||
|
- `createWallet(seed)` → `{ cash, start, canBuy, buy(offer), inventory, count, onChange }`. Start is
|
||||||
|
seeded ~$60–200 (`mulberry32`). `buy` deducts + records on success, **returns false unchanged if broke**.
|
||||||
|
`onChange` lets F's street shell subscribe the same wallet to a cash chip.
|
||||||
|
- **Test page** (`interior_test.html`): a `#cashChip` HUD (`$163 · 🛍 N`, street + interior) + an
|
||||||
|
inventory panel toggled with **I**. The dig's pull card gets a **BUY IT** button (price + band); buying
|
||||||
|
pulls the sleeve into the bag, decrements cash, updates the chip, and **depletes that bin** (bought bins
|
||||||
|
offer fewer sleeves on re-riffle). Broke → the button no-ops (can't buy). `digSoak` now buys up to 2/visit.
|
||||||
|
- Offer shape is the dig offer (`{a,t,price,s}`) OR a pack item (`{title,artist,price,price_band}`) — one
|
||||||
|
`buy()` eats both.
|
||||||
|
|
||||||
|
**C2 — book spines + toy boxes wired to `?stock=real`.** E shipped the generalized packs
|
||||||
|
(`stock_book_*` 311 items / 4 atlases, `stock_toy_*` 273 items / 5 atlases, alongside `stock_record_*`
|
||||||
|
350/6). `stockpack.js` now keys each pack to its slot via `SLOT_FOR = {record:'sleeve', book:'spine',
|
||||||
|
toy:'box'}`; `stock.js shelfLine` consults the adapter and builds real cover planes for book spines
|
||||||
|
(bookshelves) and toy boxes (cube shelves) — **static shelf stock, no dig** for those, fail-soft per pack.
|
||||||
|
Every real mesh samples its atlas material with a baked per-item UV, so a whole shelf still batches to one
|
||||||
|
draw per atlas.
|
||||||
|
|
||||||
|
**C3 — validated** (all three packs live, `?stock=real`):
|
||||||
|
- **Draws ≤350** (the law): record **41**, book **42**, toy **51** worst over seeds {1990,7,42(,101)}.
|
||||||
|
- **Determinism**: same seed → identical draw calls + mesh count, all three types (0 fails).
|
||||||
|
- **Leak-free**: 0 geometry/texture delta over 20 build/dispose cycles; **buy-soak** 12 record-dig visits,
|
||||||
|
5 bought (wallet depletes to broke → exercises the can't-buy path), `leakGeo 0 / leakTex 0`; resting geo
|
||||||
|
back to baseline after close. Atlas textures shared/cached (not per-room); per-item geo ctx-tracked & disposed.
|
||||||
|
- **Reload resets** the wallet (fresh boot → seeded start again); **flag-off + `?noassets` untouched**.
|
||||||
|
- Shot: [buyloop_r8.jpg](docs/shots/laneC/buyloop_r8.jpg) — toy boxes, book spines, record dig → BUY card.
|
||||||
|
|
||||||
|
**→ F**: two new warn-level smokes (hooks in [LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md)): (1) **buy loop** —
|
||||||
|
`createWallet` deduct/broke/inventory is pure and headless-testable; (2) **book/toy packs** — enter book or
|
||||||
|
toy shop with `?stock=real`, assert `isStock` meshes on a shared atlas material (drawSweep ≤350). Buy UI is
|
||||||
|
test-page-local; the reusable piece for the street shell is `wallet.js` + `onChange`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,31 @@
|
|||||||
# LANE C — cross-lane notes (PROCITY-C)
|
# 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 ~$60–200 (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 4–6 atlases).
|
||||||
|
|
||||||
## → Lane F: `?stock=real` smoke for the strict harness (round-7 C1 landed)
|
## → 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:
|
`?stock=real` is wired (record shops; book/toy fail-soft if E ships those packs). Smoke recipe:
|
||||||
|
|||||||
BIN
docs/shots/laneC/buyloop_r8.jpg
Normal file
BIN
docs/shots/laneC/buyloop_r8.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 160 KiB |
@ -71,12 +71,35 @@ import * as THREE from 'three';
|
|||||||
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
|
||||||
import { buildInterior, SHOP_TYPES, ARCHETYPE_KEYS, preloadStockPack, getStockPack, makeStockAdapter } from './js/interiors/interiors.js';
|
import { buildInterior, SHOP_TYPES, ARCHETYPE_KEYS, preloadStockPack, getStockPack, makeStockAdapter } from './js/interiors/interiors.js';
|
||||||
import { createDig, binSeed } from './js/interiors/dig.js';
|
import { createDig, binSeed } from './js/interiors/dig.js';
|
||||||
|
import { createWallet } from './js/interiors/wallet.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';
|
||||||
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);
|
const realAdapterFor = (type) => (STOCK_REAL ? makeStockAdapter(getStockPack(type)) : null);
|
||||||
|
|
||||||
|
// ── buy loop v0: in-memory wallet + cash HUD + inventory (I). F wires the same wallet on the street. ──
|
||||||
|
const wallet = createWallet(1990);
|
||||||
|
const boughtByBin = {}; // bin key → count sold, so a re-dug bin depletes
|
||||||
|
const cashChip = document.createElement('div'); cashChip.id = 'cashChip';
|
||||||
|
const invPanel = document.createElement('div'); invPanel.id = 'invPanel';
|
||||||
|
const walletCss = document.createElement('style'); walletCss.textContent = `
|
||||||
|
#cashChip{position:fixed;top:12px;right:14px;z-index:70;background:rgba(24,20,14,.92);border:1px solid #6a5a38;border-radius:8px;padding:8px 13px;font:700 15px "Courier New",monospace;color:#3dff8b}
|
||||||
|
#invPanel{position:fixed;top:52px;right:14px;z-index:70;width:238px;max-height:62vh;overflow:auto;background:rgba(24,20,14,.95);border:1px solid #4a4030;border-radius:8px;padding:10px 12px;font:12px "Courier New",monospace;color:#d8d8e0;display:none}
|
||||||
|
#invPanel h4{margin:0 0 6px;color:#ffd75e}
|
||||||
|
#invPanel .row{display:flex;justify-content:space-between;gap:8px;padding:2px 0;border-bottom:1px solid #2a2418}`;
|
||||||
|
document.head.appendChild(walletCss); document.body.append(cashChip, invPanel);
|
||||||
|
function updateCash() { cashChip.textContent = `$${wallet.cash()} · 🛍 ${wallet.count()}`; }
|
||||||
|
function renderInv() {
|
||||||
|
const inv = wallet.inventory();
|
||||||
|
invPanel.innerHTML = `<h4>INVENTORY (${inv.length})</h4>` + (inv.length
|
||||||
|
? inv.map(it => `<div class="row"><span>${it.artist ? it.artist + ' — ' : ''}${it.title}</span><span>$${it.price}</span></div>`).join('')
|
||||||
|
: '<div style="color:#8a8a9a">empty — walk to a bin and press E, then BUY</div>');
|
||||||
|
}
|
||||||
|
wallet.onChange(() => { updateCash(); if (invPanel.style.display === 'block') renderInv(); });
|
||||||
|
updateCash();
|
||||||
|
addEventListener('keydown', e => { if (e.code === 'KeyI' && !e.target.matches('input,select')) { const on = invPanel.style.display === 'block'; invPanel.style.display = on ? 'none' : 'block'; if (!on) renderInv(); } });
|
||||||
|
|
||||||
// ── renderer / scene / camera ──────────────────────────────────────────────
|
// ── renderer / scene / camera ──────────────────────────────────────────────
|
||||||
const app = document.getElementById('app');
|
const app = document.getElementById('app');
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
@ -347,21 +370,42 @@ function openDigOn(bin) {
|
|||||||
if (controls.isLocked) controls.unlock();
|
if (controls.isLocked) controls.unlock();
|
||||||
const p = new THREE.Vector3(); bin.getWorldPosition(p);
|
const p = new THREE.Vector3(); bin.getWorldPosition(p);
|
||||||
const key = Math.round(p.x * 100) + '_' + Math.round(p.z * 100); // stable per-bin key (deterministic position)
|
const key = Math.round(p.x * 100) + '_' + Math.round(p.z * 100); // stable per-bin key (deterministic position)
|
||||||
dig.open({ seed: binSeed(curSeed, key), count: 16, shopName: current.recipe.label, shop: { type: current.dims.type },
|
const sold = boughtByBin[key] || 0; // the bin depletes as you buy from it
|
||||||
stockAdapter: realAdapterFor(current.dims.type), getCash: () => 50, onBuy: () => true, onClose: () => {} });
|
dig.open({ seed: binSeed(curSeed, key), count: Math.max(3, 16 - sold), shopName: current.recipe.label, shop: { type: current.dims.type },
|
||||||
|
stockAdapter: realAdapterFor(current.dims.type),
|
||||||
|
getCash: wallet.cash,
|
||||||
|
onBuy: (o) => { const ok = wallet.buy(o); if (ok) boughtByBin[key] = (boughtByBin[key] || 0) + 1; return ok; },
|
||||||
|
onClose: () => {} });
|
||||||
}
|
}
|
||||||
if (DIG_ON) {
|
if (DIG_ON) {
|
||||||
addEventListener('keydown', e => { if (e.code === 'KeyE' && current && (!dig || !dig.active)) { const b = binUnderAim(); if (b) openDigOn(b); } });
|
addEventListener('keydown', e => { if (e.code === 'KeyE' && current && (!dig || !dig.active)) { const b = binUnderAim(); if (b) openDigOn(b); } });
|
||||||
document.getElementById('hint').innerHTML += ' · <b>E</b> riffle a record bin';
|
document.getElementById('hint').innerHTML += ' · <b>E</b> riffle a record bin · <b>I</b> bag';
|
||||||
}
|
}
|
||||||
// enter+leave a dig per record room, assert leak-free (round-5 acceptance)
|
// enter+leave a dig per record room, BUYING 2 items per visit, assert leak-free (round-8 buy loop)
|
||||||
async function digSoak(N = 15) {
|
async function digSoak(N = 15) {
|
||||||
if (!dig) dig = createDig(THREE, renderer);
|
if (!dig) dig = createDig(THREE, renderer);
|
||||||
dig.open({ seed: 1, count: 16 }); renderer.render(dig.scene, dig.camera); dig.close(); renderer.render(scene, camera);
|
const richWallet = createWallet(7); // fresh rich-ish wallet so buys land during the soak
|
||||||
|
let topUps = 0;
|
||||||
|
const buyer = (o) => { if (o.price > richWallet.cash()) { topUps++; return false; } return richWallet.buy(o); };
|
||||||
|
dig.open({ seed: 1, count: 16, getCash: richWallet.cash, onBuy: buyer }); renderer.render(dig.scene, dig.camera); dig.close(); renderer.render(scene, camera);
|
||||||
const bG = renderer.info.memory.geometries, bT = renderer.info.memory.textures;
|
const bG = renderer.info.memory.geometries, bT = renderer.info.memory.textures;
|
||||||
for (let i = 0; i < N; i++) { dig.open({ seed: (i * 7919 + 1) >>> 0, count: 10 + (i % 9) }); renderer.render(dig.scene, dig.camera); dig.close(); }
|
const cv = renderer.domElement;
|
||||||
|
let bought = 0;
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
dig.open({ seed: (i * 7919 + 1) >>> 0, count: 10 + (i % 9), getCash: richWallet.cash, onBuy: buyer });
|
||||||
|
for (let k = 0; k < 8; k++) dig.update(0.03);
|
||||||
|
for (let b = 0; b < 2; b++) { // buy 2 per visit via the real pull+BUY path
|
||||||
|
cv.dispatchEvent(new PointerEvent('pointerdown', { clientX: innerWidth / 2, clientY: innerHeight / 2, bubbles: true }));
|
||||||
|
cv.dispatchEvent(new PointerEvent('pointerup', { clientX: innerWidth / 2, clientY: innerHeight / 2, bubbles: true }));
|
||||||
|
for (let k = 0; k < 4; k++) dig.update(0.03);
|
||||||
|
const btn = document.querySelector('.pcdg-panel button');
|
||||||
|
if (btn && !btn.disabled) { const before = richWallet.count(); btn.click(); if (richWallet.count() > before) bought++; }
|
||||||
|
for (let k = 0; k < 4; k++) dig.update(0.03);
|
||||||
|
}
|
||||||
|
renderer.render(dig.scene, dig.camera); dig.close();
|
||||||
|
}
|
||||||
renderer.render(scene, camera);
|
renderer.render(scene, camera);
|
||||||
return { rooms: N, leakGeo: renderer.info.memory.geometries - bG, leakTex: renderer.info.memory.textures - bT };
|
return { rooms: N, bought, leakGeo: renderer.info.memory.geometries - bG, leakTex: renderer.info.memory.textures - bT };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── loop ───────────────────────────────────────────────────────────────────────
|
// ── loop ───────────────────────────────────────────────────────────────────────
|
||||||
@ -375,7 +419,7 @@ 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,
|
||||||
DIG_ON, digSoak, openDigOn, binUnderAim, get dig() { return dig; } };
|
DIG_ON, STOCK_REAL, digSoak, openDigOn, binUnderAim, get dig() { return dig; }, wallet, preloadStockPack, getStockPack, makeStockAdapter };
|
||||||
|
|
||||||
rebuild();
|
rebuild();
|
||||||
frame();
|
frame();
|
||||||
|
|||||||
@ -145,6 +145,9 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
|||||||
const scatter = slot.scatter;
|
const scatter = slot.scatter;
|
||||||
const n = Math.max(1, slot.count || 4);
|
const n = Math.max(1, slot.count || 4);
|
||||||
const pool = kind === 'spine' ? facePool(ctx, 'spine', r) : facePool(ctx, kind === 'snack' ? 'snack' : 'card', r);
|
const pool = kind === 'spine' ? facePool(ctx, 'spine', r) : facePool(ctx, kind === 'snack' ? 'snack' : 'card', r);
|
||||||
|
// ?stock=real: book spines / toy boxes pull real covers from the pack (shared atlas material → batched).
|
||||||
|
const src = !scatter && opts.adapter && opts.adapter(opts.shop, kind);
|
||||||
|
const pack = src && src.real ? src.pack : null;
|
||||||
|
|
||||||
if (scatter) { // tables / pegboards: random small goods
|
if (scatter) { // tables / pegboards: random small goods
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
@ -163,11 +166,19 @@ 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;
|
||||||
for (let i = 0; i < n; i++) {
|
for (let i = 0; i < n; i++) {
|
||||||
const m = ctx.mat('#ffffff', 0.7); m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true;
|
|
||||||
const bh = clear * (0.8 + r() * 0.2);
|
const bh = clear * (0.8 + r() * 0.2);
|
||||||
const b = ctx.box(step * 0.86, bh, depth, m, slot.x - run / 2 + (i + 0.5) * step, slot.y + bh / 2, slot.z, parent);
|
const x = slot.x - run / 2 + (i + 0.5) * step;
|
||||||
|
let b;
|
||||||
|
if (pack) { // real book spine: cover plane at the shelf front
|
||||||
|
b = realSleeveMesh(ctx, pack, pick(r, pack.items), step * 0.86, bh);
|
||||||
|
b.position.set(x, slot.y + bh / 2, slot.z + depth / 2 - 0.01);
|
||||||
|
parent.add(b);
|
||||||
|
} else {
|
||||||
|
const m = ctx.mat('#ffffff', 0.7); m.map = pick(r, pool); m.color.set('#ffffff'); m.needsUpdate = true;
|
||||||
|
b = ctx.box(step * 0.86, bh, depth, m, x, slot.y + bh / 2, slot.z, parent);
|
||||||
|
b.userData.isStock = true;
|
||||||
|
}
|
||||||
if (slot.lean && i > n - 3) b.rotation.z = 0.18; // a couple flopped over
|
if (slot.lean && i > n - 3) b.rotation.z = 0.18; // a couple flopped over
|
||||||
b.userData.isStock = true;
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -191,11 +202,17 @@ function shelfLine(ctx, parent, slot, opts, r) {
|
|||||||
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 bd = Math.min(depth, kind === 'snack' ? 0.1 : 0.26);
|
const bd = Math.min(depth, kind === 'snack' ? 0.1 : 0.26);
|
||||||
const m = ctx.mat('#ffffff', 0.8);
|
const x = slot.x - run / 2 + (i + 0.5) * step;
|
||||||
m.map = faceTex(ctx, kind, opts, r); m.color.set('#ffffff'); m.needsUpdate = true;
|
if (pack) { // real toy box: cover plane at the carton front
|
||||||
const mesh = ctx.box(bw * 0.88, bh, bd, m,
|
const b = realSleeveMesh(ctx, pack, pick(r, pack.items), bw * 0.88, bh);
|
||||||
slot.x - run / 2 + (i + 0.5) * step, slot.y + bh / 2, slot.z + depth / 2 - bd / 2 - 0.01, parent);
|
b.position.set(x, slot.y + bh / 2, slot.z + depth / 2 - 0.01);
|
||||||
mesh.userData.isStock = true;
|
parent.add(b);
|
||||||
|
} else {
|
||||||
|
const m = ctx.mat('#ffffff', 0.8);
|
||||||
|
m.map = faceTex(ctx, kind, opts, r); m.color.set('#ffffff'); m.needsUpdate = true;
|
||||||
|
const mesh = ctx.box(bw * 0.88, bh, bd, m, x, slot.y + bh / 2, slot.z + depth / 2 - bd / 2 - 0.01, parent);
|
||||||
|
mesh.userData.isStock = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -54,11 +54,16 @@ async function buildPack(type, idx, base, THREE) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The stockAdapter for ?stock=real. Returns the pack POOL for a sleeve slot; consumers (stock.binFan /
|
// Each pack type feeds exactly one stock slot kind so a record shop's pack never lands on a bric-a-brac
|
||||||
// dig.js) pick a seeded subset per bin. Non-sleeve kinds → null (fall through to procedural).
|
// shelf: record→sleeve (bins), book→spine (bookshelves), toy→box (cube shelves).
|
||||||
|
const SLOT_FOR = { record: 'sleeve', book: 'spine', toy: 'box' };
|
||||||
|
|
||||||
|
// 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.
|
||||||
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;
|
||||||
return (shop, slotKind) => (slotKind === 'sleeve' ? { real: true, pack } : null);
|
const want = SLOT_FOR[pack.type] || 'sleeve';
|
||||||
|
return (shop, slotKind) => (slotKind === want ? { real: true, pack } : 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
|
||||||
|
|||||||
33
web/js/interiors/wallet.js
Normal file
33
web/js/interiors/wallet.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// PROCITY Lane C — buy loop v0 (round 8). In-memory wallet: seeded starting cash, purchases, inventory.
|
||||||
|
// RUNTIME state only — reload resets, world generation is untouched (purchases never enter the plan or
|
||||||
|
// the seeded room build). One wallet is shared across dig sessions + shops so F's shell can render the
|
||||||
|
// same cash chip on the street. Economy is deliberately shallow (no persistence, no backend) — v0.
|
||||||
|
|
||||||
|
import { mulberry32 } from '../core/prng.js';
|
||||||
|
|
||||||
|
export function createWallet(seed = 1) {
|
||||||
|
const r = mulberry32(seed >>> 0);
|
||||||
|
const start = 60 + ((r() * 140) | 0); // seeded starting cash ~$60–200
|
||||||
|
let cash = start;
|
||||||
|
const inv = [];
|
||||||
|
const listeners = new Set();
|
||||||
|
const notify = () => listeners.forEach(fn => { try { fn(); } catch (e) {} });
|
||||||
|
return {
|
||||||
|
cash: () => cash,
|
||||||
|
start: () => start,
|
||||||
|
canBuy: (price) => (price || 0) <= cash,
|
||||||
|
// buy(offer) — offer is a dig offer ({a:artist, t:title, price, ...}) or a pack item ({title,artist,price}).
|
||||||
|
// Returns false (no state change) if broke. Deducts + records on success.
|
||||||
|
buy(o) {
|
||||||
|
const price = (o && o.price) || 0;
|
||||||
|
if (price > cash) return false;
|
||||||
|
cash -= price;
|
||||||
|
inv.push({ title: o.t || o.title || '?', artist: o.a || o.artist || '', price, band: (o.s || o.price_band || '') });
|
||||||
|
notify();
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
inventory: () => inv.slice(),
|
||||||
|
count: () => inv.length,
|
||||||
|
onChange(fn) { listeners.add(fn); return () => listeners.delete(fn); },
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user