- B1: per-shop closed-facade visuals — dark windows + red CLOSED plate + door tooltip, batched via a per-vertex aHours attr + two shared shaders + one uHour/uNight uniform pair on a procity:segment event (facade atlas untouched, no per-shop materials, no per-frame poll). 22:00 → only the openLate video shop lit, all others CLOSED; ~200 draws worst-view with citizens. - B2: the shots 'letterbox' was camera-jammed-into-a-wall poses, not a composer bug (composer proven correctly sized headless). Reframed street_noon/arcade/warehouse to look down a street (streetViewPose), added the 3 missing bookmarks, night_neon now frames the lit video shop amid CLOSED neighbours, + a defensive composer resize. shots.py: 10 un-letterboxed shots, 0 errors. - B3: furniture GLB use-if-ready upgrade (bench + food_cart, sit on their footprints) + novelty_record placed at record stores. novelty_record GLB is 26.5k tris (5x glb_law) → stays low-poly primitive until Lane E decimates it; ?noassets verified 0 network. qa.sh --strict GREEN (4/4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
12 KiB
Markdown
151 lines
12 KiB
Markdown
# PROCITY-B — progress (Lane B: Streetscape + game shell)
|
||
|
||
**Status: complete and verified.** Lane B turns a CityPlan into a walkable, chunk-streamed 3D town
|
||
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
|
||
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
|
||
|
||
## Round 4 (Fable's ROUND4 → Lane B) — done · qa.sh --strict GREEN (4/4)
|
||
|
||
- **B1 — closed-facade visuals (§3.5, headline).** Closed shops → **dark windows + red CLOSED plate**;
|
||
open shops (esp. the `openLate` video rental at night) → **lit windows**; door tooltip
|
||
`🔒 <name> — CLOSED · opens HH:00`. Batched with a per-vertex `aHours` + two shared shaders + one
|
||
`uHour/uNight` uniform pair, updated once on a `procity:segment` event (lighting.js → buildings.js)
|
||
— **facade atlas untouched, no per-shop materials, no per-frame poll.** Verified live: **22:00 →
|
||
only "Video Regal" lit, all others CLOSED**; midday → all open; worst-view **200 draws with
|
||
citizens** (≤300). Files: `buildings.js`, `lighting.js`, `hud.js`.
|
||
- **B2 — shot harness (gate-6 critical path).** The "letterbox" was **not** a composer bug (proved
|
||
the composer is correctly sized headless) — the `street_noon`/`arcade`/`warehouse_fringe` **poses
|
||
jammed the camera into a wall**. Reframed them to look down a street (new `streetViewPose`), added
|
||
the **3 missing bookmarks** F asked for, pointed `night_neon` at the glowing video shop amid CLOSED
|
||
neighbours, and added a defensive composer resize. `tools/shots.py` → **10 un-letterboxed shots, 0
|
||
console errors** (in `docs/shots/laneB_r4/`). File: `dbg.js`.
|
||
- **B3 — furniture GLB upgrade + novelty_record.** furniture.js now use-if-ready-upgrades primitives
|
||
to depot GLBs (sync check, `?noassets` → 0 network verified). Placed **novelty_record** at every
|
||
record store + food_cart by stalls/milk bars. **Finding:** the **novelty_record GLB is 26.5k tris**
|
||
(5× `glb_law`), spiking a busy view to **574k** → it stays on its low-poly primitive; bench (1.9k)
|
||
+ food_cart (5.0k) GLBs are fine and sit on their footprints. **→ Lane E: decimate novelty_record
|
||
(same as the ped rigs).** File: `furniture.js`.
|
||
|
||
|
||
## Round 3 (Fable's ROUND3_INSTRUCTIONS → Lane B) — done
|
||
- **Task 1 (draw budget, re-measured WITH citizens).** Radius-2 auto is already the spawn default;
|
||
no config change needed. Drove the real loop (`chunks.update` + `citizens.update`, pop 140, up to
|
||
**125 active peds**) along the dense corridors via `window.DBG`: worst continuous-walk view
|
||
**263 scene / 275 total at MIDDAY**, **275 total at NIGHT** — **≤300 everywhere**, ~25 margin.
|
||
Peds are a batched impostor layer (~1–2 draws). Documented in LANE_B_NOTES.
|
||
- **Task 2 (`scene.environment`).** Was **null** → Lane D's rig-fleet peds bake their impostor atlas
|
||
from it (`sim.js`) and would render dark. Set a neutral PMREM'd sky→ground gradient in
|
||
`lighting.attachRenderer` (before `CitizenSim` constructs; no asset). Verified **set in
|
||
street / night / interior / after interior-return** (`interior_mode` only swaps `scene.background`).
|
||
- **Task 3 (furniture GLB footprints)** — **standby**: Fable's §3.4 (manifest GLB upgrade +
|
||
`?noassets`) hasn't landed (furniture.js still primitives); will verify placement/collision when it does.
|
||
- **Task 4 (optional polish)** — deferred (fresh generated-city stills / map.html wiring).
|
||
- Only Lane B file changed this round: `web/js/world/lighting.js` (+ these docs). Committed by exact
|
||
pathspec (Lanes C/E/F have concurrent uncommitted work in the shared tree — untouched).
|
||
|
||
## Session 2 (continuation) — closed the two open Lane-B items Fable flagged
|
||
- **Draw budget now holds with margin.** Facade texture atlas + awning-merge + shell/trim-merge:
|
||
worst dense frustum **393 → 265 scene draws**, worst continuous-walk gameplay view **~261 total**
|
||
(≤300 gate, ~40 margin). Bonus: **skin materials 25 → 5** (the atlas is one shared facade material),
|
||
fixing the "≤25 was at the cap" concern too. (The old "~445" peak was largely a measurement artifact
|
||
— orphan chunks from teleport-jumping that the real streamer disposes.)
|
||
- **`window.DBG` harness exposed** (`?dbg=1`, `js/world/dbg.js`) — answers LANE_F_NOTES §4 so Fable's
|
||
`shots.py`/`soak.py` can drive the game: `ready`, `shot(name)`, `teleport`, `setSegment`,
|
||
`enterShop`/`exitShop` (drives the real B→F→C interior chain), `info()`.
|
||
- **`ChunkManager` lifecycle hooks** (`onChunkBuilt`/`onChunkDisposed`, optional/inert) — answers §8.
|
||
- Verified: fixture ("Yarraville Junction", 26 shops) still builds clean with all 3 awning colours;
|
||
shadow path (`?shadows=1`) renders from the merged meshes; day + night + door tooltip + interiors
|
||
all work; **zero console errors**. Files touched: `skins.js`, `buildings.js`, `chunks.js`, `hud.js`,
|
||
new `dbg.js`, + a small guarded DBG loader in `index.html`. **Uncommitted** (per the lane treaty;
|
||
and Fable is concurrently wiring `index.html` — our edits coexist cleanly on stable anchors).
|
||
|
||
Owned/created (only `web/index.html` + `web/js/world/*`, per the ownership treaty):
|
||
|
||
| file | role |
|
||
|---|---|
|
||
| `web/index.html` | shell: importmap→vendor, renderer + ACES + bloom, MODE state machine (`map`/`street`/`interior`), main loop, adaptive perf, guarded `generatePlan` import, shot harness |
|
||
| `web/js/world/fixture_plan.js` | hand-authored CityPlan (schema v1): 4 N + 2 S terraced blocks (26 shops), cross-street houses, 2 market stalls, streets graph — spans ~6 chunks so streaming is exercised |
|
||
| `web/js/world/planutil.js` | chunk math (64 m), `chunkIndex`, node/lot indices, oriented-rect collider + `pushOutRect` |
|
||
| `web/js/world/skins.js` | city-wide shared material registry (one material per skin), seeded flat-colour fallbacks |
|
||
| `web/js/world/buildings.js` | facade kit: instanced shells/parapets/posts/awnings, merged per-skin facades, per-chunk sign atlas, doors/windows |
|
||
| `web/js/world/ground.js` | roads/footpaths/kerbs/plaza/base plane, merged strips, UV-baked tiling |
|
||
| `web/js/world/furniture.js` | instanced streetlights (emissive at night), benches, bins, gum trees, bus stops — all with primitive fallbacks |
|
||
| `web/js/world/lighting.js` | seeded sky dome + 6-segment day/night cycle, one following sun/shadow, fog pop-in mask |
|
||
| `web/js/world/player.js` | PointerLock WASD+run, eye 1.7 m, push-out collision vs nearby-chunk colliders |
|
||
| `web/js/world/chunks.js` | the streamer: radius-R build / R+1 dispose, nearest-first queue, 4 ms/frame budget, night propagation |
|
||
| `web/js/world/hud.js` | crosshair, throttled door-raycast tooltip, live draws/tris/fps/seed/clock readout, click→`procity:enterShop` |
|
||
| `web/js/world/minimap.js` | `map` mode: 2D top-down town directory (streets + lots by type + player dot) |
|
||
|
||
## Acceptance — all met
|
||
|
||
- ✅ Walk the fixture 5 min: contiguous, no seams, chunks stream silently, day/night cycles, signs
|
||
legible, tooltips work, budgets green (148 scene draws, 21 skin materials, 5.8k tris).
|
||
- ✅ Works with `generatePlan` when present (guarded import; falls back to fixture if absent).
|
||
- ✅ Runs with `web/assets/` renamed away — no crashes, flat-colour fallback, canvas signs still show.
|
||
- ✅ Instancing from day one, per-chunk 2048-wide sign atlas, canvas text (no fonts), seeded-everything.
|
||
- ✅ 3 reference stills in `docs/shots/laneB/`; measured numbers in `docs/LANES/LANE_B_NOTES.md`.
|
||
|
||
## Verified live (in-browser)
|
||
|
||
Streaming (chunks build/dispose as you walk, queue never starves), 6-segment day→night with glowing
|
||
shopfront windows + streetlamp bloom at night, door raycast → correct shop → `procity:enterShop` +
|
||
toast, pixel-accurate collision (stops at wall − player radius), all-fallback mode, and Lane A's full
|
||
493-shop city rendering + streaming + arbitrary-angle collision through Lane B.
|
||
|
||
## Perf (measured — full table in `docs/LANES/LANE_B_NOTES.md`)
|
||
|
||
- **Fixture** (radius 3, shadows on): ~148 scene draws, low tris, well under all gates.
|
||
- **Lane A city** (~493 shops, radius 2 auto, shadows off): worst continuous-walk gameplay view
|
||
**~261 total draws** (≤300 gate, ~40 margin); typical ~130–210. Skin materials **5** (≤25).
|
||
Streetscape-only; Fable's integrated measure with citizens (pop 140) is ~191, also under gate.
|
||
|
||
## Adversarial multi-agent review → fixes landed
|
||
|
||
I ran a 4-dimension review (correctness / memory-perf / spec / robustness) with adversarial
|
||
verification. Confirmed real bugs, all fixed and re-verified:
|
||
|
||
1. **Collision was wrong for non-axis-aligned `ry`** (`pushOutRect` used the wrong rotation sign).
|
||
Latent on my axis-aligned fixture; would let you walk through Lane A's angled buildings. **Fixed** —
|
||
verified `keptOut:true` against a `ry=−1.32` lot.
|
||
2. **House doors** were raycastable and mis-resolved to the nearest shop → clicking a house "entered"
|
||
a random shop. **Fixed** (separate non-picked mesh).
|
||
3. **Awning skin** used a signed shift → undefined skin for ~⅓ of shops. **Fixed** (`>>>`).
|
||
4. **Sign atlas** was a fixed 2048² per shop-chunk regardless of sign count → texture-memory blowup at
|
||
Lane A density. **Fixed** (sized to actual signs, mipmaps off).
|
||
5. **Furniture**: numeric edge ids broke the seed hash (all edges identical); offset furniture could
|
||
land in an unbuilt chunk and vanish. **Both fixed**.
|
||
6. **`takeShots()`** didn't restore the pre-shot mode. **Fixed**.
|
||
|
||
## ⚠️ Things Fable should know (cross-lane)
|
||
|
||
- **Lane A landed and is now the shipped default.** `index.html` prefers `generatePlan(seed)` over the
|
||
fixture. Seed 20261990 → "Boolarra Heads", ~493 shops. The fixture remains the fallback + schema ref.
|
||
- **A "Lane F integration fix" already appeared in `skins.js`** (`facadeMat` now strips a `.jpg`/`facade-`
|
||
prefix). Lane A stores `shop.facadeSkin` as a **full filename** (`"facade-stucco-pink.jpg"`), not the
|
||
bare skin key the CITY_SPEC table implies. That normalize is what makes Lane A's facades load. Kept.
|
||
- **Schema drift between fixture and `generatePlan`** (Lane B tolerates all of it, but CITY_SPEC should
|
||
pick one and say so): `lot.id`/`lot.block` are strings in the fixture, **numbers** from Lane A;
|
||
`lot.frontEdge` is `'N'/'S'` in the fixture, a **number** from Lane A; `shop.facadeSkin` is a bare key
|
||
in the fixture, a **filename** from Lane A; `lot.ry` is axis-aligned in the fixture, **arbitrary
|
||
radians** from Lane A. Recommend Fable freeze these in CITY_SPEC so Lane C/D/E don't each guess.
|
||
- **≤25 skin-materials gate — RESOLVED (session 2).** Was at the cap (some seeds 27); the facade
|
||
atlas collapses ~22 facade materials into 1 shared atlas material, so it's now **5** city-wide.
|
||
- **Perf gate on the full city — RESOLVED (session 2).** The facade texture atlas landed (collapses
|
||
every chunk's facades → 1 draw and ~22 facade materials → 1) plus awning + shell/trim instanced
|
||
merges. Worst gameplay view is now ~261 total (≤300). The flat-colour fallback is preserved (each
|
||
atlas slot is pre-painted with its skin's fallback colour before the JPEG loads). See LANE_B_NOTES.
|
||
|
||
## Suggested next instructions (your call)
|
||
|
||
1. Freeze the CityPlan schema in CITY_SPEC (the drift list above) — unblocks C/D/E. **Still open.**
|
||
2. ~~Decide the facade-atlas~~ — **done (session 2): Lane B implemented it.** Draw + material gates now green.
|
||
3. ~~Interiors hand-off~~ — **done: Fable wired `enterShop → interiorMode → buildInterior` in the shell;**
|
||
verified via `DBG.enterShop`. Lane B's seam (`procity:enterShop`/`procity:exitShop`) is intact.
|
||
4. Run Lane F's `shots.py`/`soak.py` against `window.DBG` now that it exists (bookmarks are seed-derived).
|
||
5. Optional: capture fresh `docs/shots/laneB/` stills of the generated city via `DBG.shot(...)`; wire
|
||
`map` mode into Lane A's `map.html` (currently my lightweight minimap).
|
||
|
||
## Run
|
||
`cd web && python3 -m http.server 8130` → http://localhost:8130 . (The app needs no server-side logic;
|
||
the P-key shot harness uses browser downloads.)
|