348 lines
26 KiB
Markdown
348 lines
26 KiB
Markdown
# LANE F — NOTES (integration runbook + QA gates)
|
||
|
||
*Lane F is integration + QA. It runs **after** A–E land. This file is the durable technical spec:
|
||
the exact wiring edits, the contracts F needs from each lane, and the QA gate procedures. It is
|
||
written **before** A–E finished so the wiring is a fast, mechanical pass the moment they do —
|
||
and so each lane can see the small hook F will ask of it and build it in up front.*
|
||
|
||
Status of this file: **living**. Update the ✅/⏳ markers as wiring lands. Snapshot of lane
|
||
readiness lives in [`F-progress.md`](../../F-progress.md); run `node tools/qa/scaffold_check.mjs`
|
||
for the live matrix.
|
||
|
||
---
|
||
|
||
## 0. Ground truth / environment
|
||
|
||
- Repo root on this machine: `/Users/m3ultra/Documents/procity` (the lane prompts say
|
||
`/Users/jing/Documents/PROCITY` — that path is from the authoring machine; **use the real path
|
||
above**). Reference repos are cloned alongside: `../90sDJsim`, `../thriftgod`. Ultra box
|
||
(existing 3D library / Blender / clip bank): `ssh johnking@100.91.239.7`.
|
||
- Serve: `cd web && python3 -m http.server 8130`. Node 26, Python 3.14 present.
|
||
- `web/package.json` has `"type":"module"` (Lane A) so `node web/js/**/*.js` runs as ESM.
|
||
- **F never edits another lane's internals.** F adds *wiring* — new files it owns, and small,
|
||
clearly-marked hook edits at the seams. Where a hook must live inside a lane's file, F first
|
||
looks for the hook that lane already exposed (see contracts below); the lane owns its file, F
|
||
owns the glue.
|
||
|
||
## 1. Files Lane F owns / creates
|
||
|
||
| path | purpose |
|
||
|---|---|
|
||
| `tools/qa.sh` | the QA gate runner (scaffold + citygen selfcheck + manifest validator) ✅ |
|
||
| `tools/qa/scaffold_check.mjs` | scaffold + PRNG determinism law + **live lane-readiness matrix** ✅ |
|
||
| `tools/shots.py` | Playwright tour-capture → `docs/shots/v1_tour/` + contact sheet (ported from 90sDJsim `tools/shots.py`) |
|
||
| `tools/soak.py` | Playwright 10-minute scripted-walk soak (chunk/heap/leak/error gate) |
|
||
| `web/js/citygen/index.js` | barrel so B's `import('./js/citygen/index.js')` resolves to A's `plan.js` ✅ (§3.1) |
|
||
| `web/js/world/interior_mode.js` | **interior bridge** — B's shell ↔ C's `buildInterior`; owns the `interior` mode branch ✅ (§3.2) |
|
||
| `docs/V2_IDEAS.md` | parked-scope catalogue ✅ |
|
||
| `docs/shots/` | reference shot tree (`laneA…E/`, `v1_tour/`, `before/`, `references/`) |
|
||
| `F-progress.md` | status report for fable |
|
||
|
||
The wiring is kept as **focused F-owned modules** (e.g. `interior_mode.js`) that the shell imports,
|
||
rather than logic sprinkled through B's files. Each seam edit inside `index.html` is one line,
|
||
commented `[Lane F integration]`, and reversible. That keeps ownership clean: B owns the shell +
|
||
mode machine, C/D own their libraries, F owns the glue modules and the marked seams that call them.
|
||
|
||
---
|
||
|
||
## 2. Contracts F depends on (verify these as lanes land)
|
||
|
||
Pulled from CITY_SPEC + each lane prompt. The scaffold check's readiness matrix tracks presence;
|
||
these are the **shapes** F wires against. If a lane ships a different shape, fix it here and in
|
||
the wiring, not by guessing.
|
||
|
||
> **⚠ Corrected against the REAL landed code (2026-07-14).** The bullets below are what the lanes
|
||
> actually shipped — several differ from the spec-derived guesses in an earlier draft (verified by
|
||
> reading each file + the Lane F self-audit workflow).
|
||
|
||
**Lane A — `web/js/citygen/plan.js`** *(landed, verified)*
|
||
- `generatePlan(citySeed) → CityPlan` (schema in CITY_SPEC §Layer 1). <3ms, deterministic, JSON-clean.
|
||
- `chunkIndex(plan) → { chunkSize, chunks }` where `chunks["cx,cz"] = { lots:[id…], shops:[…], edges:[…] }`
|
||
(a **plain object, not a callable**; a lot may bucket into several chunks). Also exports `chunkKey(cx,cz)`.
|
||
- `web/js/core/registry.js` exports `SHOP_TYPES` — per type `{ label, facades:[<full filename>.jpg…],
|
||
sign, interior, fittings:[…], storeys:[min,max], hours:{open,close} }`. **`facades` are full
|
||
filenames** (`facade-timber-teal.jpg`), not bare keys — see the skins seam in §7.
|
||
- `web/map.html` — 2D plan viewer (verified rendering real towns). `web/js/citygen/index.js` **is the
|
||
barrel Lane F added** so the shell's `import('./js/citygen/index.js')` resolves (§3.1).
|
||
|
||
**Lane B — `web/index.html` + `web/js/world/*`** *(landed, verified live)*
|
||
- Mode state machine `MODE ∈ {map, street, interior}` (interior branch not yet used), one renderer.
|
||
- **`createChunkManager(plan, scene, ctx)`** — a **factory**, not a class; returns
|
||
`{ update(pos), warmup(pos), getColliders(x,z), setNight(bool), getDoorMeshes(), count, dispose() }`.
|
||
**No `onChunkBuilt/onChunkDisposed` lifecycle hooks** — so §3.3 must drive citizens off the street
|
||
graph, not chunk lifecycle (or F adds a hook; prefer graph-driven).
|
||
- Door pick is wired via HUD: `createHUD({ onEnterShop: (shopId,name)=>enterShop(...) })`; `enterShop()`
|
||
dispatches `procity:enterShop` and is otherwise **a stub** (`hud.showToast('interiors coming')`) —
|
||
this is the seam F completes in §3.2. Interior returns via `procity:exitShop`.
|
||
- HUD shows draws / tris / fps / seed / time-of-day (F reads these for the budget gate).
|
||
- Exposes `window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skins }`
|
||
(F builds the `window.DBG` harness hook on top of this — §4).
|
||
|
||
**Lane C — `web/js/interiors/interiors.js`** *(landed, verified)*
|
||
- `buildInterior(shop, THREE, opts={registry,archetype,stockAdapter}) → { group, spawn:{x,z,ry},
|
||
exits:[…], places:[…], dims, recipe:{counterPos,…}, dispose() }`. Matches the wiring plan.
|
||
- Caller must attach lot dims: pass `{ ...shop, lot: plan.lots.find(l=>l.id===shop.lot) }`.
|
||
`dispose()` frees tracked GPU resources (soak gate asserts baseline).
|
||
|
||
**Lane D — `web/js/citizens/*`** *(landed, verified)*
|
||
- `sim.js` exports **`class CitizenSim`** — `new CitizenSim({ renderer, scene, camera, citySeed,
|
||
graph, fleet, group })`. It is **street-graph driven, not per-chunk**: there is no
|
||
`spawnRoster/despawn/setDaySegment` — build it once from the street graph and tick it in the loop.
|
||
- `keepers.js` exports **`class KeeperManager`** — `new KeeperManager({ camera, citySeed, fleet })`,
|
||
then `spawn(target, { x, z, ry, shopId, type }) → handle`, `remove(handle)`, `disposeAll()`,
|
||
`update(dt, playerPos)`. (Not `spawnFor/disposeFor`.)
|
||
|
||
**Lane E — `web/assets/manifest.json` + `pipeline/validate_manifest.py`**
|
||
- Manifest: `{depot, fittings:{id:{file,footprint,thumb}}, furniture:{…}, skins:{facade,sky,ground,wall,interior}}`.
|
||
- `python3 pipeline/validate_manifest.py` green (F's gate 5 runs it).
|
||
- B/C read the manifest for GLB upgrades; **primitive fallbacks must still fire under `?noassets=1`**.
|
||
|
||
---
|
||
|
||
## 3. Wiring checklist (LANE_F §Wiring — expanded to exact steps)
|
||
|
||
All of this lives in `web/js/world/wiring.js` (F-owned) + the minimal hooks each lane exposes.
|
||
|
||
### 3.1 Plan source swap (A → B) ✅ DONE (verified live)
|
||
- Lane B already reads `?seed=N` (default `20261990`), guards the citygen import, and surfaces the
|
||
town name + seed in the start panel and HUD. **But it imported `./js/citygen/index.js`, which did
|
||
not exist** (Lane A shipped `plan.js`), so it silently fell back to the 14-shop fixture.
|
||
- **Fix:** Lane F added `web/js/citygen/index.js` (barrel re-exporting `generatePlan/chunkIndex/chunkKey`
|
||
from `plan.js`). Verified live: shell logs "using Lane A generatePlan", HUD shows the real town
|
||
("Boolarra Heads", 523 shops), 31 chunks stream.
|
||
- Map key **M** is wired by Lane B to an in-shell minimap (not map.html). Fine for v1.
|
||
- **Gate:** plan determinism asserted green by `scaffold_check.mjs`. Map-PNG determinism pending the
|
||
shot harness + DBG hook (§4).
|
||
|
||
### 3.2 enterShop → interiors (B ↔ C) ✅ DONE (verified live, 2026-07-14)
|
||
Implemented as an **F-owned bridge module `web/js/world/interior_mode.js`** (`createInteriorMode`),
|
||
plus a thin marked seam in the shell. The bridge renders the interior into its **own dark
|
||
THREE.Scene** — the street scene is simply left frozen (not disposed → instant return, CITY_SPEC L3
|
||
"pause, not dispose"), which is cleaner than hiding B's ground+chunk meshes in a shared scene.
|
||
```
|
||
enterShop(shopId, name): // shell seam (was the toast stub)
|
||
shop = plan.shops.find(s => s.id === shopId) // id is numeric; matches door.shopId
|
||
lot = plan.lots.find(l => l.id === shop.lot) // attach {w,d} — Lane C sizes the room to it
|
||
interiorMode.enter({ ...shop, lot:{w,d} }, name); setMode('interior')
|
||
interiorMode.enter(shop): // F module
|
||
current = buildInterior(shop, THREE); scene.add(current.group) // Lane C
|
||
doorReturn = {x,y,z,ry of the player on the street}
|
||
camera → current.spawn (x, 1.6, z, ry); exitArmed = false
|
||
loop [MODE==='interior']: if interiorMode.update(dt,keys) leaveShop()
|
||
update: walk via current._debug.grid occ/bounds (mirrors interior_test.html) → renderer.render(ownScene)
|
||
exit arms once player steps >2m from the door, then fires within 1m of exits[].toStreet
|
||
leaveShop() / 'procity:exitShop' / Esc(unlock): // three exit routes, all → leaveShop
|
||
interiorMode.exit(): current.dispose() (frees GPU + unparents); camera → doorReturn
|
||
setMode('street') // pointer stayed locked → walking resumes at once
|
||
```
|
||
- **Verified live** (seed 20261990 "Boolarra Heads"): door → **Toy Shop "Numbat Playthings"**
|
||
renders (ivy wallpaper, stocked shelves, counter, exit doorway); exit state-machine correct
|
||
(no instant-exit at spawn; arms then fires at the door); **leak-free** — GPU memory 134 geo/37 tex
|
||
→ 217 loaded → **back to 134/37 after `dispose()`**. Swept **all 9 shop types** (toy/video/pawn/
|
||
opshop/book/record/stall/dept/milkbar), every one `ok+pathOK+leftClean`, worst build **8.4ms**
|
||
(budget 50), zero console errors.
|
||
- **Gate:** determinism (byte-identical placement per seed) is asserted by Lane C's own soak page;
|
||
memory-to-baseline after `dispose()` confirmed above. Real pointer-lock walk-in is the one bit the
|
||
automated browser can't drive — needs a human (or the DBG hook in §4).
|
||
|
||
### 3.3 Citizens + keepers (D)
|
||
**Keepers ✅ DONE (verified live).** Wired into the interior bridge (`interior_mode.js`), not the shell:
|
||
`new KeeperManager({ camera, citySeed: plan.citySeed })` — **no `fleet` passed → asset-free placeholder
|
||
actor** (rig-fleet upgrade is later, with §3.4). On `enter`, find the counter interactable
|
||
(`places.find(p => p.userData.keeperStand)` — Lane C tags `{x,z,ry}` there) and
|
||
`keepers.spawn(interior.group, { x, z, ry, shopId, type })`; tick `keepers.update(dt)` each interior
|
||
frame (playerPos defaults to the camera); `keepers.disposeAll()` on `exit`, before the room disposes.
|
||
- **Verified:** placeholder keeper (11-mesh humanoid) stands at the counter pose (record shop:
|
||
`{2.55,-2.46, ry 1.571}`), **turns to greet** a nearby player (0.21 rad) and eases back when they
|
||
leave (~0), disposes to 0. **Leak-free**: 5 enter/leave cycles over 5 shop types → after-leave
|
||
memory constant at 96 geo/16 tex, **net growth 0**; 0 console errors.
|
||
|
||
**CitizenSim (street peds) ✅ DONE (verified live).** Happy surprise: **`plan.streets` already IS the
|
||
graph** — `{ nodes:[{id,x,z}], edges:[{id,a,b,width,kind}] }` — so `graph: plan.streets` passes
|
||
straight in, **no adapter needed** (the runbook's earlier `streetGraphFrom` is unnecessary). In the
|
||
shell: `new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets })`,
|
||
`setPopulation(140)` (`?pop=` tunable), then in the **street** branch each frame:
|
||
`setTimeOfDay((clk.seg + clk.frac)/6)` + **`update(dt)`** — note `update` takes **only dt** and reads
|
||
`this.camera.position` internally (the runbook's `update(dt,pos)` was wrong). `visibilitychange →
|
||
setPaused`. No fleet → asset-free placeholder(near)/impostor(mid) tiers; sim self-manages NEAR/MID/FAR
|
||
LOD off camera distance (not chunk-driven — chunks.js has no lifecycle hook).
|
||
- **⚠ gotcha (fixed):** `getClock().hour` is a **display string** (`'12:30'`), so `hour/24` → NaN →
|
||
0 peds. Use the numeric day fraction `(seg + frac)/6` instead.
|
||
- **Verified:** 140 pop → **96 active midday** (density curve; `~7` at night), peds walk footpaths at
|
||
~1.5 m/s, deterministic per seed (Lane D property; F passes the same graph+seed). **Budget:** 1 near
|
||
placeholder ≈ **11 draws** (11 meshes), bounded by `NEAR_MAX=24`; but a hamlet never fills it —
|
||
worst across all 6 main/high edges was **near 3, ~191 composer draws** (budget 300). Coexists with
|
||
interiors+keepers (round-trip regression clean); 0 console errors.
|
||
- **Note for §3.4:** placeholder near-tier is the draw cost; the **rig-fleet upgrade** (shared meshes +
|
||
impostor promotion) is what lets `?pop=` rise well past 140. Until then 140 is the safe lively default.
|
||
|
||
### 3.4 Manifest + `?noassets` + rig-fleet upgrade ✅ DONE (R3, verified live)
|
||
Wired in the shell (all gated by `NOASSETS = ?noassets=1`):
|
||
- **Rig fleet:** `loadPedFleet('models/peds/')` (19 char GLBs + walk/idle clips exist locally) → passed
|
||
to `new CitizenSim({…,fleet})` and (via `createInteriorMode({…,fleet})`) to `KeeperManager`. Peds +
|
||
keeper **upgrade placeholder→shared GLB rigs** once `fleet.whenReady` resolves (verified:
|
||
`citizens.mode==='rig'`, `keeperKind==='rig'`, detailed characters on the street + behind counters).
|
||
- **Interior GLB:** `preloadManifest()` at boot + `buildInterior(shop,THREE,{useGLB:!NOASSETS})` →
|
||
Lane C `glb.js` swaps fitting primitives for **depot GLBs** (`depot:<file>` → 3GOD, reachable).
|
||
Verified: bookshop fittings load real GLBs (+8 meshes, dense shelves). *Not all kinds map* — `counter`
|
||
is intentionally primitive; `record_crate` is a known-failing GLB → fail-soft to primitive.
|
||
- **`?noassets=1`:** `fleet=null`, manifest not preloaded, `useGLB=false`. Verified **zero network**
|
||
(Performance API: 0 GLB, 0 depot, 0 manifest, 0 ped-model requests); 100% primitive, still legible.
|
||
- **Leak-free:** rig keeper + interior GLBs dispose clean — the persistent geometry is the intentional
|
||
**shared `loadGLB` cache** (cold entry +N geo, re-entries flat: proven cycle2/3 = 189→189). soak.py
|
||
pre-warms this cache so the gate is honest.
|
||
- **⚠ finding (D/E):** GLB **character models are triangle-heavy** (~45k each near rig) → busiest view
|
||
tris ~279k, soak peak ~1.0M, over the **200k** soft budget. Draws are fine (~200/300). **Decimate the
|
||
ped GLBs** (and heavy fittings) to hit the tri budget; **F holds `?pop=140`** until then (raising pop
|
||
adds near rigs = more tris). See Cross-lane requests §9.
|
||
|
||
### 3.5 Hours / closed shops ✅ DONE (R3, verified live)
|
||
- `currentHour()` parses `lighting.getClock().hour` (`'HH:MM'`→float); `isOpen(shop)` = `open ≤ H < close`.
|
||
Both exposed on `window.PROCITY`. **`enterShop` gates on it**: closed → `🔒 CLOSED · opens HH:00`, no build.
|
||
- Lane A ships exactly one **`openLate:true`** landmark/town — always the **video rental** (Video Regal
|
||
`[11,23]` etc.). Verified: **midday** toy shop `[10,16]` enters; **night (22:00)** toy shop blocked,
|
||
**only the video shop opens** (the night destination).
|
||
- **Cross-lane (B):** the facade *visual* closed-state (dark windows + CLOSED plate) + a "CLOSED" door
|
||
tooltip are Lane B's rendering — `window.PROCITY.isOpen(shop)` is the predicate. See §9.
|
||
|
||
### 3.5 Hours / closed shops (A data → B facade + C gate) ⏳
|
||
- `isOpen(shop, seg)` from `shop.hours:[open,close]` vs the current day segment (map 6 segments →
|
||
hour). Closed ⇒ B renders dark windows + CLOSED plate (facade state); enterShop → locked toast.
|
||
- Exactly one late-night shop per town (Lane A seeds a shop whose `hours` run late). **F asserts it
|
||
exists**: `plan.shops.some(s => s.hours[1] >= 22)` (tune threshold to A's convention).
|
||
- **Gate:** at night, ≥1 open shop; closed shops visibly closed + un-enterable.
|
||
|
||
---
|
||
|
||
## 4. QA gates — ALL GREEN ✅ (v1.0 criterion met, R4). `tools/qa.sh --strict` is the static umbrella.
|
||
|
||
Measured against the fully-committed R4 tree (seed 20261990, "Boolarra Heads"), verified live +
|
||
by a 6-auditor adversarial v1-readiness workflow (2026-07-14) → **GO**.
|
||
|
||
| # | gate | criterion | result (R4) |
|
||
|---|---|---|---|
|
||
| 1 | **Determinism** | seed → identical plan (deep-equal + golden fingerprint) + selfcheck | ✅ scaffold_check green; selfcheck **1301/1301**, fingerprint **0x3fa36874** (intended A2 shift, not drift); two node processes → identical `sha256(plan)` per seed; chunkIndex covers every lot |
|
||
| 2 | **Soak** | walk ≥30 chunks, ≥15 shops; geo/tex baseline after each interior; heap stable; **0** console errors | ✅ **37 chunks, 20 shops, heap 38→38 MB, 0 errors, leak-free** |
|
||
| 3 | **Budget** | busiest intersection: ≤300 draws, ≤200k tris (settled clean measure) | ✅ **draws 138 · tris ~31k** (baseline 372k; peds decimated 924k→46k ≤3k each, D2 sign-off) |
|
||
| 4 | **Asset-free** | `?noassets=1` full playthrough, zero crashes, zero network | ✅ **heap 13→13 MB, 0 errors, 0 asset requests** |
|
||
| 5 | **Selfchecks** | `selfcheck.js` + `validate_manifest.py --depot` green via qa.sh | ✅ **qa.sh --strict 4/4**; manifest `--depot`: **23 GLBs live, 0 err** |
|
||
| 6 | **Shots** | `docs/shots/v1_tour/` 10 beauty shots + contact sheet | ✅ **10 full-frame 1280×720, no letterbox**, 3 new poses resolve; incl. §3.5 **night closed-facade** (CLOSED plates + lit openLate video) + real **record/opshop interiors** (keeper + hero props) |
|
||
|
||
> Note: soak peak-draw/tri *warnings* are transient teleport/impostor-atlas-bake sampling spikes;
|
||
> gate 3's settled busiest-intersection measure is the authoritative budget check (draws 138 / tris 31k).
|
||
|
||
### DBG hook — LANDED (Lane B `web/js/world/dbg.js`, `?dbg=1`)
|
||
`window.DBG` exposes `ready / shot(name) / teleport(x,z,ry) / setSegment(seg) / enterShop(id) /
|
||
exitShop() / info()` and drives `tools/shots.py` + `tools/soak.py` (mirrors 90sDJsim's `window.DBG`).
|
||
Bookmarks: street_noon, arcade, market_square, milkbar_dusk, night_neon, crossroads_busy,
|
||
residential_collar, warehouse_fringe. **Interior tour shots** (record/opshop) are captured by
|
||
`enterShop(id)` directly (not via `shot()`, which forces street mode) — see `tools/shots.py`.
|
||
|
||
---
|
||
|
||
## 5. Order of operations once lanes land
|
||
|
||
1. ✅ `node tools/qa/scaffold_check.mjs` — A's plan determinism + registry confirmed present.
|
||
2. ✅ Wire §3.1 (plan swap) → generated seed boots on the shell (barrel + skins fix, verified live).
|
||
3. ✅ Wire §3.2 (interiors) → door opens, enter/walk/exit + dispose verified across all 9 types.
|
||
4. ✅ Wire §3.3 (citizens + keepers) → keeper greets at the counter; peds walk the graph, budget-safe.
|
||
5. ✅ Wire §3.4 (manifest + `?noassets` + rig-fleet) and §3.5 (hours) — all verified live (R3).
|
||
6. ◑ Gates: 2 & 4 **PASS** (soak/asset-free), 5 **GREEN** (`--depot`), 3 tris-blocked on D/E decimation,
|
||
6 shots need Lane B bookmark poses. `docs/shots/v1_tour/` captured.
|
||
7. ⏳ `tools/qa.sh --strict` green + gate 3 tris ⇒ tag v1 (pending D/E ped-GLB decimation).
|
||
|
||
## 7. Live smoke-test findings (2026-07-14, real generated town, seed 20261990)
|
||
|
||
Booted the wired game in a browser and drove the camera via `window.PROCITY`. State:
|
||
`Boolarra Heads · 523 shops · 711 lots · 31 chunks live · 31 door meshes · 24.7k tris`.
|
||
|
||
- ✅ **Seed → real town** works after the barrel fix (§3.1). Contiguous strip, awnings, verandah
|
||
posts, doors, windows, gum trees, sky dome, 6-segment day cycle — all render.
|
||
- ✅ **Facade skins seam (FIXED).** `skins.js facadeMat` built `assets/gen/facade-<name>.jpg` but
|
||
Lane A's `facadeSkin` is the **full filename** (`facade-fibro-blue.jpg`) → every facade requested
|
||
`facade-facade-…jpg.jpg` (404 → flat colour). Lane F normalized the name at the `facadeMat`
|
||
chokepoint (strip a leading `facade-` + image extension; idempotent for bare keys). Verified:
|
||
facade JPEGs now return **200 OK**, town is textured. *Root cause is a cross-lane naming contract
|
||
ambiguity — registry stores filenames, skins.js expected bare keys. Worth a CITY_SPEC line so
|
||
ground/awning/interior skins don't hit the same trap.*
|
||
- ⚠ **Budget:** ~**334 draw calls** at spawn (radius 3), over the CITY_SPEC ≤300. Tris fine (~25k).
|
||
Options for Lane B: drop default radius to 2, or merge more per chunk. Gate 3 will fail until shaved.
|
||
- ⏳ **Interiors not wired** (§3.2), **citizens not wired** (§3.3), **manifest/`?noassets` not wired**
|
||
(§3.4) — the remaining integration passes.
|
||
|
||
## 8. Open questions for the lanes (surface, don't guess)
|
||
- **B:** will you expose the `window.DBG` hook (§4) and the `ChunkManager` lifecycle callbacks
|
||
(§3.3)? If not, F wires citizens via a MutationObserver-style scan — slower, please prefer callbacks.
|
||
- **A:** which field marks the "open late" shop, and what's the `hours` encoding (24h ints? segment idx)?
|
||
- **C:** does `places` tag the counter slot with a stable `kind:'counter'` so keepers find it?
|
||
- **E:** is `?noassets` honored inside your manifest-consuming code, or does F gate it at the shell?
|
||
- **D:** roster spawn/despawn API name + does it accept a chunk key directly?
|
||
|
||
---
|
||
|
||
## 9. Cross-lane requests (Lane F → others, R3) — the integration surfaced these; not silent-editing your files
|
||
|
||
**→ Lane D / Lane E — decimate the ped GLBs (blocks gate 3 tris).** The rig fleet (`web/models/peds/*.glb`)
|
||
is wired and works, but the character models are triangle-heavy: **~45k tris each near rig**, so the
|
||
busiest-intersection view hits **~279k tris** (soak peak ~1.0M) vs the **200k** CITY_SPEC budget. Draws are
|
||
fine (~200/300 — the shared-mesh sharing worked). Please **decimate/LOD the 19 ped GLBs** (target ≤~10–15k
|
||
tris each) so gate 3 passes; then F can raise `?pop=` past 140. Same applies to any heavy interior fitting GLB.
|
||
|
||
**→ Lane B — three dbg.js / shell items:**
|
||
1. **shots.py bookmark poses (gate 6).** `dbg.js BOOKMARKS` defines `street_noon/arcade/market_square/
|
||
milkbar_dusk/night_neon`, but F's v1_tour also uses `warehouse_fringe / residential_collar /
|
||
crossroads_busy` — these fall back to `street_noon` (identical frames). Please add those 3 poses.
|
||
2. **shots letterbox.** In headless (1280×720), `DBG.shot()`→`composer.render()` renders into a square
|
||
sub-region even though renderer/canvas/camera are correctly sized — the EffectComposer render targets
|
||
don't rebuild on the initial resize. A real viewport toggle in shots.py didn't fix it; likely the
|
||
composer needs an explicit `setSize`/reset on first `shot()`. (In-game, real window sizes render fine.)
|
||
3. **§3.5 closed-facade visual.** F gates *enterability* by hours (`window.PROCITY.isOpen(shop)`); the
|
||
*visual* closed state — dark windows + a CLOSED plate, and a "CLOSED" door tooltip in `hud.js` — is
|
||
your facade/HUD render. Predicate is ready on `window.PROCITY`.
|
||
|
||
**Note:** F-owned harness bugs found + fixed this round (so the gates actually run): `shots.py`/`soak.py`
|
||
polled `window.DBG` before its async install (race → false "absent"); read `DBG.plan` (unexposed → use
|
||
`window.PROCITY.plan`); `enterShop({shopId})` vs the bare-id contract; `shop.lot` treated as an object
|
||
(it's a lot **id**); soak now pins midday (else §3.5 closes shops mid-walk) + pre-warms the shared GLB
|
||
cache (else the one-time upload reads as a leak) + budget peaks demoted to warnings (DBG.info samples spike
|
||
during streaming; gate 3's clean single-pass is authoritative).
|
||
|
||
---
|
||
|
||
## 10. Round 5 — v1.1 close-out + v2 flag seams (Lane F tracking)
|
||
|
||
Round 5 (v1.1 polish + v2 foundations behind flags). **v2 prime law:** default boot stays
|
||
v1-identical; `GOLDEN.hash 0x3fa36874` must not move; `qa.sh --strict` green at every commit.
|
||
|
||
### v1.1 tag — ⏳ WATCH (blocked on C1 + E1)
|
||
Tag `v1.1` once **Lane C** re-maps `counter_till` + `record_crate` (C1) and **Lane E** bakes the
|
||
hero props ≤8k (E1). F then: verify record/milkbar/opshop interiors (GLB counter+till correct, bins
|
||
upgraded, baked props look right), re-capture ONLY the 2 interior tour shots if improved, qa green →
|
||
`git tag v1.1` (local). *Nothing to tag yet — HEAD `039c179`, no round-5 lane commits.*
|
||
|
||
### v2 flag seams — table (F wires `web/index.html` + `tools/` as lanes document them)
|
||
Every flag **default-off + independent**. Wire only from the owning lane's NOTES call-site spec.
|
||
|
||
| flag | owner | what | F wiring | state |
|
||
|---|---|---|---|---|
|
||
| `?plansrc=osm` | A | second plan source (OSM fixture) behind the CityPlan contract | select in shell plan bootstrap | ❌ **A did not run R5** — task carries to R6 |
|
||
| `?winmap=1` | B | parallax interior-mapping window shader | none needed (self-contained in `buildings.js`) | ✅ landed `2a923b6` |
|
||
| `?dig=1` | C | dig.js crate-riffle on procedural stock | wired by F (`26d68cd`): `DIG_ON` → `interiorMode`, Esc/pointer-lock guards | ✅ landed `7131f53` + F2 |
|
||
| `?roster=stream` | D | chunk-streamed citizen roster | none needed (self-contained in `sim.js`) | ✅ landed `93a3168` |
|
||
|
||
*(table updated by Fable in the R5 review — F3 harness extensions below did NOT land in R5; they
|
||
are the top Lane F item for R6.)*
|
||
|
||
### v2 QA harness extensions (F3) — build as flags land
|
||
- **Determinism gate:** pin `plan-hash` + `first-interior-hash` per `(seed, plansrc)`. Synthetic
|
||
stays `0x3fa36874`; add the osm-fixture hash when A lands.
|
||
- **Flags-off regression check:** boot no-flags → assert plan hash + a settled draws/tris snapshot
|
||
within tolerance of the v1 baseline (catches a v2 flag leaking into the default path). *This is the
|
||
enforcement arm of the v2 prime law — build early, before flags land.*
|
||
- **v2 smoke:** boot each flag on, walk 2 chunks, enter 1 interior (+1 dig if C landed), 0 console
|
||
errors. Wire into `qa.sh` as a **non-strict (warn) gate** this round; strict next round.
|
||
|
||
### Marshal
|
||
Shared-tree arbiter this round: F owns `web/index.html` + `tools/`; if two lanes collide on a file,
|
||
F arbitrates. Cross-lane asks → the owning lane's NOTES, not silent edits.
|