diff --git a/F-progress.md b/F-progress.md index a20ac0c..7b46e85 100644 --- a/F-progress.md +++ b/F-progress.md @@ -4,7 +4,22 @@ --- -## Round 7 (the flip round → v2.0-beta) — IN PROGRESS +## Round 8 (catch-up + the town comes alive → v2.0-rc) — DONE ✅ + +All lanes landed (E packs, B weather, A 2nd town, C buy, D patronage). F wired the shell seams, +graduated the harness, tagged. + +| task | status | +|---|---| +| **shell wiring (F owns index.html)** | ✅ **A** `&town=` → `generatePlanFor(seed,'osm',{town})` (Katoomba, 19 shops, verified). **B** weather (3-line contract): `createWeather` on `?weather=1`, `PROCITY.weather={state,intensity}` ({clear,0} when off for D), `weather.update` in loop. **C** session `wallet` (createWallet(seed)) bound to the dig BUY (`getCash`/`onBuy` in interior_mode). **D** patronage: shop door-points per chunk → `citizens.setShops`, `?patronage=0` off-switch, `citizens.setWeather` each frame. All boot clean (0 console errors, settled). | +| **F1 — smoke graduations + additions** | ✅ `stock=real` + `weather=rain` **graduated warn→STRICT**. New warn smokes: **buy-v0** (dig BUY→wallet: cash 191→179, bag 0→1), **patronage** (7 peds visit shop doors/~18s), **book/toy stock** (6/11 real atlas meshes). Katoomba golden `0x0f652510` pinned in the determinism gate (fixed a hex zero-pad bug it caught). | +| **F2 — patronage baseline** | ✅ No re-pin needed — flags-off regression held (patronage nets *fewer* draws, inside tolerance; ≤300 cap + draws-±40% both green). | +| **F3 — tag v2.0-rc** | ✅ **TAGGED.** qa.sh --strict GREEN 5/5 (harness strict). | +| **Marshal** | All lanes committed round-8; no slips this round. | + +--- + +## Round 7 (the flip round → v2.0-beta) — DONE ✅ (v2.0-beta tagged) Critical path resolved: E1 ped-merge (`447188a`), E2 stock pack (`0c12dac`), **D's GO** (`43edabf`), C1 `?stock=real` (`3724a65`) all landed. F executes flip → strict harness → tag. diff --git a/docs/shots/laneF/r8_katoomba.png b/docs/shots/laneF/r8_katoomba.png new file mode 100644 index 0000000..d3b18d8 Binary files /dev/null and b/docs/shots/laneF/r8_katoomba.png differ diff --git a/docs/shots/laneF/r8_rain_street.png b/docs/shots/laneF/r8_rain_street.png new file mode 100644 index 0000000..79f4c5b Binary files /dev/null and b/docs/shots/laneF/r8_rain_street.png differ diff --git a/tools/flags_check.py b/tools/flags_check.py index 5f0cb5f..2c152f0 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -245,51 +245,138 @@ def interior_budget(p): finally: b.close() +def _enter_type(pg, t, extra=''): + """Enter an open shop of type `t`; returns the shop name or {err}.""" + return pg.evaluate("""(t) => { + const P=window.PROCITY, D=window.DBG; D.setSegment(2); + const s=(P.plan.shops||[]).find(x=>x.type===t && P.isOpen(x)); + if(!s) return {err:'no open '+t+' shop'}; + D.enterShop(s.id); return { shop: s.name }; + }""", t) + +def _pack_ready(pg, t): + try: + pg.wait_for_function("async () => { const m = await import('./js/interiors/interiors.js');" + f" return !!(m.getStockPack && m.getStockPack('{t}')); }}", timeout=12000) + return True + except Exception: + return False + def smoke_stock(p): - """NEW FLAG (warn-level, decision #4): ?stock=real → real GODVERSE sleeves in the crates. - Assert non-parody: real sleeves are batched atlas meshes tagged userData.isStock (Lane C recipe).""" - head('SMOKE: stock=real (?stock=real — real sleeves; new flag → warn-level)') + """GRADUATED STRICT (R8): ?stock=real → real GODVERSE sleeves (batched userData.isStock atlas meshes).""" + head('SMOKE: stock=real (?stock=real — real record sleeves; STRICT r8)') b, pg, errs = new_page(p) try: boot(pg, 'stock=real&dig=1') - ready = False - try: # the shell preloads the pack async — wait for getStockPack('record') to resolve - pg.wait_for_function( - "async () => { const m = await import('./js/interiors/interiors.js');" - " return !!(m.getStockPack && m.getStockPack('record')); }", timeout=12000) - ready = True - except Exception: - pass - res = pg.evaluate("""() => { - const P=window.PROCITY, D=window.DBG; D.setSegment(2); - const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s)); - if(!rec) return {err:'no open record shop'}; - D.enterShop(rec.id); - let n=0; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); - return { shop: rec.name, isStock: n }; - }""") - if res.get('err'): WARN(f"stock=real: {res['err']}") - elif not ready: WARN(f"stock=real: pack didn't preload in 12s — parody fallback (fail-soft); {res['isStock']} isStock meshes") - elif res['isStock'] > 0: OK(f"stock=real: {res['isStock']} real atlas sleeve mesh(es) in '{res['shop']}' — non-parody, batched") - else: WARN(f"stock=real: pack loaded but 0 isStock atlas meshes in '{res['shop']}'") - if errs: WARN(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}") + ready = _pack_ready(pg, 'record') + r = _enter_type(pg, 'record') + if r.get('err'): FAIL(f"stock=real: {r['err']}"); return + n = pg.evaluate("() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); return n; }") + if not ready: FAIL(f"stock=real: record pack didn't preload in 12s (E ships it) — got {n} isStock meshes") + elif n > 0: OK(f"stock=real: {n} real atlas sleeve mesh(es) in '{r['shop']}' — non-parody, batched") + else: FAIL(f"stock=real: pack loaded but 0 isStock atlas meshes in '{r['shop']}'") + if errs: FAIL(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}") finally: b.close() def smoke_weather(p): - """NEW FLAG (warn-level): ?weather=1 (Lane B). Auto-skips if not landed yet.""" + """GRADUATED STRICT (R8): ?weather=rain → rain THREE.Points layer + PROCITY.weather.state=='rain'.""" + head('SMOKE: weather (?weather=rain — rain particles + wet ground; STRICT r8)') b, pg, errs = new_page(p) try: - boot(pg, 'weather=1') - w = pg.evaluate("() => { const P=window.PROCITY||{}; const w=P.weather||(P.lighting&&P.lighting.weather); " - "return w ? { active:!!w.active, particles:!!(w.particles||w.rain), wet:!!w.wet } : null; }") - if not w: - print("· weather=1 not landed (no weather subsystem) — skipping (Lane B)") - return - head('SMOKE: weather=1 (new flag → warn-level)') - if w['active'] and w['particles']: OK(f"weather=1 active with particle layer: {w}") - else: WARN(f"weather=1 present but incomplete: {w}") - if errs: WARN(f"weather=1: {len(errs)} console error(s); first: {errs[0][:140]}") + boot(pg, 'weather=rain') + st = pg.evaluate("() => { const P=window.PROCITY; return { state: P.weather&&P.weather.state, " + "intensity: P.weather&&P.weather.intensity, rainPts: !!P.scene.getObjectByProperty('type','Points') }; }") + if st['state'] == 'rain' and st['rainPts']: + OK(f"weather=rain: rain Points present + PROCITY.weather.state=='rain' (intensity {st['intensity']})") + else: FAIL(f"weather=rain broken: {st}") + if errs: FAIL(f"weather=rain: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + +def smoke_buy(p): + """NEW (warn): buy-v0 — open dig, pull + BUY a sleeve, assert wallet cash dropped + item banked.""" + head('SMOKE: buy-v0 (dig BUY → session wallet; new → warn-level)') + b, pg, errs = new_page(p) + try: + boot(pg, 'dig=1') + setup = pg.evaluate("""() => { + const P=window.PROCITY, D=window.DBG; D.setSegment(2); + const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s)); + if(!rec) return {err:'no record shop'}; + D.enterShop(rec.id); + const bins=[]; P.interiorMode.current.group.traverse(o=>{if(o.userData&&o.userData.kind==='bin')bins.push(o)}); + if(!bins.length) return {err:'no bins'}; + const bp=new P.THREE.Vector3(); bins[0].getWorldPosition(bp); + P.camera.position.set(bp.x,1.6,bp.z+1.4); P.camera.lookAt(bp.x,bp.y+0.4,bp.z); P.camera.updateMatrixWorld(); + return { cash0: P.wallet.cash(), count0: P.wallet.count() }; + }""") + if setup.get('err'): WARN(f"buy-v0: {setup['err']}"); return + pg.evaluate("() => window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}))") # open dig + pg.wait_for_timeout(600) + # pull the front sleeve (canvas click, no drag → dig.pull) then click its BUY IT button + pg.evaluate("""() => { const c=window.PROCITY.renderer.domElement; + c.dispatchEvent(new PointerEvent('pointerdown',{clientX:640,clientY:360,bubbles:true})); + c.dispatchEvent(new PointerEvent('pointerup',{clientX:640,clientY:360,bubbles:true})); }""") + pg.wait_for_timeout(400) + res = pg.evaluate("""() => { + const w=window.PROCITY.wallet, cash0=w.cash(), count0=w.count(); + const btn=document.querySelector('.pcdg-panel button'); + if(!btn) return {err:'no BUY panel (pull failed)'}; + if(btn.disabled) return {err:'BUY disabled (broke?)'}; + btn.click(); + return { cash0, count0, cash1:w.cash(), count1:w.count() }; + }""") + if res.get('err'): WARN(f"buy-v0: {res['err']}") + elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1: + OK(f"buy-v0: bought 1 — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']}") + else: WARN(f"buy-v0: unexpected wallet delta {res}") + if errs: WARN(f"buy-v0: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + +def smoke_patronage(p): + """NEW (warn): peds duck into open shops — assert ≥1 ped parks at a shop door over N seconds.""" + head('SMOKE: patronage (peds visit open shops; new → warn-level)') + b, pg, errs = new_page(p) + try: + boot(pg, '') # default: streamed roster + patronage on + # advance the sim ~12s of ticks at a busy midday node; count peds flagged as inside/at-door + res = pg.evaluate("""() => { + const P=window.PROCITY, D=window.DBG; D.setSegment(2); + const plan=P.plan, deg={}; for(const e of plan.streets.edges){deg[e.a]=(deg[e.a]||0)+1;deg[e.b]=(deg[e.b]||0)+1;} + let best=null,bd=0; for(const n of plan.streets.nodes){const d=deg[n.id]||0;if(d>bd){bd=d;best=n;}} + D.teleport(best.x,best.z,0); P.citizens.setPopulation(200); + let everInside=0; const seen=new Set(); + for(let i=0;i<360;i++){ P.citizens.update(0.05); + const list=P.citizens._activeList||[]; + for(const c of list){ if(c && (c.patronInside||c.inside||c.patronTarget) && !seen.has(c.id)){ seen.add(c.id); everInside++; } } + } + return { active:(P.citizens._activeList||[]).length, everVisited: everInside, patronOn: P.citizens.patronageOn }; + }""") + if not res['patronOn']: WARN("patronage: off (patronageOn=false) — expected default-on") + elif res['everVisited'] > 0: OK(f"patronage: {res['everVisited']} ped(s) headed to/into a shop door over ~18s (active {res['active']})") + else: WARN(f"patronage: 0 peds visited a shop over ~18s (active {res['active']}) — check door points / timing") + if errs: WARN(f"patronage: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + +def smoke_booktoy(p): + """NEW (warn): book/toy packs — ?stock=real static shelf stock (real spine/box atlas meshes).""" + head('SMOKE: book/toy stock (?stock=real — real spines/boxes; new → warn-level)') + b, pg, errs = new_page(p) + try: + boot(pg, 'stock=real') + for t in ('book', 'toy'): + _pack_ready(pg, t) + r = _enter_type(pg, t) + if r.get('err'): WARN(f"book/toy: {r['err']}"); continue + n = pg.evaluate("() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); return n; }") + if n > 0: OK(f"{t} stock: {n} real atlas mesh(es) in '{r['shop']}'") + else: WARN(f"{t} stock: 0 isStock atlas meshes in '{r['shop']}' (pack missing → parody fallback?)") + pg.evaluate("() => window.DBG.exitShop && window.DBG.exitShop()") + pg.wait_for_timeout(150) + if errs: WARN(f"book/toy: {len(errs)} console error(s); first: {errs[0][:140]}") finally: b.close() @@ -308,8 +395,11 @@ def main(): for fl in flags: smoke(p, fl.split('=')[0], fl) smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True) - smoke_stock(p) # new flag (warn-level) — ?stock=real - smoke_weather(p) # new flag (warn-level) — ?weather=1 (auto-skips if not landed) + smoke_stock(p) # STRICT (r8) — ?stock=real real record sleeves + smoke_weather(p) # STRICT (r8) — ?weather=rain + smoke_buy(p) # new (warn) — buy-v0 + smoke_patronage(p) # new (warn) — peds visit open shops + smoke_booktoy(p) # new (warn) — book/toy real stock finally: if srv: srv.terminate() diff --git a/tools/qa/scaffold_check.mjs b/tools/qa/scaffold_check.mjs index 2658b37..d06314e 100755 --- a/tools/qa/scaffold_check.mjs +++ b/tools/qa/scaffold_check.mjs @@ -214,20 +214,23 @@ if (existsSync(R('web/js/citygen/plan.js'))) { info(`plan(1234): ${p1?.shops?.length ?? '?'} shops, ${p1?.lots?.length ?? '?'} lots, ` + `${p1?.streets?.edges?.length ?? '?'} edges, name "${p1?.name ?? '?'}"`); - // [F2] plan-source selector (?plansrc=osm) — pin BOTH goldens keyed by (seed, plansrc). - // Lane A contract (LANE_A_NOTES F2): synthetic 0x3fa36874 (selfcheck), osm 0x34cfdec0. + // [F2/R8] plan-source selector — pin goldens keyed by (seed, plansrc, TOWN). + // Lane A contract: synthetic 0x3fa36874 (selfcheck) · osm/melbourne 0x34cfdec0 · osm/katoomba 0x0f652510. // Guarded: no-op until A ships generatePlanFor + generatePlanOSM. try { const bar = await import(R('web/js/citygen/index.js')); if (typeof bar.generatePlanFor === 'function' && typeof bar.generatePlanOSM === 'function') { const { xmur3 } = await import(R('web/js/core/prng.js')); - const oa = bar.generatePlanOSM(20261990), ob = bar.generatePlanOSM(20261990); - assert(JSON.stringify(oa) === JSON.stringify(ob), - 'generatePlanOSM(20261990) is deterministic across two runs', 'OSM plan NON-DETERMINISTIC'); - const oh = (xmur3(JSON.stringify(oa))() >>> 0).toString(16); - assert(`0x${oh}` === '0x34cfdec0', - `osm plan fingerprint == golden 0x34cfdec0 ("${oa.name}", ${oa.shops?.length} shops)`, - `osm plan fingerprint DRIFTED: 0x${oh} (expect 0x34cfdec0)`); + const TOWN_GOLDENS = { melbourne: '0x34cfdec0', katoomba: '0x0f652510' }; + for (const [town, golden] of Object.entries(TOWN_GOLDENS)) { + const a = bar.generatePlanFor(20261990, 'osm', { town }), b2 = bar.generatePlanFor(20261990, 'osm', { town }); + assert(JSON.stringify(a) === JSON.stringify(b2), + `osm/${town} plan is deterministic across two runs`, `osm/${town} plan NON-DETERMINISTIC`); + const h = (xmur3(JSON.stringify(a))() >>> 0).toString(16).padStart(8, '0'); // pad: goldens are 0x + 8 hex + assert(`0x${h}` === golden, + `osm/${town} fingerprint == golden ${golden} ("${a.name}", ${a.shops?.length} shops)`, + `osm/${town} fingerprint DRIFTED: 0x${h} (expect ${golden})`); + } assert(bar.generatePlanFor(20261990, 'osm').source === 'osm' && (bar.generatePlanFor(20261990, 'synthetic').source || 'synthetic') === 'synthetic', 'generatePlanFor routes synthetic|osm correctly', 'generatePlanFor selector mis-routes'); diff --git a/web/index.html b/web/index.html index 352cd80..93f5767 100644 --- a/web/index.html +++ b/web/index.html @@ -53,7 +53,9 @@ import { createPlayer } from './js/world/player.js'; import { createHUD } from './js/world/hud.js'; import { createMinimap } from './js/world/minimap.js'; import { createInteriorMode } from './js/world/interior_mode.js'; // [Lane F integration] Lane C interior bridge +import { createWallet } from './js/interiors/wallet.js'; // [Lane F R8] Lane C buy loop v0 (session wallet) import { CitizenSim } from './js/citizens/sim.js'; // [Lane F integration] Lane D street pedestrians +import { createWeather } from './js/world/weather.js'; // [Lane F R8] Lane B seeded weather (?weather=1) import { loadPedFleet } from './js/citizens/rigs.js'; // [Lane F §3.4] Lane D rig fleet (GLB peds/keepers) import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack @@ -75,14 +77,15 @@ const NOASSETS = params.has('noassets') && params.get('noassets') !== '0'; // golden-hash generator (prime law); 'osm' = Lane A's real-data fixture importer (generatePlanFor, // LANE_A_NOTES F2). Zero network either way. Guarded: falls back to synthetic generatePlan, then fixture. const PLANSRC = params.get('plansrc') === 'osm' ? 'osm' : 'synthetic'; +const TOWN = params.get('town') || undefined; // [Lane F R8] &town= selects an OSM town (A: melbourne|katoomba) let plan; try { const citygen = await import('./js/citygen/index.js'); const gen = citygen.generatePlanFor - ? (s) => citygen.generatePlanFor(s, PLANSRC) // A's selector (synthetic | osm) + ? (s) => citygen.generatePlanFor(s, PLANSRC, { town: TOWN }) // A's selector (synthetic | osm[+town]) : (citygen.generatePlan || citygen.default); // pre-R6 fallback (synthetic only) plan = gen ? gen(seed) : fixturePlan(seed); - console.log(`[procity] plan source: ${PLANSRC} — ${plan.name}, ${plan.shops.length} shops`); + console.log(`[procity] plan source: ${PLANSRC}${TOWN ? '/' + TOWN : ''} — ${plan.name}, ${plan.shops.length} shops`); } catch (e) { plan = fixturePlan(seed); console.log('[procity] Lane A citygen absent — running on fixture_plan'); @@ -146,7 +149,10 @@ const DIG_ON = params.has('dig') && params.get('dig') !== '0'; // the first interior/dig can batch real covers; fail-soft (missing pack → parody canvas). Off under ?noassets. const STOCK_REAL = params.get('stock') === 'real' && !NOASSETS; if (STOCK_REAL) preloadStockPack('record'); -const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, dig: DIG_ON, stockReal: STOCK_REAL }); +// [Lane F R8 — Lane C buy loop v0] ONE session wallet (seeded start cash) bound to the dig BUY across shops. +// Runtime-only: never writes back into the plan/room build, so goldens + draw-counts are unaffected. +const wallet = createWallet(seed); +const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, dig: DIG_ON, stockReal: STOCK_REAL, wallet }); // [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's // open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin // doesn't read as "walked out of the shop". @@ -170,9 +176,31 @@ if (!rosterV1) { for (let dx = -1; dx <= 1; dx++) for (let dz = -1; dz <= 1; dz++) keys.push(citizens.chunkKeyAt(lot.x + dx * 64, lot.z + dz * 64)); citizens.setNightLivelyChunks(keys); } + // [Lane F R8 — Lane D patronage] shop door points per chunk (facade normal → the street), fed once to the + // sim so streamed peds can duck into open shops they pass. Computed from the plan (sim stays graph-only). + const shopsByChunk = new Map(); + for (const s of plan.shops) { + const l = plan.lots.find((x) => x.id === s.lot); if (!l) continue; + const ry = l.ry || 0, fx = -Math.sin(ry), fz = -Math.cos(ry); + const x = l.x + fx * (l.d / 2 + 0.6), z = l.z + fz * (l.d / 2 + 0.6); + const k = citizens.chunkKeyAt(x, z); + if (!shopsByChunk.has(k)) shopsByChunk.set(k, []); + shopsByChunk.get(k).push({ x, z, hours: s.hours }); + } + citizens.setShops(shopsByChunk); + if (params.get('patronage') === '0') citizens.setPatronage(false); // off-switch } document.addEventListener('visibilitychange', () => citizens.setPaused(document.hidden)); +// [Lane F R8 — Lane B weather] ?weather=1 seeded (seed 20261990 default town rolls 'clear'); ?weather=rain| +// overcast|clear forces a state. Self-contained module; flag-off never constructs it (byte-identical). D reads +// window.PROCITY.weather (always a valid {state,intensity} — {clear,0} when off), it does NOT import weather.js. +const _wp = params.get('weather'); +const weather = (_wp && _wp !== '0') + ? createWeather({ scene, plan, skins, lighting, camera, force: /^(clear|overcast|rain)$/.test(_wp) ? _wp : null }) + : null; +const weatherState = weather ? weather.state : { state: 'clear', intensity: 0 }; + // spawn on the footpath near the west end of the strip, looking east down main street player.teleport(-96, 9, -Math.PI / 2); chunks.warmup(player.position); @@ -276,6 +304,8 @@ function frame() { citizens.setTimeOfDay((clk.seg + clk.frac) / 6); // [Lane F] 0..1 day fraction → density (busy midday, empty at night) // [Lane F R4] no setExposure call: D3 (a5e4b64) made the impostor toneMapped:true, so three / // OutputPass tone-map it globally via renderer.toneMappingExposure — the passthrough is now a no-op. + if (weather) weather.update(dt); // [Lane F R8] Lane B rain particles + wet ground + citizens.setWeather(weatherState); // [Lane F R8] Lane D rain reaction (thin + shelter) citizens.update(dt); // [Lane F] reads camera pos internally for LOD tiers hud.tickToast(dt); hud.update(dt, { clock: clk, chunks: chunks.count }); // reads prev frame's total @@ -295,6 +325,8 @@ frame(); // expose for debugging / Lane F window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skins, interiorMode, citizens, enterShop, leaveShop, isOpen, currentHour, fleet, noassets: NOASSETS, + weather: weatherState, // [Lane F R8] Lane B contract: {state,intensity}, {clear,0} when off (Lane D reads this) + wallet, // [Lane F R8] Lane C buy loop v0 (session wallet: cash/buy/count) THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks // [Lane B] QA debug harness (window.DBG) — loaded only with ?dbg=1 (LANE_F_NOTES §4). Logic lives // in js/world/dbg.js; the shell just hands it the systems it owns so shots.py/soak.py can drive it. diff --git a/web/js/world/interior_mode.js b/web/js/world/interior_mode.js index 4aaaee2..697c693 100644 --- a/web/js/world/interior_mode.js +++ b/web/js/world/interior_mode.js @@ -26,7 +26,7 @@ const ARM_DIST = 2.0; // must step this far from the door before the exit arms const EXIT_DIST = 1.0; // …then coming back within this distance leaves to the street export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false, - dig: digEnabled = false, stockReal = false }) { + dig: digEnabled = false, stockReal = false, wallet = null }) { const scene = new THREE.Scene(); scene.background = new THREE.Color('#0e0b07'); @@ -81,6 +81,10 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null shopName: (current && current.recipe && current.recipe.label) || (currentShop && currentShop.name), shop: currentShop, stockAdapter: currentAdapter, // [F2 §stock=real] real covers in the riffle (null → parody) + // [F R8 — Lane C buy loop v0] bind the session wallet so pulling a sleeve deducts cash + banks it. + // dig removes the pulled sleeve from the bin; onBuy returns false when broke (no state change). + getCash: wallet ? () => wallet.cash() : undefined, + onBuy: wallet ? (item) => wallet.buy(item) : undefined, onClose: () => window.dispatchEvent(new CustomEvent('procity:digClose')), }); window.dispatchEvent(new CustomEvent('procity:digOpen')); // shell releases pointer-lock for the cursor