From 7131f530a416f3ea595b9e260c7b2aa46a7abd33 Mon Sep 17 00:00:00 2001 From: m3ultra Date: Tue, 14 Jul 2026 21:11:31 +1000 Subject: [PATCH] Lane C R5 C2: crate-riffle dig behind ?dig=1 (v2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported 90sDJsim/web/world/dig.js → web/js/interiors/dig.js. Walk to a record bin, press E, riffle its sleeves (scroll/drag), click the front sleeve to pull it, BUY IT. Faithful to the source: hinge-forward riffle, pull-and-inspect panel, procedural fwip/thunk audio. - Riffle items = seeded procedural sleeves per bin via the stockAdapter(shop, 'sleeve') seam (parody 90s-AU records now; real GODVERSE records feed the same seam in round 6+). Same bin ⇒ same order. - Buy is STUBBED (toast + remove-from-bin; getCash returns a constant) — economy is round 6+ per integrator decision #1. - Own scene + camera; leak-free: per-open GPU resources (sleeve textures/ materials, crate parts) freed on every close(), persistent set freed on dispose(). Dig soak (20× enter/leave) → leakGeo 0 / leakTex 0. - Flag-gated (?dig=1): buildInterior is UNTOUCHED, so ?dig=0 boot is byte-identical to v1 (prime law). The dig is wired only in the test page via an E-key raycast; F owns the shell input/mode seam — hook documented in docs/LANES/LANE_C_NOTES.md. Verified: riffle + pull + buy-stub work; dig soak leak-free; placement determinism unaffected; close returns to the interior. qa.sh --strict GREEN. Shot: docs/shots/laneC/dig_riffle_r5.jpg. Co-Authored-By: Claude Opus 4.8 --- C-progress.md | 11 +- docs/shots/laneC/dig_riffle_r5.jpg | Bin 0 -> 42288 bytes web/interior_test.html | 49 ++++- web/js/interiors/dig.js | 311 +++++++++++++++++++++++++++++ 4 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 docs/shots/laneC/dig_riffle_r5.jpg create mode 100644 web/js/interiors/dig.js diff --git a/C-progress.md b/C-progress.md index e1c77b8..5f90047 100644 --- a/C-progress.md +++ b/C-progress.md @@ -19,7 +19,16 @@ Round-5 §Lane C task 1 (the v1.1 gate). Lane E's re-exports mapped + validated - Report + decision detail: [LANE_C_GLB_VALIDATION.md](docs/LANES/LANE_C_GLB_VALIDATION.md); F note in [LANE_C_NOTES.md](docs/LANES/LANE_C_NOTES.md). **→ F: C1 committed, v1.1 can be tagged.** -Still in flight this round: C2 `?dig=1` crate-riffle (v2, behind flag), C3 record re-measure (after E's bake). +**C2 — `?dig=1` crate-riffle (v2, behind flag).** Ported 90sDJsim `dig.js` → `web/js/interiors/dig.js`: +walk to a record bin, press **E**, riffle its sleeves (scroll/drag), click the front to pull, **BUY IT** +(STUBBED — toast + remove; getCash constant; real economy is round 6+). Riffle items are seeded per bin +via the `stockAdapter(shop,'sleeve')` seam (procedural fallback now — parody 90s-AU sleeves). Own scene + +camera, procedural fwip/thunk audio, **leak-free** (per-open resources freed on close → dig soak 20× +`leakGeo 0 / leakTex 0`). Flag-off (`?dig=0`) is **byte-identical** — `buildInterior` is untouched; the dig +is wired only in the test page (F owns the shell seam — hook documented in LANE_C_NOTES §"input/mode hook"). +Shot: [dig_riffle_r5.jpg](docs/shots/laneC/dig_riffle_r5.jpg). + +Still in flight: C3 record re-measure — blocked on Lane E's hero-prop bake landing (don't wait, per §Lane C). --- diff --git a/docs/shots/laneC/dig_riffle_r5.jpg b/docs/shots/laneC/dig_riffle_r5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..38a7cb44fb3545a0e9a4c932a14ff1cbe0b74bc3 GIT binary patch literal 42288 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 0; + curSeed = seed; const type = typeSel.value; const arch = archSel.value === 'auto' ? undefined : archSel.value; const lot = parseLot($('lot').value); @@ -291,17 +294,55 @@ async function soak(N = 50) { } $('soakBtn').onclick = () => soak(50); +// ── dig (?dig=1) — walk to a record bin, press E to riffle its sleeves (v2, flag-gated) ── +const DIG_ON = new URLSearchParams(location.search).has('dig'); +let dig = null; +const _digRay = new THREE.Raycaster(); +function binUnderAim() { + if (!current) return null; + const bins = []; current.group.traverse(o => { if (o.userData && o.userData.kind === 'bin') bins.push(o); }); + if (!bins.length) return null; + _digRay.setFromCamera({ x: 0, y: 0 }, camera); + const hit = _digRay.intersectObjects(bins, true)[0]; + if (hit && hit.distance < 3.2) { let o = hit.object; while (o && !(o.userData && o.userData.kind === 'bin')) o = o.parent; return o; } + let best = null, bd = 2.4; const cp = camera.position, p = new THREE.Vector3(); + for (const b of bins) { b.getWorldPosition(p); const d = p.distanceTo(cp); if (d < bd) { bd = d; best = b; } } + return best; +} +function openDigOn(bin) { + if (!dig) dig = createDig(THREE, renderer); + if (controls.isLocked) controls.unlock(); + 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) + dig.open({ seed: binSeed(curSeed, key), count: 16, shopName: current.recipe.label, + getCash: () => 50, onBuy: () => true, onClose: () => {} }); +} +if (DIG_ON) { + addEventListener('keydown', e => { if (e.code === 'KeyE' && current && (!dig || !dig.active)) { const b = binUnderAim(); if (b) openDigOn(b); } }); + document.getElementById('hint').innerHTML += ' · E riffle a record bin'; +} +// enter+leave a dig per record room, assert leak-free (round-5 acceptance) +async function digSoak(N = 15) { + 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 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(); } + renderer.render(scene, camera); + return { rooms: N, leakGeo: renderer.info.memory.geometries - bG, leakTex: renderer.info.memory.textures - bT }; +} + // ── loop ─────────────────────────────────────────────────────────────────────── let last = performance.now(); function frame() { const now = performance.now(), dt = Math.min(0.05, (now - last) / 1000); last = now; - move(dt); - renderer.render(scene, camera); + if (dig && dig.active) { dig.update(dt); renderer.render(dig.scene, dig.camera); } + else { move(dt); renderer.render(scene, camera); } requestAnimationFrame(frame); } // expose for headless verification (screenshot harness, workflow checks) -window.PROCITY_C = { buildInterior, THREE, SHOP_TYPES, ARCHETYPE_KEYS, soak, rebuild, get current() { return current; }, scene, camera, renderer }; +window.PROCITY_C = { buildInterior, THREE, SHOP_TYPES, ARCHETYPE_KEYS, soak, rebuild, get current() { return current; }, scene, camera, renderer, + DIG_ON, digSoak, openDigOn, binUnderAim, get dig() { return dig; } }; rebuild(); frame(); diff --git a/web/js/interiors/dig.js b/web/js/interiors/dig.js new file mode 100644 index 0000000..c3476a7 --- /dev/null +++ b/web/js/interiors/dig.js @@ -0,0 +1,311 @@ +// PROCITY Lane C — crate riffle, ported from 90sDJsim/web/world/dig.js (itself from thriftgod/RecordGod). +// v2 headline behind `?dig=1`: walk to a record bin → flip through its sleeves, pull one, "buy" it. +// +// Scope (round 5): the riffled items are the EXISTING procedural stock — a seeded sleeve list per bin +// (same seed ⇒ same order). Real records feed in later via `stockAdapter(shop, 'sleeve')` (round 6+, +// GODVERSE/mint). **Buy is STUBBED** (a toast + remove-from-bin; getCash returns a constant). +// +// Self-contained: own scene + camera, procedural fwip/thunk audio (no assets), and LEAK-FREE — every +// geometry/material/CanvasTexture minted here is tracked and freed on close()/dispose(). Nothing here +// runs unless the shell/test-page opens it, so `?dig=0` is byte-identical to v1. + +import { mulberry32, xmur3 } from '../core/prng.js'; + +const MAXFLIP = 1.95, BACK = -0.12; +const SLEEVE = [0.31, 0.31, 0.0060]; // w, h, thickness (packed a touch chunkier than vinyl) +// 90s-Australian crate genres → sleeve card colour. +const GENRE = [['ROCK', '#7a2a3a'], ['SOUL', '#b0894a'], ['JAZZ', '#2a4a6a'], ['DISCO', '#8a3a7a'], + ['COUNTRY', '#6a5a2a'], ['PUNK', '#3a3a3a'], ['REGGAE', '#2a6a4a'], ['POP', '#b05a8a']]; +const ARTISTS = ['THE VERANDAHS', 'DEZ & THE UTES', 'MERINO', 'HILLS HOIST', 'THE ESKIES', 'GOON BAG', + 'STOBIE POLES', 'THE FIBROS', 'WARATAH', 'THONGS', 'THE CLOTHESLINE', 'GALAH']; +const TITLES = ['Sunburnt', 'Back o’ Bourke', 'Servo at Midnight', 'Long Weekend', 'Tinny', + 'Southerly Buster', 'Milk Bar Dreams', 'Cane Toad Blues', 'Ute Muster', 'Arvo']; +const CONDS = ['VG+', 'VG', 'G+', 'NM', 'EX']; + +// Deterministic sleeve list for a bin — the "existing procedural stock" as riffle-able offers. +function proceduralSleeves(seed, n) { + const r = mulberry32(seed >>> 0); + const out = []; + for (let i = 0; i < n; i++) { + const g = GENRE[(r() * GENRE.length) | 0]; + out.push({ + i, kind: 'record', + a: ARTISTS[(r() * ARTISTS.length) | 0], + t: TITLES[(r() * TITLES.length) | 0], + y: 1971 + ((r() * 28) | 0), + s: g[0], color: g[1], + cond: CONDS[(r() * CONDS.length) | 0], + price: 2 + ((r() * 28) | 0), + rare: r() < 0.12, + }); + } + return out; +} + +export function createDig(THREE, renderer) { + // ── tracked resources (leak-free) ── + // Persistent (p*): the fixed set — floor, backdrop, shared sleeve geometry — freed only on dispose(). + // Per-open (o*): sleeve textures/materials + crate parts — freed on EVERY close() so repeated + // open/close (e.g. the soak entering a dig per interior) returns to baseline. + const pGeo = new Set(), pMat = new Set(), pTex = new Set(); + const oGeo = new Set(), oMat = new Set(), oTex = new Set(); + const trackGeo = g => (pGeo.add(g), g); + const trackMat = m => (pMat.add(m), m); + const trackTex = t => (pTex.add(t), t); + + const scene = new THREE.Scene(); + scene.background = new THREE.Color(0x14110c); + const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.01, 50); + camera.position.set(0, 0.5, 0.92); + camera.lookAt(0, 0.12, -0.2); + const onResize = () => { camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix(); }; + addEventListener('resize', onResize); + + scene.add(new THREE.AmbientLight(0xfff4de, 0.55)); + const spot = new THREE.SpotLight(0xfff2e6, 26, 5, 0.7, 0.5, 1.2); + spot.position.set(0.4, 1.5, 0.9); spot.target.position.set(0, 0, -0.3); + scene.add(spot, spot.target); + const floor = new THREE.Mesh(trackGeo(new THREE.BoxGeometry(1.4, 0.02, 1.4)), + trackMat(new THREE.MeshStandardMaterial({ color: 0x2a2318, roughness: 0.9 }))); + floor.position.set(0.3, -0.011, -0.35); scene.add(floor); + // a warm gradient backdrop (canvas — no external art), so the sleeves stay the subject + const back = new THREE.Mesh(trackGeo(new THREE.PlaneGeometry(5.5, 3.1)), + trackMat(new THREE.MeshBasicMaterial({ map: backdropTex() }))); + back.position.set(0, 0.35, -2.8); scene.add(back); + function backdropTex() { + const cv = document.createElement('canvas'); cv.width = 64; cv.height = 48; + const x = cv.getContext('2d'); + const g = x.createLinearGradient(0, 0, 0, 48); + g.addColorStop(0, '#2a2016'); g.addColorStop(1, '#0e0b07'); + x.fillStyle = g; x.fillRect(0, 0, 64, 48); + const t = new THREE.CanvasTexture(cv); t.colorSpace = THREE.SRGBColorSpace; return trackTex(t); + } + + // ── sleeve art (cardboard card + genre + $ sticker) — matches the interior stock aesthetic ── + function sleeveTexture(o) { + const cv = document.createElement('canvas'); cv.width = cv.height = 256; + const c = cv.getContext('2d'); + c.fillStyle = o.color; c.fillRect(0, 0, 256, 256); + c.fillStyle = 'rgba(0,0,0,.22)'; c.fillRect(0, 0, 256, 12); c.fillRect(0, 244, 256, 12); + c.strokeStyle = 'rgba(255,255,255,.28)'; c.lineWidth = 3; c.strokeRect(14, 14, 228, 228); + c.fillStyle = 'rgba(255,255,255,.92)'; c.textAlign = 'center'; + c.font = '700 22px "Courier New",monospace'; c.fillText(o.a.slice(0, 18), 128, 80); + c.fillStyle = 'rgba(255,255,255,.75)'; c.font = '17px "Courier New",monospace'; + c.fillText('“' + o.t + '”', 128, 116); + c.fillStyle = 'rgba(0,0,0,.4)'; c.font = '14px "Courier New",monospace'; + c.fillText(`${o.y} · ${o.s} · ${o.cond}`, 128, 210); + c.beginPath(); c.arc(210, 48, 30, 0, 7); c.fillStyle = '#f4e11a'; c.fill(); // price sticker + c.fillStyle = '#c1121f'; c.font = '700 20px Arial'; c.fillText('$' + o.price, 210, 55); + if (o.rare) { c.fillStyle = '#ffd23d'; c.font = '20px monospace'; c.fillText('★', 34, 232); } + const t = new THREE.CanvasTexture(cv); t.colorSpace = THREE.SRGBColorSpace; t.anisotropy = 4; + oTex.add(t); return t; // per-open → freed on close() + } + + let recs = [], cursor = 0, vel = 0, active = false, pulled = null, lastFloor = -1; + let crate = null, offers = [], onCloseCb = null, onBuyCb = null, getCashCb = null; + // shared across opens (persistent): sleeves are all the same box; sides + crate timber never vary + const sleeveGeo = trackGeo(new THREE.BoxGeometry(SLEEVE[0], SLEEVE[1], SLEEVE[2])); + const sideMat = trackMat(new THREE.MeshStandardMaterial({ color: 0x8a8070, roughness: 0.85 })); + const crateWood = trackMat(new THREE.MeshStandardMaterial({ color: 0x8a6a45, roughness: 0.85 })); + + // ── DOM overlay (hint / cursor label / pull panel / walk-out) ── + const el = (tag, cls, txt) => { const e = document.createElement(tag); e.className = cls; if (txt) e.textContent = txt; return e; }; + const css = el('style'); css.textContent = ` + .pcdg-hint{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:rgba(0,0,0,.7);color:#d8d8e0;padding:6px 14px;font:13px "Courier New",monospace;pointer-events:none;display:none;z-index:60} + .pcdg-cur{position:fixed;left:50%;top:64px;transform:translateX(-50%);color:#ffd23d;font:700 15px "Courier New",monospace;text-shadow:0 1px 4px #000;pointer-events:none;text-align:center;max-width:80%;display:none;z-index:60} + .pcdg-panel{position:fixed;right:6%;top:50%;transform:translateY(-50%);width:250px;background:#0e0e16;border:1px solid #ffd23d;padding:16px;color:#d8d8e0;font:14px "Courier New",monospace;display:none;z-index:61} + .pcdg-panel h3{margin:.1em 0;color:#ffd23d;font-size:15px} + .pcdg-panel .meta{color:#8a8a9a;font-size:12px;margin:4px 0 8px} + .pcdg-panel .price{font-size:24px;color:#3dff8b} + .pcdg-panel button{margin-top:12px;width:100%;padding:9px;border:1px solid #666;background:#1d1d2b;color:#d8d8e0;font:inherit;cursor:pointer} + .pcdg-panel button:hover{border-color:#3dff8b;color:#3dff8b} + .pcdg-panel .x{position:absolute;top:6px;right:11px;cursor:pointer;color:#888} + .pcdg-out{position:fixed;left:16px;top:16px;background:#1d1d2b;border:1px solid #666;color:#d8d8e0;padding:8px 14px;font:13px "Courier New",monospace;cursor:pointer;display:none;z-index:60} + .pcdg-out:hover{border-color:#3dff8b;color:#3dff8b} + .pcdg-toast{position:fixed;left:50%;top:40%;transform:translate(-50%,-50%);background:#0e0e16;border:1px solid #3dff8b;color:#3dff8b;padding:14px 22px;font:700 15px "Courier New",monospace;z-index:62;display:none}`; + document.head.appendChild(css); + const hint = el('div', 'pcdg-hint', 'scroll / drag to riffle · click the front sleeve to pull it · Esc / WALK OUT to leave'); + const curLbl = el('div', 'pcdg-cur'); + const panel = el('div', 'pcdg-panel'); + const outBtn = el('div', 'pcdg-out', '← WALK OUT'); + const toast = el('div', 'pcdg-toast'); + outBtn.onclick = () => close(); + document.body.append(hint, curLbl, panel, outBtn, toast); + + // ── procedural audio ── + let actx; + function blip(kind) { + try { + actx = actx || new (window.AudioContext || window.webkitAudioContext)(); + if (kind === 'fwip') { + const n = Math.floor(actx.sampleRate * 0.05), buf = actx.createBuffer(1, n, actx.sampleRate), d = buf.getChannelData(0); + for (let i = 0; i < n; i++) d[i] = (Math.sin(i * 0.7) * (1 - i / n)) * 0.6; // deterministic-ish fwip (no Math.random) + const s = actx.createBufferSource(); s.buffer = buf; + const f = actx.createBiquadFilter(); f.type = 'bandpass'; f.frequency.value = 2100; f.Q.value = 0.7; + const g = actx.createGain(); g.gain.value = 0.2; + s.connect(f); f.connect(g); g.connect(actx.destination); s.start(); + } else { + const o = actx.createOscillator(); o.type = 'sine'; o.frequency.value = 105; + const g = actx.createGain(); g.gain.setValueAtTime(0.0001, actx.currentTime); + g.gain.exponentialRampToValueAtTime(0.5, actx.currentTime + 0.005); + g.gain.exponentialRampToValueAtTime(0.0001, actx.currentTime + 0.28); + o.connect(g); g.connect(actx.destination); o.start(); o.stop(actx.currentTime + 0.3); + } + } catch (e) {} + } + + function buildCrate(depth) { + if (crate) { scene.remove(crate); crate = null; } + crate = new THREE.Group(); + const [w, h] = SLEEVE; + const W = w + 0.06, H = h * 0.72, D = depth + 0.10, th = 0.016; + const add = (bw, bh, bd, x, y, z) => { const g = new THREE.BoxGeometry(bw, bh, bd); oGeo.add(g); const m = new THREE.Mesh(g, crateWood); m.position.set(x, y, z); crate.add(m); }; + add(W, th, D, 0, -th / 2, -D / 2 + 0.05); + add(th, H, D, -W / 2, H / 2, -D / 2 + 0.05); add(th, H, D, W / 2, H / 2, -D / 2 + 0.05); + add(W, H, th, 0, H / 2, -D + 0.05); add(W, H * 0.55, th, 0, H * 0.275, 0.05); + scene.add(crate); + } + + function buildStack(list) { + recs.forEach(r => scene.remove(r.group)); recs = []; + const [w, h, th] = SLEEVE; + let z = -0.05; + list.forEach((o, i) => { + const grp = new THREE.Group(); grp.position.set(0, 0, z); + const face = new THREE.MeshStandardMaterial({ map: sleeveTexture(o), roughness: 0.75 }); oMat.add(face); + const mesh = new THREE.Mesh(sleeveGeo, [sideMat, sideMat, sideMat, sideMat, face, sideMat]); // shared sleeve geo + mesh.position.y = h / 2; + grp.add(mesh); grp.rotation.x = BACK; + grp.rotation.z = (((i * 2654435761) >>> 0) % 100 - 50) / 1800; // each sleeve leans its own way (seeded by index) + grp.position.x = (((i * 40503) >>> 0) % 100 - 50) / 12000; + scene.add(grp); + recs.push({ group: grp, offer: o, angle: BACK, homeZ: z }); + z -= th + 0.010; + }); + buildCrate(Math.max(0.2, -z)); + cursor = 0; vel = 0; lastFloor = -1; updateCur(); + } + + function updateCur() { + const r = recs[Math.round(cursor)]; + curLbl.textContent = r ? `${r.offer.a} — “${r.offer.t}”` : (recs.length ? '' : 'bin’s cleaned out'); + } + + function removeEntry(entry) { + const idx = recs.indexOf(entry); if (idx < 0) return; + scene.remove(entry.group); recs.splice(idx, 1); + let z = -0.05; + recs.forEach(x => { x.homeZ = z; x.group.position.z = z; z -= SLEEVE[2] + 0.010; }); + cursor = Math.min(cursor, Math.max(0, recs.length - 1)); + pulled = null; panel.style.display = 'none'; updateCur(); + } + + function showPanel(entry) { + const o = entry.offer; + const cash = getCashCb ? getCashCb() : 999; + panel.innerHTML = `

${o.a}

` + + `
“${o.t}” · ${o.y} ${o.s} · ${o.cond}${o.rare ? ' · ★ RARE' : ''}
` + + `
$${o.price}
` + + ``; + panel.style.display = 'block'; + panel.querySelector('.x').onclick = unpull; + const b = panel.querySelector('button'); + if (b && !b.disabled) b.onclick = () => { + // STUB economy (round 6+): onBuy may veto; default is a toast + remove-from-bin. + const ok = onBuyCb ? onBuyCb(o) : true; + if (ok === false) { unpull(); return; } + blip('thunk'); + showToast(`SOLD — ${o.a} · $${o.price}`); + removeEntry(entry); + }; + } + let toastT = null; + function showToast(msg) { toast.textContent = msg; toast.style.display = 'block'; clearTimeout(toastT); toastT = setTimeout(() => (toast.style.display = 'none'), 1400); } + + function pull() { const r = recs[Math.round(cursor)]; if (!r) return; pulled = r; blip('thunk'); showPanel(r); } + function unpull() { if (pulled) pulled.group.position.set(pulled.group.position.x, 0, pulled.homeZ); pulled = null; panel.style.display = 'none'; updateCur(); } + + function update(dt) { + if (!active) return; + vel *= Math.pow(0.0008, dt); + cursor += vel * dt; + if (cursor < 0) { cursor = 0; vel = 0; } + if (cursor > recs.length - 1) { cursor = Math.max(0, recs.length - 1); vel = 0; } + const fl = Math.floor(cursor); + if (fl !== lastFloor) { lastFloor = fl; blip('fwip'); updateCur(); } + recs.forEach((r, i) => { + const f = THREE.MathUtils.smoothstep(cursor - i, -0.4, 1.2); + const target = BACK + f * (MAXFLIP - BACK); + r.angle += (target - r.angle) * Math.min(1, dt * 12); + if (r !== pulled) r.group.rotation.x = r.angle; + }); + if (pulled) { + pulled.group.position.lerp(new THREE.Vector3(0, 0.42, 0.55), Math.min(1, dt * 5)); + pulled.group.rotation.x += (0 - pulled.group.rotation.x) * Math.min(1, dt * 6); + } + } + + // ── input ── + let dragY = null, moved = 0; + const onWheel = e => { if (!active) return; e.preventDefault(); vel += e.deltaY * 0.012; }; + const onDown = e => { if (!active) return; dragY = e.clientY; moved = 0; }; + const onMove = e => { if (!active || dragY == null) return; const dy = e.clientY - dragY; moved += Math.abs(dy); cursor += dy * 0.02; vel = dy * 0.6; dragY = e.clientY; }; + const onUp = () => { if (!active) return; if (moved < 4 && !pulled) pull(); dragY = null; }; + const onKey = e => { if (!active) return; if (e.key === 'Escape') { e.stopPropagation(); if (pulled) unpull(); else close(); } }; + renderer.domElement.addEventListener('wheel', onWheel, { passive: false }); + renderer.domElement.addEventListener('pointerdown', onDown); + renderer.domElement.addEventListener('pointermove', onMove); + renderer.domElement.addEventListener('pointerup', onUp); + document.addEventListener('keydown', onKey, true); + + // open({ seed, count, shopName, shop, stockAdapter, onClose, onBuy, getCash }) + function open(opts = {}) { + active = true; + onCloseCb = opts.onClose || null; onBuyCb = opts.onBuy || null; getCashCb = opts.getCash || null; + const count = opts.count || 16; + // riffle items: the stockAdapter seam (real records later) or the seeded procedural sleeves now. + offers = (opts.stockAdapter && opts.stockAdapter(opts.shop, 'sleeve')) || proceduralSleeves(opts.seed ?? 1, count); + curLbl.textContent = (opts.shopName ? opts.shopName + ' — ' : ''); + [hint, curLbl, outBtn].forEach(e => e.style.display = 'block'); + buildStack(offers); + if (opts.shopName) curLbl.textContent = opts.shopName + ' — ' + curLbl.textContent; + } + + function freeOpen() { // free per-open GPU resources → each open/close is leak-free + recs.forEach(r => scene.remove(r.group)); recs = []; + if (crate) { scene.remove(crate); crate = null; } + for (const g of oGeo) g.dispose(); for (const m of oMat) m.dispose(); for (const t of oTex) t.dispose(); + oGeo.clear(); oMat.clear(); oTex.clear(); + } + + function close() { + if (!active) return; + active = false; pulled = null; + [hint, curLbl, panel, outBtn, toast].forEach(e => e.style.display = 'none'); + freeOpen(); + const cb = onCloseCb; onCloseCb = null; + if (cb) cb(); + } + + // full teardown — frees the persistent GPU resources + removes listeners/DOM (call when the shell + // is done with the dig, e.g. leaving interior mode entirely). + function dispose() { + close(); + removeEventListener('resize', onResize); + renderer.domElement.removeEventListener('wheel', onWheel); + renderer.domElement.removeEventListener('pointerdown', onDown); + renderer.domElement.removeEventListener('pointermove', onMove); + renderer.domElement.removeEventListener('pointerup', onUp); + document.removeEventListener('keydown', onKey, true); + for (const g of pGeo) g.dispose(); for (const m of pMat) m.dispose(); for (const t of pTex) t.dispose(); + pGeo.clear(); pMat.clear(); pTex.clear(); + [css, hint, curLbl, panel, outBtn, toast].forEach(e => e.remove()); + } + + return { scene, camera, open, close, update, dispose, get active() { return active; } }; +} + +// A stable per-bin seed from the shop + a bin index, so a given bin riffles the same order every time. +export function binSeed(shopSeed, binId) { + return (xmur3(`dig:${shopSeed}:${binId}`)()) >>> 0; +}