Lane C round-9 handshake: browse-points interface (C→D→F seam)

Publishes decision #1's contract FIRST (before code) so D and F build
in parallel: room.browsePoints[] (0..3, deterministic per shop.seed) —
{x,z,ry,atKind,slotIndex}, same coord frame + ry convention as
counter.stand. Guarantees: walkable, reachable (door→counter flood-
fill), ≥0.6m spaced, every archetype. C adds no meshes/draws; D's rigs
ride the ≤350 interior budget. Includes the D→F→C handoff (occupancy
count → browsePoints.slice(0,min(count,3)) → one rig per point).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-15 09:27:12 +10:00
parent e8f92a0da2
commit 5529d62263

View File

@ -1,5 +1,57 @@
# LANE C — cross-lane notes (PROCITY-C)
## → Lane D + Lane F: BROWSE-POINTS interface (round-9 C1 handshake — AGREE BEFORE CODE)
This is decision #1's contract, published *first* so D and F can build in parallel. It is the seam the
whole round hangs off. **C owns the points + room fit; D owns occupancy truth; F owns the handoff.**
### What C exposes on the room API (live once C1 commits)
```jsonc
room.browsePoints: BrowsePoint[] // 0..3 entries, DETERMINISTIC per shop.seed, stably ordered
BrowsePoint = {
x, z, // room-local floor position (metres). Origin = room centre, +Z = door/street side,
// Z = back — the SAME frame as room.spawn / room.counter.stand.
ry, // facing yaw, oriented to look AT the shelf. SAME convention as room.counter.stand.ry
// (rig-front = local Z, i.e. the value keepers.js already consumes). Drop your
// browser rig in with rotation.y = ry and it faces the goods.
atKind, // string — the fitting kind being browsed ('bin' | 'shelf' | 'rack' | 'table' | 'case' …).
// Purely informational (pick a browse clip by it if you like); not load-bearing.
slotIndex, // 0-based, == array position. Stable across rebuilds of the same seed.
}
```
**Guarantees C makes about every point** (drawSweep + a new browse-collision assert prove them):
- **Walkable**: the cell is floor-free (occ==0) — never inside a fitting footprint, wall, door corridor
reserve, or the counter-front service strip.
- **Reachable**: connected to the spawn cell by the same 4-connected flood-fill that guards door→counter.
- **Spaced**: points are ≥0.6 m apart from each other AND from the keeper stand, so rigs don't interpenetrate.
- **Deterministic**: pure function of `shop.seed`; same seed → same points, same order.
- **Every archetype**: cosy/gallery/wide/hall/pokey all yield ≥1 point when the room has ≥1 browsable
floor fitting (a bare room — e.g. a tiny pokey with only a counter — may yield 0; treat 0 as "no browsers").
C adds **no meshes and no draws** for browse points — they are pure poses. The ≤350 law is unaffected by C1;
the only draw cost is D's rigs, which ride the interior budget (see below).
### The D → F → C handoff (who calls whom)
1. **D owns occupancy truth.** Propose `occupancyOf(shopId) → { count, occupants:[{ seed, enteredAt }] }`
(your shape — C doesn't consume it). Count is however many peds patronage says are inside, pre-cap.
2. **F wires it at interior build.** On enter, F reads D's occupancy, then:
`const pts = room.browsePoints.slice(0, Math.min(occ.count, 3));` and asks D's fleet to stand one
browser rig per `pt` at `(pt.x, pt.z)`, `rotation.y = pt.ry`. **Cap 3** is enforced by there being ≤3
points — F just clamps count. Identity: pair `occupants[i].seed` with `pts[i]` so the same ped is the
same rig deterministically.
3. **Rig lifecycle is D's** (KeeperManager-style fleet clone, the leak-proven keeper pattern): idle/browse
pose, dispose on player exit, despawn on dwell-expiry. Browsers count against the interior ≤350 — C's
soak will run WITH browsers injected at the points to prove keeper+stock+dig+browsers all compose.
**Open question for A (via D):** closing-time behaviour for occupants — A rules, D implements, C's points
don't care (a closed shop simply isn't entered). No blocker on C.
If any field above doesn't fit D's rig spawner or F's wiring, flag it here **before** I finalize — but the
shape mirrors `counter.stand` exactly, which both of you already consume, so it should slot straight in.
## → Lane F: buy loop v0 + book/toy packs — two warn-level smokes (round-8 C1/C2 landed)
**Buy loop** is a runtime-only wallet in `web/js/interiors/wallet.js` — pure, headless-testable, no DOM: