Lane C: keeper stand pose on the counter contract (re-land on main)
buildInterior now returns room.counter = { mesh, pose, stand } and tags the counter
interactable with userData.keeperStand. stand = {x,z,ry} where a shopkeeper stands
(service side, facing the customer, rig-front = local -Z per keepers.js), clamped
in-bounds. Deterministic; purely additive.
Note: commit 4235060 carries this message but actually contains Lane A's round-2
content (shared-index race, flagged in 3e98f05). This commit lands the real Lane C
change — identical content to stray branch lane-c/keeper-stand (85438b5), which is
now redundant. Verified: 36-build sweep + 50-room soak green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
063a959e29
commit
8577c48594
@ -3,7 +3,52 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-14 · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-14 (b) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-14 (b) — keeper stand pose (resolves Lane F §8 · unblocks §3.2/§3.3)
|
||||
|
||||
Lane F's runbook asked (LANE_F_NOTES §8): *"does `places` tag the counter slot with a stable
|
||||
`kind:'counter'` so keepers find it?"* **Yes** — the counter interactable has always carried
|
||||
`mesh.userData.kind === 'counter'` (+ `interactable:true`). But Lane D's keeper needs more than a
|
||||
tag: `keepers.spawn(group,{x,z,ry})` needs *where the shopkeeper stands and which way it faces*
|
||||
(keepers.js:28 — "`ry` = the counter's outward facing"). I computed the customer-facing side
|
||||
internally but never surfaced it — so Lane D would have read the bench's raw transform and stood a
|
||||
keeper embedded in the counter, possibly facing a wall. Closed that gap (my files only, purely additive):
|
||||
|
||||
- **New: `room.counter = { mesh, pose:{x,z,ry}, stand:{x,z,ry} }`** (interiors.js). `pose` is the
|
||||
bench; **`stand` is the keeper pose** — behind the bench on the service side, facing the customer,
|
||||
clamped inside the room, in **room-local space** (the same space Lane D adds the keeper into via
|
||||
`interior.group`). `ry` uses the rig-front = local −Z convention keepers.js expects.
|
||||
- **The same `{x,z,ry}` also rides the counter interactable**: `place.userData.keeperStand` — for the
|
||||
`places`-iteration path Lane D/F described. Deep-equal to `room.counter.stand`.
|
||||
|
||||
**Lane D/F wiring (drop-in for §3.2/§3.3):**
|
||||
```js
|
||||
const room = buildInterior(shop, THREE);
|
||||
const s = room.counter.stand; // or: room.places.find(p=>p.userData.kind==='counter').userData.keeperStand
|
||||
keepers.spawn(room.group, { ...s, shopId: shop.id, type: shop.type });
|
||||
// on exit: keepers.remove(handle) / keepers.disposeAll(), then room.dispose()
|
||||
```
|
||||
|
||||
Verified in-browser (9 types × 4 seeds = 36 builds, + 50-room soak):
|
||||
`stand in-bounds ✓ · faces-customer ✓ (dot>0 for all) · places-carries-keeperStand ✓ (deep-equal) ·
|
||||
stand deterministic ✓ · placement determinism unchanged ✓ · soak avg 3.6ms / worst 7.7ms · leak geo0 tex0 ·
|
||||
pathFail 0 · every room has a counter+stand`. No change to layout/placement of existing rooms.
|
||||
|
||||
**Minor known cosmetic (corner counters):** a corner counter sits ~0.55 m off the east wall, so the
|
||||
service gap (~0.25 m) is narrower than a person — the keeper stands *at* the till with a small hidden
|
||||
overlap into the solid waist-high bench (torso reads above the counter, legs behind it). Facing and
|
||||
side are correct; it reads as "keeper at the till." If Lane D/Fable want real standing clearance I can
|
||||
nudge corner counters ~0.2 m off the wall — but that changes those rooms' committed placement +
|
||||
screenshots, so it's a decision, not a bug.
|
||||
|
||||
**Two contract-drift corrections for the F runbook** (§2's Lane-C line was written against an earlier
|
||||
draft of my API): (1) there is **no `opts.registry`** — registry override is the one-time global
|
||||
`mergeRegistry(registry)` (a per-build registry mutation broke same-seed determinism; removed in the
|
||||
review pass). (2) `recipe.counterPos` is a **placement-strategy string** (`door|forward|back|corner`),
|
||||
**not** a world position — use `room.counter.stand` for the keeper pose.
|
||||
|
||||
---
|
||||
|
||||
@ -41,6 +86,7 @@ room.dispose(); // frees ALL per-room GPU resources; removes group
|
||||
// spawn: { x, z, ry }, // enter just inside the door, facing −Z (into the shop)
|
||||
// exits: [ { x, z, w, toStreet } ], // door back to street
|
||||
// places: [ …meshes w/ userData ], // interactables: counter, bin, case, fridge, returns, exit
|
||||
// counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = keeper (Lane D) spawn pose
|
||||
// dims: { W, D, H, archetype, type },
|
||||
// placement, // deterministic summary (deep-equal per seed)
|
||||
// pathOK, buildMs, counts(), dispose()
|
||||
@ -120,6 +166,9 @@ Serve: `cd web && python3 -m http.server 8130` → open `/interior_test.html`.
|
||||
- **Lane E `web/assets/manifest.json`** — its `fittings` section (9 depot GLBs with footprints) is now
|
||||
wired through `glb.js` (opt-in `useGLB`, primitive fallback). Depot GLBs may not be uploaded yet;
|
||||
verified the fallback (unreachable depot → primitives persist, no crash, no leak).
|
||||
- **Lane D `citizens/keepers.js`** — its `keepers.spawn(group,{x,z,ry,shopId,type})` now has a clean
|
||||
source: `room.counter.stand` (also on `place.userData.keeperStand`). See the *Update (b)* section at
|
||||
the top. Verified the pose is on the service side, faces the customer, and is deterministic.
|
||||
|
||||
## Adversarial review — 5 findings, all fixed
|
||||
|
||||
|
||||
@ -132,7 +132,8 @@ function updateHUD() {
|
||||
`room <b>${d.W.toFixed(1)}×${d.D.toFixed(1)}×${d.H.toFixed(1)}</b> m\n` +
|
||||
`build <b>${current.buildMs.toFixed(1)}</b> ms · path <b>${current.pathOK ? 'ok' : 'FAIL'}</b>${current.carved ? ' (carved)' : ''}\n` +
|
||||
`geo ${c.geometries} · mat ${c.materials} · tex ${c.canvasTextures}\n` +
|
||||
`places ${current.places.length} · counter ${current.recipe.counterPos}`;
|
||||
`places ${current.places.length} · counter ${current.recipe.counterPos}` +
|
||||
(current.counter ? `\nkeeper stand ${current.counter.stand.x.toFixed(2)}, ${current.counter.stand.z.toFixed(2)} · ry ${current.counter.stand.ry.toFixed(2)}` : '');
|
||||
}
|
||||
|
||||
// ── debug overlays ───────────────────────────────────────────────────────────
|
||||
@ -167,6 +168,19 @@ function buildPathHelper() {
|
||||
grp.add(m);
|
||||
};
|
||||
mk(start, '#3ad66a'); mk(target, '#ffd75e');
|
||||
// keeper stand pose (Lane D spawns a shopkeeper here) — magenta post + a white nose showing the
|
||||
// facing (local −Z = rig front, the convention keepers.js uses).
|
||||
const ks = current.counter && current.counter.stand;
|
||||
if (ks) {
|
||||
const post = new THREE.Mesh(new THREE.CylinderGeometry(0.13, 0.13, 1.6, 12),
|
||||
new THREE.MeshBasicMaterial({ color: '#d63ad6', transparent: true, opacity: 0.6 }));
|
||||
post.position.set(ks.x, 0.8, ks.z);
|
||||
const noseGrp = new THREE.Group();
|
||||
const nose = new THREE.Mesh(new THREE.BoxGeometry(0.07, 0.07, 0.5), new THREE.MeshBasicMaterial({ color: '#ffffff' }));
|
||||
nose.position.set(0, 0, -0.4); // local −Z = the way the keeper faces
|
||||
noseGrp.add(nose); noseGrp.position.set(ks.x, 1.4, ks.z); noseGrp.rotation.y = ks.ry;
|
||||
grp.add(post, noseGrp);
|
||||
}
|
||||
pathHelper = grp; scene.add(grp);
|
||||
}
|
||||
function applyWire() {
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
// spawn: { x, z, ry }, // where/what-heading the player enters at (just inside the door)
|
||||
// exits: [ { x, z, w, toStreet } ], // door(s) back to the street
|
||||
// places: [ …meshes with userData ], // interactables: counter, bins, case, fridge, returns, exit
|
||||
// counter: { mesh, pose, stand }, // pay point. stand:{x,z,ry} = where a shopkeeper (Lane D)
|
||||
// // stands, in room-local space, facing the customer.
|
||||
// dims: { W, D, H, archetype, type }, // room metrics
|
||||
// placement, // deterministic placement summary (deep-equal per seed)
|
||||
// pathOK, // door→counter connectivity held (always true; carved if needed)
|
||||
@ -97,6 +99,7 @@ export function buildInterior(shop, THREE, opts) {
|
||||
spawn: shell.spawn,
|
||||
exits: shell.exits,
|
||||
places: lay.places,
|
||||
counter: lay.counter, // { mesh, pose:{x,z,ry}, stand:{x,z,ry} } — Lane D keeper spawn pose
|
||||
dims: { ...dims, archetype, type: recipe.key },
|
||||
recipe: { key: recipe.key, label: recipe.label, counterPos: recipe.counterPos, clutter: recipe.clutter },
|
||||
placement: lay.placement,
|
||||
|
||||
@ -104,7 +104,24 @@ function placeCounter(ctx, recipe, W, D, r) {
|
||||
const f = buildFitting('counter', ctx, { w: cw }, r);
|
||||
f.group.position.set(x, 0, z); f.group.rotation.y = ry;
|
||||
const [hw, hd] = halfExtents(f.footprint.w, f.footprint.d, ry);
|
||||
return { fitting: f, x, z, ry, hw, hd, front, priority: 9, kind: 'counter' };
|
||||
const stand = keeperStand(x, z, front, W, D);
|
||||
return { fitting: f, x, z, ry, hw, hd, front, stand, priority: 9, kind: 'counter' };
|
||||
}
|
||||
|
||||
// The pose a shopkeeper (Lane D) stands at: behind the bench on the service side (opposite the
|
||||
// customer-facing `front`), facing the customer. Clamped inside the room so a corner/back-wall
|
||||
// counter keeper never clips through a wall. `ry` uses the rig-front = local −Z convention that
|
||||
// keepers.js expects (spawn's ry is "the counter's outward facing"). Deterministic — a pure
|
||||
// function of the (already-seeded) counter pose.
|
||||
function keeperStand(cx, cz, front, W, D) {
|
||||
let dx = front.x - cx, dz = front.z - cz; // counter → customer
|
||||
const len = Math.hypot(dx, dz) || 1; dx /= len; dz /= len;
|
||||
const off = 0.5; // stand this far back from the bench centre
|
||||
let x = cx - dx * off, z = cz - dz * off; // opposite the customer side (behind the bench)
|
||||
const mx = W / 2 - 0.45, mz = D / 2 - 0.45; // keep clear of the walls
|
||||
x = Math.max(-mx, Math.min(mx, x));
|
||||
z = Math.max(-mz, Math.min(mz, z));
|
||||
return { x: round(x), z: round(z), ry: round(Math.atan2(-dx, -dz)) };
|
||||
}
|
||||
|
||||
// ── wall-slot system (shuffled pool → art + signs + wall fittings never overlap) ──────
|
||||
@ -159,6 +176,10 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
counter.rect = rectCells(grid, counter.x, counter.z, counter.hw, counter.hd);
|
||||
stampRect(grid, counter.rect);
|
||||
placed.push(counter);
|
||||
// Surface the keeper stand pose on the counter's interactable so Lane D (keepers.js) can spawn a
|
||||
// shopkeeper behind it without guessing which side is the service side. Also exposed top-level as
|
||||
// room.counter.stand (see interiors.js). The counter is never path-pulled, so this always survives.
|
||||
counter.fitting.group.userData.keeperStand = counter.stand;
|
||||
// reserve the strip in front of the till so nothing blocks service
|
||||
reserveRect(grid, counter.front.x - 0.5, counter.front.z - 0.5, counter.front.x + 0.5, counter.front.z + 0.5);
|
||||
|
||||
@ -229,6 +250,11 @@ export function layout(ctx, { recipe, dims, shell, adapter, shop }) {
|
||||
placed: placed.filter(p => !p.removed),
|
||||
places, decor: decor.meshes, grid, pathOK, carved,
|
||||
spawnCell, targetCell, placement,
|
||||
counter: {
|
||||
mesh: counter.fitting.group,
|
||||
pose: { x: round(counter.x), z: round(counter.z), ry: round(counter.ry) }, // the bench itself
|
||||
stand: counter.stand, // where the keeper stands
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user