Lane F R8: wire town/weather/patronage/buy + graduate harness + new smokes
Shell seams (F owns index.html): A &town=<key> -> generatePlanFor(seed,'osm',{town}) (Katoomba);
B weather (createWeather on ?weather=1; PROCITY.weather={state,intensity}, {clear,0} off; update in
loop); C session wallet bound to the dig BUY (getCash/onBuy in interior_mode); D patronage door
points -> citizens.setShops + ?patronage=0 off-switch + setWeather each frame.
Harness: 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). Determinism gate: pinned osm/katoomba golden 0x0f652510 keyed on (seed,plansrc,
town) + fixed a hex zero-pad bug the katoomba golden exposed (0x0f652510 vs unpadded 0xf652510).
No patronage baseline re-pin needed (nets fewer draws, within tolerance). qa.sh --strict GREEN 5/5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
3daf8df3ec
commit
9cc00ad29c
@ -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=<key>` → `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.
|
||||
|
||||
BIN
docs/shots/laneF/r8_katoomba.png
Normal file
BIN
docs/shots/laneF/r8_katoomba.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 851 KiB |
BIN
docs/shots/laneF/r8_rain_street.png
Normal file
BIN
docs/shots/laneF/r8_rain_street.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 834 KiB |
@ -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()
|
||||
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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=<key> 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.
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user