Lane F R5 F2: wire ?dig=1 crate-riffle into interior mode
C shipped dig.js + the flag but left the in-game input/mode hook to F (LANE_C_NOTES C2). interior_mode.js (F-owned): aim at a userData.kind==='bin' fitting + press E -> createDig + dig.open (per-bin seed binSeed(shop.seed,pos)); the dig owns the frame (renders dig.scene, suppresses interior walk); WALK OUT/Esc closes back to the room; closed on interior exit. Shell (index.html): ?dig=1 -> dig: opt; procity:digOpen releases pointer-lock (riffle is cursor-driven); the unlock->leaveShop guard reads interiorMode.digActive so opening a bin isn't read as walking out; canvas re-locks for interior walk after close. Fresh-chromium smoke: E opens the riffle (seeded sleeves + prices render), WALK OUT closes to the interior, leak-free (steady-state geo/tex flat over 3 open/close cycles), 0 console errors. Flag off => no listener, dig stays null => byte-identical to v1 (prime law). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
019304d647
commit
26d68cdb4f
@ -15,7 +15,8 @@ Full tracking in [`LANE_F_NOTES §10`](docs/LANES/LANE_F_NOTES.md).
|
||||
| **archive R3/R4 instruction claims** (F4) | ✅ Added "SUPERSEDED by R5" banner to `ROUND3_INSTRUCTIONS.md` + `ROUND4_INSTRUCTIONS.md`. |
|
||||
| **v2 flags table** (F4) | ✅ Scaffolded in LANE_F_NOTES §10 (`?plansrc=osm` A, `?winmap=1` B, `?dig=1` C, `?roster=stream` D) — all pending owning-lane specs. |
|
||||
| **F1 tag v1.1** | ✅ **TAGGED.** C1 (`aa9c140`: record_crate fixed + counter_till re-map) + E1 (`c522464`: 6 hero props baked 288k→48k, ≤8k each, live on depot) landed. Verified: depot serves baked props (counter_till 7999, drinks_fridge 8000, listening_booth 7986 tris ≤8k), C's `glb.js` maps them, record/opshop interiors render the **counter_till GLB** with keeper stand pose working + no double till, bins upgraded. Re-captured the 2 interior tour shots (fresh chromium → baked props). qa GREEN. `git tag v1.1` (local). |
|
||||
| **F2 wire v2 seams** | ⏳ pending lanes documenting call-sites (esp. C's `?dig=1` interior-mode input seam — real work, F owns the mode machine). |
|
||||
| **F2 wire `?dig=1`** | ✅ **DONE + verified.** C shipped `dig.js` + the flag but left the in-game input/mode hook to F (LANE_C_NOTES §C2). Wired the consumer pattern into `interior_mode.js` (F-owned): aim at a `userData.kind==='bin'` fitting + press **E** → `createDig`/`dig.open` (per-bin seed via `binSeed(shop.seed,pos)`); the dig owns the frame (render `dig.scene`, no interior walk); WALK OUT / Esc closes → back to the room. Shell seam (`index.html`): `?dig=1` → `dig:` opt; `procity:digOpen` releases pointer-lock (riffle is cursor-driven); unlock→leaveShop guard reads `interiorMode.digActive` so opening a bin ≠ leaving. **Fresh-chromium smoke:** E opens riffle (seeded sleeves + prices render), WALK OUT closes to interior, **leak-free** (steady-state geo/tex flat over 3 cycles), **0 console errors**. Flag off ⇒ no listener/no dig ⇒ byte-identical to v1. |
|
||||
| **F2 — other v2 seams** | B `?winmap` / D `?roster=stream` are self-contained (no shell input hook needed); smoked clean (boot, 0 errors, budget OK). A `?plansrc` not taken. |
|
||||
| **F3 v2 QA harness** | ⏳ determinism gate (first-interior-hash per seed) · flags-off regression check (v2-prime-law enforcement) · v2 smoke → qa.sh warn-gate. Build as flags land. |
|
||||
|
||||
Base state: v1.0 (`2698b40`), `qa.sh --strict` GREEN 4/4, tree clean.
|
||||
|
||||
BIN
docs/shots/laneF/dig_closed.png
Normal file
BIN
docs/shots/laneF/dig_closed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/shots/laneF/dig_open.png
Normal file
BIN
docs/shots/laneF/dig_open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 107 KiB |
@ -134,7 +134,13 @@ const minimap = createMinimap(plan);
|
||||
const fleet = NOASSETS ? null : loadPedFleet('models/peds/');
|
||||
if (!NOASSETS) preloadManifest(); // prime the manifest so the first interior can upgrade its fittings
|
||||
// [Lane F integration] interior bridge — owns the 'interior' branch (Lane C buildInterior + Lane D keeper)
|
||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS });
|
||||
// [Lane F §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1.
|
||||
const DIG_ON = params.has('dig') && params.get('dig') !== '0';
|
||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, dig: DIG_ON });
|
||||
// [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".
|
||||
window.addEventListener('procity:digOpen', () => player.unlock());
|
||||
// [Lane F integration] street pedestrians (Lane D). graph = plan.streets directly; with the rig fleet the
|
||||
// near tier shares GLB meshes (cheap). `?pop=` tunable. setTimeOfDay/update in the street loop.
|
||||
const citizens = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets, fleet });
|
||||
@ -198,9 +204,9 @@ addEventListener('blur', () => keys.clear());
|
||||
const startPanel = document.getElementById('pc-start');
|
||||
document.getElementById('pc-startseed').textContent = `${plan.name} · seed ${plan.citySeed}`;
|
||||
document.getElementById('pc-go').addEventListener('click', () => { startPanel.style.display = 'none'; player.lock(); });
|
||||
player.controls.addEventListener('unlock', () => { keys.clear(); if (MODE === 'interior') leaveShop(); }); // [Lane F] Esc leaves the shop
|
||||
player.controls.addEventListener('unlock', () => { keys.clear(); if (MODE === 'interior' && !interiorMode.digActive) leaveShop(); }); // [Lane F] Esc leaves the shop — but not when the unlock is us opening a dig (§F2)
|
||||
// click the world to (re)grab the pointer when walking
|
||||
canvas.addEventListener('click', () => { if (MODE === 'street' && !player.isLocked) player.lock(); });
|
||||
canvas.addEventListener('click', () => { if (!player.isLocked && !interiorMode.digActive && (MODE === 'street' || MODE === 'interior')) player.lock(); }); // [Lane F §F2] re-lock for interior walk after closing a dig
|
||||
|
||||
addEventListener('resize', () => {
|
||||
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
import { buildInterior } 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)
|
||||
|
||||
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
|
||||
@ -24,7 +25,7 @@ const SPEED = 3.2; // interior walk speed (matches the Lane C tester)
|
||||
const ARM_DIST = 2.0; // must step this far from the door before the exit arms (spawn is ~1.5m away)
|
||||
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 }) {
|
||||
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false, dig: digEnabled = false }) {
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#0e0b07');
|
||||
|
||||
@ -33,12 +34,56 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
const keepers = new KeeperManager({ camera, citySeed: plan.citySeed, fleet });
|
||||
|
||||
let current = null; // active Lane C interior handle
|
||||
let currentShop = null; // the shop record for the active interior (dig per-bin seeding)
|
||||
let doorReturn = null; // { x, y, z, ry } on the street to restore on exit
|
||||
let exitArmed = false; // disarmed at spawn (spawn sits by the door); arms once player steps inside
|
||||
let dig = null; // [F2] Lane C crate-riffle instance, created lazily on first use (only if ?dig=1)
|
||||
|
||||
const _fwd = new THREE.Vector3();
|
||||
const _right = new THREE.Vector3();
|
||||
const _up = new THREE.Vector3(0, 1, 0);
|
||||
const _digRay = new THREE.Raycaster();
|
||||
|
||||
// [Lane F §F2] crate-riffle wiring (v2, ?dig=1). Lane C ships dig.js + gates the flag; F owns the
|
||||
// in-game *input/mode* hook (its LANE_C_NOTES contract) — the same consumer pattern as Lane C's
|
||||
// interior_test.html: aim at a record bin, press E → open the riffle, which owns the frame + cursor
|
||||
// (pointer must be UNLOCKED — the shell handles that on procity:digOpen). Flag off ⇒ none of this runs.
|
||||
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); // crosshair-centre ray
|
||||
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;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openDig(bin) {
|
||||
if (!dig) dig = createDig(THREE, renderer);
|
||||
if (dig.active) return;
|
||||
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 pos)
|
||||
dig.open({
|
||||
seed: binSeed((currentShop && currentShop.seed) || 1, key), count: 16,
|
||||
shopName: (current && current.recipe && current.recipe.label) || (currentShop && currentShop.name),
|
||||
shop: currentShop,
|
||||
onClose: () => window.dispatchEvent(new CustomEvent('procity:digClose')),
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent('procity:digOpen')); // shell releases pointer-lock for the cursor
|
||||
}
|
||||
|
||||
if (digEnabled) {
|
||||
addEventListener('keydown', (e) => {
|
||||
if (e.code !== 'KeyE' || !current || (dig && dig.active)) return;
|
||||
const bin = binUnderAim();
|
||||
if (bin) openDig(bin);
|
||||
});
|
||||
}
|
||||
|
||||
// a small unobtrusive banner (F-owned; the street HUD is hidden in interior mode)
|
||||
const banner = document.createElement('div');
|
||||
@ -73,6 +118,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
function enter(shop, name) {
|
||||
if (current) exit(); // never stack interiors
|
||||
current = buildInterior(shop, THREE, { useGLB }); // [Lane F §3.4] depot GLB fittings when assets are on
|
||||
currentShop = shop;
|
||||
scene.add(current.group);
|
||||
// Lane D keeper at the counter — Lane C tags the stand pose on the counter interactable
|
||||
const counter = current.places.find((p) => p.userData && p.userData.keeperStand);
|
||||
@ -84,7 +130,10 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
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
|
||||
exitArmed = false;
|
||||
banner.textContent = `🚪 ${name || shop.name || 'shop'} — walk out the door or press Esc to leave`;
|
||||
let hasBins = false;
|
||||
if (digEnabled) current.group.traverse((o) => { if (o.userData && o.userData.kind === 'bin') hasBins = true; });
|
||||
banner.textContent = `🚪 ${name || shop.name || 'shop'} — walk out the door or press Esc to leave`
|
||||
+ (hasBins ? ' · E: riffle a record bin' : '');
|
||||
banner.style.display = 'block';
|
||||
return current;
|
||||
}
|
||||
@ -93,6 +142,14 @@ 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;
|
||||
// [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) {
|
||||
if (dig.update) dig.update(dt);
|
||||
renderer.autoClear = true;
|
||||
renderer.render(dig.scene, dig.camera);
|
||||
return false;
|
||||
}
|
||||
const f = (keys.has('KeyW') || keys.has('ArrowUp') ? 1 : 0) - (keys.has('KeyS') || keys.has('ArrowDown') ? 1 : 0);
|
||||
const s = (keys.has('KeyD') || keys.has('ArrowRight') ? 1 : 0) - (keys.has('KeyA') || keys.has('ArrowLeft') ? 1 : 0);
|
||||
if (f || s) {
|
||||
@ -120,9 +177,11 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
// exit: dispose the room (frees GPU + removes group), restore the player to the street door.
|
||||
function exit() {
|
||||
if (!current) return;
|
||||
if (dig && dig.active) dig.close(); // [F2] close an open riffle before leaving (frees its per-open GPU)
|
||||
keepers.disposeAll(); // Lane D: free the keeper actor(s) before the room
|
||||
current.dispose();
|
||||
current = null;
|
||||
currentShop = null;
|
||||
banner.style.display = 'none';
|
||||
if (doorReturn) {
|
||||
camera.position.set(doorReturn.x, doorReturn.y, doorReturn.z);
|
||||
@ -138,5 +197,6 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
keepers,
|
||||
get active() { return !!current; },
|
||||
get current() { return current; },
|
||||
get digActive() { return !!(dig && dig.active); }, // [F2] shell reads this to keep pointer-lock/Esc sane while riffling
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user