Lane F R9 F1+F2: interior presence + tram + buy-anywhere wiring; harness graduated + new smokes
Wiring (F owns index.html + interior_mode.js): • Interior presence (C->D->F seam): occupancyOf closure -> interior_mode spawns browser rigs at C's room.browsePoints (one per patronage occupant, cap 3); freed by exit()'s keepers.disposeAll. Door points now carry shopId for D's occupancy truth. • Tram (Lane B, ?tram=1): createTram + update in loop (2 draws, non-blocking). • Buy-anywhere (Lane C C2): E aimed at a book-spine/toy-box buyMesh buys over the same wallet (collapseBuyItem + toast); preload book+toy packs too. Records keep the dig. Harness: graduated buy-v0 / patronage / book-toy stock warn->STRICT (katoomba golden already strict). New warn smokes: interior presence (browsers==min(occ,3), dispose clean), shelf-buy (book spine bought, cash drops), tram (present). Book-stock check now counts isStock OR buyMesh (C2 tags book spines buyMesh, not isStock). flags_check GREEN 0/0; qa.sh --strict GREEN 5/5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
16c4450cd9
commit
5a24f8aa52
@ -296,7 +296,7 @@ def smoke_weather(p):
|
||||
|
||||
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)')
|
||||
head('SMOKE: buy-v0 (dig BUY → session wallet; STRICT r9)')
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, 'dig=1')
|
||||
@ -311,7 +311,7 @@ def smoke_buy(p):
|
||||
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
|
||||
if setup.get('err'): FAIL(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
|
||||
@ -327,17 +327,17 @@ def smoke_buy(p):
|
||||
btn.click();
|
||||
return { cash0, count0, cash1:w.cash(), count1:w.count() };
|
||||
}""")
|
||||
if res.get('err'): WARN(f"buy-v0: {res['err']}")
|
||||
if res.get('err'): FAIL(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]}")
|
||||
else: FAIL(f"buy-v0: unexpected wallet delta {res}")
|
||||
if errs: FAIL(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)')
|
||||
head('SMOKE: patronage (peds visit open shops; STRICT r9)')
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, '') # default: streamed roster + patronage on
|
||||
@ -354,29 +354,111 @@ def smoke_patronage(p):
|
||||
}
|
||||
return { active:(P.citizens._activeList||[]).length, everVisited: everInside, patronOn: P.citizens.patronageOn };
|
||||
}""")
|
||||
if not res['patronOn']: WARN("patronage: off (patronageOn=false) — expected default-on")
|
||||
if not res['patronOn']: FAIL("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]}")
|
||||
else: FAIL(f"patronage: 0 peds visited a shop over ~18s (active {res['active']}) — check door points / timing")
|
||||
if errs: FAIL(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)')
|
||||
head('SMOKE: book/toy stock (?stock=real — real spines/boxes; STRICT r9)')
|
||||
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?)")
|
||||
if r.get('err'): FAIL(f"book/toy: {r['err']}"); continue
|
||||
# R9: real static stock is EITHER an isStock atlas mesh (records/toys) OR a buyMesh shelf
|
||||
# (book spines / toy boxes, C2 buy-anywhere). Parody stock has neither.
|
||||
n = pg.evaluate("""() => { let n=0; window.PROCITY.interiorMode.current.group.traverse(o=>{
|
||||
const u=o.userData||{}; if((u.isStock && o.material && o.material.map) || (u.buyMesh && u.buyItems && u.buyItems.length)) n++; }); return n; }""")
|
||||
if n > 0: OK(f"{t} stock: {n} real stock mesh(es) in '{r['shop']}'")
|
||||
else: FAIL(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]}")
|
||||
if errs: FAIL(f"book/toy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
finally:
|
||||
b.close()
|
||||
|
||||
def smoke_presence(p):
|
||||
"""NEW (warn): interior presence — enter an OCCUPIED shop, assert browser rigs == min(occupancy,3),
|
||||
dispose clean on exit, 0 errors (the C→D→F seam)."""
|
||||
head('SMOKE: interior presence (browsers in occupied shops; new → warn-level)')
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, 'stock=real')
|
||||
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);
|
||||
for(let i=0;i<400;i++) P.citizens.update(0.05);
|
||||
let shop=null,occ=null;
|
||||
for(const s of plan.shops){ const o=P.citizens.occupancyOf(s.id); if(o&&o.count>0){shop=s;occ=o;break;} }
|
||||
if(!shop) return {err:'no occupied shop after patronage'};
|
||||
D.enterShop(shop.id);
|
||||
const kl=P.interiorMode.keepers.keepers||[];
|
||||
const total=kl.length, pts=(P.interiorMode.current.browsePoints||[]).length;
|
||||
return { shop:shop.name, occ:occ.count, expect:Math.min(occ.count,3), totalKeepers:total, browsePoints:pts };
|
||||
}""")
|
||||
if res.get('err'): WARN(f"presence: {res['err']}"); return
|
||||
# total keepers = 1 counter + browsers; browsers should == min(occ,3)
|
||||
browsers = res['totalKeepers'] - 1
|
||||
if browsers == res['expect'] and res['browsePoints'] >= 1:
|
||||
OK(f"presence: {browsers} browser(s) in '{res['shop']}' == min(occupancy {res['occ']},3); {res['browsePoints']} browse points")
|
||||
else:
|
||||
WARN(f"presence: browsers {browsers} != expect {res['expect']} (occ {res['occ']}, pts {res['browsePoints']}, keepers {res['totalKeepers']})")
|
||||
clean = pg.evaluate("() => { window.DBG.exitShop(); return (window.PROCITY.interiorMode.keepers.keepers||[]).length; }")
|
||||
if clean == 0: OK("presence: browsers disposed clean on exit (0 keepers)")
|
||||
else: WARN(f"presence: {clean} keeper(s) leaked after exit")
|
||||
if errs: WARN(f"presence: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
finally:
|
||||
b.close()
|
||||
|
||||
def smoke_shelfbuy(p):
|
||||
"""NEW (warn): buy-anywhere — E aimed at a book spine / toy box buys over the wallet (Lane C C2)."""
|
||||
head('SMOKE: shelf-buy (?stock=real book/toy pull-and-buy; new → warn-level)')
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, 'stock=real')
|
||||
_pack_ready(pg, 'book')
|
||||
res = pg.evaluate("""() => {
|
||||
const P=window.PROCITY, D=window.DBG; D.setSegment(2);
|
||||
const bk=(P.plan.shops||[]).find(s=>s.type==='book' && P.isOpen(s));
|
||||
if(!bk) return {err:'no book shop'};
|
||||
D.enterShop(bk.id);
|
||||
const meshes=[]; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.buyMesh&&o.userData.buyItems&&o.userData.buyItems.length) meshes.push(o); });
|
||||
if(!meshes.length) return {err:'no buy meshes (pack not loaded?)'};
|
||||
const m=meshes[0], it=m.userData.buyItems[0], wp=new P.THREE.Vector3();
|
||||
m.localToWorld(wp.copy(it.center));
|
||||
P.camera.position.set(wp.x,wp.y,wp.z+1.0); P.camera.lookAt(wp.x,wp.y,wp.z); P.camera.updateMatrixWorld();
|
||||
const cash0=P.wallet.cash(), count0=P.wallet.count();
|
||||
window.dispatchEvent(new KeyboardEvent('keydown',{code:'KeyE'}));
|
||||
return { shop:bk.name, buyMeshes:meshes.length, cash0, count0, cash1:P.wallet.cash(), count1:P.wallet.count() };
|
||||
}""")
|
||||
if res.get('err'): WARN(f"shelf-buy: {res['err']}")
|
||||
elif res['cash1'] < res['cash0'] and res['count1'] == res['count0'] + 1:
|
||||
OK(f"shelf-buy: bought a book spine in '{res['shop']}' — cash {res['cash0']}→{res['cash1']}, bag {res['count0']}→{res['count1']} ({res['buyMeshes']} buy meshes)")
|
||||
else: WARN(f"shelf-buy: no purchase — {res}")
|
||||
if errs: WARN(f"shelf-buy: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
finally:
|
||||
b.close()
|
||||
|
||||
def smoke_tram(p):
|
||||
"""NEW (warn): ?tram=1 — Lane B tram loop. Auto-skips if not landed."""
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, 'tram=1')
|
||||
present = pg.evaluate("() => !!window.PROCITY.scene.getObjectByName('tram')")
|
||||
if not present:
|
||||
print("· tram=1 not landed (no 'tram' in scene) — skipping (Lane B)")
|
||||
return
|
||||
head('SMOKE: tram (?tram=1; new → warn-level)')
|
||||
OK("tram=1: 'tram' present in scene")
|
||||
if errs: WARN(f"tram: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
finally:
|
||||
b.close()
|
||||
|
||||
@ -397,9 +479,12 @@ def main():
|
||||
smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True)
|
||||
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
|
||||
smoke_buy(p) # STRICT (r9) — buy-v0
|
||||
smoke_patronage(p) # STRICT (r9) — peds visit open shops
|
||||
smoke_booktoy(p) # STRICT (r9) — book/toy real stock
|
||||
smoke_presence(p) # new (warn) — interior presence (browsers)
|
||||
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
||||
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
|
||||
@ -56,6 +56,7 @@ import { createInteriorMode } from './js/world/interior_mode.js'; // [Lane F in
|
||||
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 { createTram } from './js/world/tram.js'; // [Lane F R9] Lane B tram (?tram=1, non-blocking)
|
||||
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
|
||||
|
||||
@ -148,11 +149,16 @@ const DIG_ON = params.has('dig') && params.get('dig') !== '0';
|
||||
// [Lane F §F2 stock=real] real GODVERSE sleeves in the crates (v2). Preload the record pack at boot so
|
||||
// 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');
|
||||
// [Lane F R9] preload ALL three packs so record dig sleeves AND book-spine/toy-box shelves (buy-anywhere)
|
||||
// are real. Fail-soft per pack (missing → parody canvas). getStockPack(type) is what buildInterior reads.
|
||||
if (STOCK_REAL) { preloadStockPack('record'); preloadStockPack('book'); preloadStockPack('toy'); }
|
||||
// [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 R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
|
||||
// time (door click), long after citizens is initialized — so the forward reference is safe.
|
||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
|
||||
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id) });
|
||||
// [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".
|
||||
@ -185,7 +191,7 @@ if (!rosterV1) {
|
||||
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 });
|
||||
shopsByChunk.get(k).push({ x, z, hours: s.hours, shopId: s.id }); // [R9] shopId → occupancy truth (Lane D)
|
||||
}
|
||||
citizens.setShops(shopsByChunk);
|
||||
if (params.get('patronage') === '0') citizens.setPatronage(false); // off-switch
|
||||
@ -200,6 +206,9 @@ 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 };
|
||||
// [Lane F R9 — Lane B tram] ?tram=1: one seeded tram looping the main-street spine, door-dwell at the
|
||||
// bus shelters. Self-contained (2 draws); flag-off never constructs it (byte-identical). Non-blocking for v2.0.
|
||||
const tram = (params.get('tram') && params.get('tram') !== '0') ? createTram({ scene, plan, camera, lighting }) : null;
|
||||
|
||||
// spawn on the footpath near the west end of the strip, looking east down main street
|
||||
player.teleport(-96, 9, -Math.PI / 2);
|
||||
@ -305,6 +314,7 @@ function frame() {
|
||||
// [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
|
||||
if (tram) tram.update(dt); // [Lane F R9] Lane B tram loop
|
||||
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);
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
import { buildInterior, getStockPack, makeStockAdapter } from '../interiors/interiors.js';
|
||||
import { KeeperManager } from '../citizens/keepers.js'; // Lane D — shopkeeper behind the counter
|
||||
import { createDig, binSeed } from '../interiors/dig.js'; // Lane C — crate-riffle (v2, gated on ?dig=1)
|
||||
import { collapseBuyItem } from '../interiors/stockpack.js'; // Lane C — R9 buy-anywhere (book spines / toy boxes)
|
||||
|
||||
const EYE = 1.6; // interior eye height (matches interior_test.html; rooms tuned for it)
|
||||
const RADIUS = 0.30; // player collision radius inside the room
|
||||
@ -26,7 +27,8 @@ 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, wallet = null }) {
|
||||
dig: digEnabled = false, stockReal = false, wallet = null,
|
||||
occupancyOf = null }) {
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#0e0b07');
|
||||
|
||||
@ -90,11 +92,41 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
window.dispatchEvent(new CustomEvent('procity:digOpen')); // shell releases pointer-lock for the cursor
|
||||
}
|
||||
|
||||
if (digEnabled) {
|
||||
// [F R9 — Lane C buy-anywhere] E aimed at a book spine / toy box shelf (userData.buyMesh) buys the
|
||||
// nearest item over the same wallet: collapse the quad in place + bank it. Parody stock has no buyMesh
|
||||
// → no-op ("not for sale"). Records keep the dig. Reuses collapseBuyItem (Lane C stockpack export).
|
||||
const _buyRay = new THREE.Raycaster(), _lp = new THREE.Vector3();
|
||||
function buyShelfUnderAim() {
|
||||
if (!wallet || !current) return;
|
||||
const meshes = [];
|
||||
current.group.traverse((o) => { if (o.userData && o.userData.buyMesh && o.userData.buyItems && o.userData.buyItems.length) meshes.push(o); });
|
||||
if (!meshes.length) return;
|
||||
_buyRay.setFromCamera({ x: 0, y: 0 }, camera);
|
||||
const hit = _buyRay.intersectObjects(meshes, false)[0];
|
||||
if (!hit || hit.distance > 3.0) return;
|
||||
const mesh = hit.object;
|
||||
_lp.copy(hit.point); mesh.worldToLocal(_lp);
|
||||
let best = null, bd = Infinity;
|
||||
for (const it of mesh.userData.buyItems) {
|
||||
if (it.sold) continue;
|
||||
const d = (it.center.x - _lp.x) ** 2 + (it.center.y - _lp.y) ** 2 + (it.center.z - _lp.z) ** 2;
|
||||
if (d < bd) { bd = d; best = it; }
|
||||
}
|
||||
if (!best) return;
|
||||
const item = best.item || {};
|
||||
if (wallet.buy({ title: item.title, artist: item.artist, price: item.price || 20 })) {
|
||||
collapseBuyItem(mesh, best); // zero-area the bought quad in place
|
||||
flashToast(`SOLD — ${item.title || 'item'} · $${item.price || 20} · $${wallet.cash()} left`);
|
||||
} else {
|
||||
flashToast('not enough cash');
|
||||
}
|
||||
}
|
||||
|
||||
if (digEnabled || wallet) {
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.code !== 'KeyE' || !current || (dig && dig.active)) return;
|
||||
const bin = binUnderAim();
|
||||
if (bin) openDig(bin);
|
||||
if (digEnabled) { const bin = binUnderAim(); if (bin) { openDig(bin); return; } } // bin → riffle
|
||||
buyShelfUnderAim(); // shelf → buy
|
||||
});
|
||||
}
|
||||
|
||||
@ -106,6 +138,16 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
'font:14px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;text-shadow:0 1px 2px #000;pointer-events:none';
|
||||
document.body.appendChild(banner);
|
||||
|
||||
// [F R9] transient buy-feedback toast (shelf-buy). Separate from the persistent banner.
|
||||
const toast = document.createElement('div');
|
||||
toast.style.cssText =
|
||||
'position:fixed;left:50%;bottom:60px;transform:translateX(-50%);z-index:21;display:none;' +
|
||||
'background:rgba(14,28,18,.92);color:#8fffb0;padding:8px 16px;border-radius:8px;' +
|
||||
'font:600 14px -apple-system,Segoe UI,Roboto,sans-serif;text-shadow:0 1px 2px #000;pointer-events:none';
|
||||
document.body.appendChild(toast);
|
||||
let _toastT = 0;
|
||||
function flashToast(msg) { toast.textContent = msg; toast.style.display = 'block'; _toastT = 1.6; }
|
||||
|
||||
// interior-grid collision — mirrors interior_test.html walkable(): room bounds + occ==1 fittings.
|
||||
function walkable(x, z) {
|
||||
const g = current._debug.grid;
|
||||
@ -142,6 +184,19 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
const st = counter.userData.keeperStand;
|
||||
keepers.spawn(current.group, { x: st.x, z: st.z, ry: st.ry, shopId: shop.id, type: shop.type });
|
||||
}
|
||||
// [F R9 — interior presence, C→D→F seam] browser rigs at C's browse points, one per patronage
|
||||
// occupant, capped at 3 (≤3 points). D owns occupancy truth + the rig (KeeperManager browse pose,
|
||||
// ~1 draw, no player-greet); F reads occupancy and places them. Freed with the room by exit()'s
|
||||
// keepers.disposeAll(). occupants[i].pedIndex → the exact ped who ducked in off the street.
|
||||
if (occupancyOf && current.browsePoints && current.browsePoints.length) {
|
||||
const occ = occupancyOf(shop.id) || { count: 0, occupants: [] };
|
||||
const pts = current.browsePoints.slice(0, Math.min(occ.count | 0, 3));
|
||||
pts.forEach((pt, i) => keepers.spawn(current.group, {
|
||||
x: pt.x, z: pt.z, ry: pt.ry, shopId: shop.id, browse: true,
|
||||
pedIndex: occ.occupants && occ.occupants[i] ? occ.occupants[i].pedIndex : null,
|
||||
seedKey: `${shop.id}#${i}`,
|
||||
}));
|
||||
}
|
||||
doorReturn = { x: camera.position.x, y: camera.position.y, z: camera.position.z, ry: camera.rotation.y };
|
||||
camera.position.set(current.spawn.x, EYE, current.spawn.z);
|
||||
camera.rotation.set(0, current.spawn.ry, 0, 'YXZ'); // PointerLockControls re-reads this on next mouse move
|
||||
@ -158,6 +213,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
// (the shell then calls exit() + setMode('street') — update never disposes on its own).
|
||||
function update(dt, keys) {
|
||||
if (!current) return false;
|
||||
if (_toastT > 0) { _toastT -= dt; if (_toastT <= 0) toast.style.display = 'none'; } // [R9] buy toast decay
|
||||
// [F2] riffling: the dig owns the whole frame (its own scene/camera + cursor input). No interior
|
||||
// walk, no exit check — the player is "in the crate" until they close it (Esc / WALK OUT button).
|
||||
if (dig && dig.active) {
|
||||
@ -203,6 +259,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
currentShop = null;
|
||||
currentAdapter = null;
|
||||
banner.style.display = 'none';
|
||||
toast.style.display = 'none'; _toastT = 0;
|
||||
if (doorReturn) {
|
||||
camera.position.set(doorReturn.x, doorReturn.y, doorReturn.z);
|
||||
camera.rotation.set(0, doorReturn.ry, 0, 'YXZ');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user