Lane F R3: hours/closed-shops + manifest/rig-fleet/?noassets + --depot + QA harness fixes

§3.5 hours: isOpen() gates enterShop — closed shops locked; at night only the openLate video
rental opens. §3.4: ?noassets (zero-network primitive path, verified via Performance API),
interior fitting GLB upgrades from the 3GOD depot (useGLB), and the rig-fleet upgrade
(models/peds/*.glb) so peds + keepers become shared GLB rigs, leak-free (shared loadGLB cache
proven, not a leak). qa.sh manifest gate now --depot (16 GLBs live) — GREEN 4/4.

Browser QA harness fixes so shots.py/soak.py actually run: DBG async-poll (was racing the async
install), plan via window.PROCITY (DBG.plan unexposed), enterShop bare-id, shop.lot is a lot id,
midday pin (§3.5 else closes shops mid-walk), GLB-cache warmup, budget peaks -> warnings (gate 3
clean pass is authoritative). Gate 2 soak PASS both asset modes; gate 4 asset-free PASS;
docs/shots/v1_tour/ (10 shots) + contact sheet captured.

Findings (LANE_F_NOTES §9): D/E decimate tri-heavy ped GLBs (gate 3 tris ~279k > 200k budget;
draws fine ~200/300). B: add 3 shots bookmark poses, fix headless composer letterbox in
DBG.shot(), render the §3.5 closed-facade visual (predicate window.PROCITY.isOpen).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-14 17:19:11 +10:00
parent fe40ea7d0a
commit 53f472d0d7
18 changed files with 199 additions and 46 deletions

View File

@ -4,7 +4,35 @@
---
## TL;DR
## Round 3 (lead lane) — DONE, verified live
Executed Fable's `ROUND3_INSTRUCTIONS.md` Lane F tasks. All wiring verified in-browser on seed 20261990.
| task | result |
|---|---|
| **§3.5 hours / closed shops** | `isOpen()` gate on `enterShop`. Midday: open. **Night (22:00): all closed except the openLate video rental** (Video Regal [11,23]) — verified toy shop blocked, video enters. |
| **§3.4 `?noassets`** | **Zero network** (Perf API: 0 GLB/depot/manifest/ped-model requests); 100% primitive, legible. |
| **§3.4 interior GLB** | `useGLB` → fitting GLBs load from the **3GOD depot** (bookshop shelves, +8 meshes, visually confirmed). |
| **§3.4 rig fleet** | 19 ped GLBs → peds **and** keepers upgrade placeholder→**GLB rigs**; **leak-free** (shared-cache proven, cycle2/3 flat). |
| **`--depot` on qa.sh** | `validate_manifest.py --depot` = 0 err (16 GLBs live). **qa.sh GREEN 4/4.** |
| **Gate 2 soak** | **PASS** both modes — assets: 20 interiors leak-free (+GLB-cache warmup), heap 98MB stable; noassets: 37 chunks, heap 15MB. 0 console errors. |
| **Gate 4 asset-free** | **PASS.** |
| **Gate 6 shots** | `docs/shots/v1_tour/` (10 shots) + contact sheet captured; letterbox + 3 fallback bookmarks are Lane B dbg.js. |
**Findings → other lanes** (details in [`LANE_F_NOTES §9`](docs/LANES/LANE_F_NOTES.md)):
- **D/E:** rig ped GLBs are **tri-heavy** (~45k each) → gate 3 tris **~279k > 200k** budget (draws fine ~200/300).
Decimate the ped GLBs; F holds `?pop=140` until then.
- **B:** add 3 shots bookmark poses; fix the headless composer letterbox in `DBG.shot()`; render the §3.5
closed-facade visual (dark windows + CLOSED plate; predicate `window.PROCITY.isOpen`).
- Fixed **5 F-owned harness bugs** so `shots.py`/`soak.py` actually run (DBG async-poll, `DBG.plan`→`PROCITY.plan`,
`enterShop` bare-id, `shop.lot` is an id, midday-pin + GLB-cache warmup + budget→warning).
**Git:** working tree synced to canonical `main` (862bbcd) mid-session — my R2 work is committed as `aa4ed4f`.
R3 edits below are staged by pathspec (see Housekeeping). Nothing pushed.
---
## TL;DR (round 2 — for context)
All five lanes (AE) landed; F stood up the QA harness, **adversarially reviewed and fixed its own
tooling**, then wired the lanes into **a walkable, living town** — interiors, shopkeepers, and street

View File

@ -176,14 +176,34 @@ LOD off camera distance (not chunk-driven — chunks.js has no lifecycle hook).
- **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 Point B/C at E's manifest ⏳
- Shell loads `web/assets/manifest.json` if present → `window.PROCITY.manifest`. B/C prefer manifest
GLB upgrades over primitives; **registry.js (Lane A) stays the type→skin source of truth**, the
manifest is the *GLB-upgrade + validated-skin* layer.
- `?noassets=1`: shell sets `window.PROCITY.noassets = true`; manifest load is skipped and every
loader call is short-circuited to the primitive/flat-colour fallback. (F defines the flag; B/C/E
check `window.PROCITY.noassets` before touching `loaders.js`.)
- **Gate:** asset-free — `?noassets=1` full playthrough, zero crashes, town still legible.
### 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 →
@ -196,14 +216,14 @@ LOD off camera distance (not chunk-driven — chunks.js has no lifecycle hook).
## 4. QA gates (all green ⇒ v1). `tools/qa.sh --strict` is the umbrella.
| # | gate | how | tool | state |
| # | gate | how | tool | state (R3) |
|---|---|---|---|---|
| 1 | **Determinism** | seed 1234 → identical map PNG + identical first-interior placement list, 2 runs/machines | `scaffold_check.mjs` (plan) + `shots.py --seed 1234` (map PNG) + interior placement hash | ⏳ needs A,B,C |
| 2 | **Soak** | 10-min scripted walk, ≥30 chunks, ≥15 shops; `renderer.info.memory` geo/tex back to baseline after each interior; JS heap stable; **0** console errors | `tools/soak.py` | ⏳ needs B,C |
| 3 | **Budget** | busiest intersection, midday: HUD ≤300 draws, ≤200k tris | read HUD via `soak.py`/browser | ⏳ needs B |
| 4 | **Asset-free** | `?noassets=1` full playthrough, zero crashes | `soak.py --noassets` | ⏳ needs B |
| 5 | **Selfchecks** | `node web/js/citygen/selfcheck.js` + `python3 pipeline/validate_manifest.py` green | `tools/qa.sh` | ✅ runner ready; ⏳ inputs |
| 6 | **Shots** | refresh `docs/shots/*`; add `docs/shots/v1_tour/` (10 beauty shots) | `tools/shots.py` | ⏳ needs B,C |
| 1 | **Determinism** | seed → identical plan (deep-equal, golden hash) + identical interior placement per seed | `scaffold_check.mjs` + Lane C soak page | ✅ plan/interior determinism green (map-PNG delta pending shot poses) |
| 2 | **Soak** | scripted walk, ≥30 chunks, ≥15 shops; geo/tex back to baseline after each interior; JS heap stable; **0** console errors | `tools/soak.py` | **PASS** both modes (assets: 20 shops leak-free w/ GLB-cache warmup, heap 98MB stable; noassets: 37 chunks) |
| 3 | **Budget** | busiest intersection: ≤300 draws, ≤200k tris (clean single-pass) | browser clean measure | ⚠ **draws PASS (~200)**; **tris FAIL (~279k)** with rig fleet — decimate note for D/E (§9) |
| 4 | **Asset-free** | `?noassets=1` full playthrough, zero crashes, **zero network** | `soak.py --noassets` | ✅ **PASS** — 0 GLB/depot/manifest/ped requests, legible, no crash |
| 5 | **Selfchecks** | `selfcheck.js` + `validate_manifest.py --depot` green | `tools/qa.sh` | ✅ **GREEN 4/4** (manifest now `--depot`: 16 GLBs live, 0 err) |
| 6 | **Shots** | `docs/shots/v1_tour/` (10 beauty shots) + contact sheet | `tools/shots.py` | ◑ harness runs + 10 shots captured; **letterbox + 3 fallback bookmarks = Lane B dbg.js** (§9) |
### DBG hook F requests from Lane B (for gates 14,6)
The house shot/soak harnesses drive the game through a debug object. **Ask Lane B to expose**, on
@ -225,9 +245,10 @@ Until it exists, `shots.py`/`soak.py` degrade to booting the default view and wa
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`) — also the rig-fleet upgrade. Then §3.5 (hours). Commit.
6. ⏳ Run gates 16; capture `docs/shots/v1_tour/`; write final numbers into `F-progress.md`.
7. ⏳ `tools/qa.sh --strict` green ⇒ tag v1.
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)
@ -255,3 +276,32 @@ Booted the wired game in a browser and drove the camera via `window.PROCITY`. St
- **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 ≤~1015k
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).

1
docs/shots/contact.html Normal file
View File

@ -0,0 +1 @@
<!doctype html><meta charset="utf-8"><title>PROCITY contact sheet</title><style>body{background:#111;color:#ddd;font:14px monospace;padding:16px}.row{display:flex;gap:8px}figure{margin:0;flex:1;min-width:0}img{width:100%}figcaption{padding:2px 0;color:#8f8}.empty{display:flex;align-items:center;justify-content:center;border:1px dashed #444;color:#666;min-height:120px}</style><h3>street_noon</h3><div class="row"><figure><img src="v1_tour/street_noon.png" loading="lazy"><figcaption>street_noon (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>arcade</h3><div class="row"><figure><img src="v1_tour/arcade.png" loading="lazy"><figcaption>arcade (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>market_square</h3><div class="row"><figure><img src="v1_tour/market_square.png" loading="lazy"><figcaption>market_square (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>milkbar_dusk</h3><div class="row"><figure><img src="v1_tour/milkbar_dusk.png" loading="lazy"><figcaption>milkbar_dusk (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>night_neon</h3><div class="row"><figure><img src="v1_tour/night_neon.png" loading="lazy"><figcaption>night_neon (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>warehouse_fringe</h3><div class="row"><figure><img src="v1_tour/warehouse_fringe.png" loading="lazy"><figcaption>warehouse_fringe (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>residential_collar</h3><div class="row"><figure><img src="v1_tour/residential_collar.png" loading="lazy"><figcaption>residential_collar (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>record_interior</h3><div class="row"><figure><img src="v1_tour/record_interior.png" loading="lazy"><figcaption>record_interior (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>opshop_interior</h3><div class="row"><figure><img src="v1_tour/opshop_interior.png" loading="lazy"><figcaption>opshop_interior (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div><h3>crossroads_busy</h3><div class="row"><figure><img src="v1_tour/crossroads_busy.png" loading="lazy"><figcaption>crossroads_busy (v1_tour)</figcaption></figure><figure class="empty"><figcaption>reference — none</figcaption></figure></div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 359 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

View File

@ -59,9 +59,12 @@ else
fi
# ── Gate 3: Lane E manifest validator ────────────────────────────────────────
# [Lane F R3] --depot: the 16 GLBs are published + live on the 3GOD depot (Fable, 2026-07-14), so the
# gate now REQUIRES them reachable (was a soft local-only check). Override the endpoint with GOD3_DEPOT
# (tailnet path) if digalot.fyi is unreachable; the validator falls back to the manifest's depot field.
if [ -f pipeline/validate_manifest.py ]; then
run_gate "manifest validator (files exist · footprints sane · JSON parses)" \
python3 pipeline/validate_manifest.py
run_gate "manifest validator (files exist · footprints sane · GLBs live on depot · JSON parses)" \
python3 pipeline/validate_manifest.py --depot
else
skip_gate "manifest validator" "pipeline/validate_manifest.py not landed yet (Lane E)"
fi

View File

@ -71,7 +71,13 @@ def main():
errors = []
pg.on('console', lambda m: errors.append(m.text) if m.type == 'error' else None)
pg.goto(url)
has_dbg = pg.evaluate("() => !!(window.DBG)")
# DBG installs asynchronously (main() → dynamic import of dbg.js), so POLL for it — reading
# window.DBG the instant goto's load event fires races the async boot and false-reports absent.
try:
pg.wait_for_function("() => !!(window.DBG)", timeout=15000)
has_dbg = True
except Exception:
has_dbg = False
if not has_dbg:
print("⚠ window.DBG absent (Lane B shell not landed or ?dbg hook missing) — "
"capturing a single default frame only.")
@ -82,6 +88,20 @@ def main():
return
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
# dismiss the start splash (#pc-start, z-index 40) — DBG.shot() renders the canvas beneath it,
# but a headless capture never clicks "walk into town", so the overlay would cover every frame.
pg.evaluate("() => { const o = document.getElementById('pc-start'); if (o) o.style.display = 'none'; }")
# best-effort: let the rig fleet finish so peds/keepers show as GLB characters, not placeholders
try:
pg.wait_for_function("() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)",
timeout=12000)
except Exception:
pass
# A REAL viewport toggle (not a synthetic resize event) forces the EffectComposer to rebuild its
# render targets at the true size — headless can construct the composer before the viewport settles,
# which otherwise letterboxes every frame into a square sub-region.
pg.set_viewport_size({'width': 1281, 'height': 721}); pg.wait_for_timeout(120)
pg.set_viewport_size({'width': 1280, 'height': 720}); pg.wait_for_timeout(250)
for name in TOUR:
enter = ENTER.get(name)
if enter:

View File

@ -56,21 +56,48 @@ def main():
pg.on('pageerror', lambda e: cerr.append(str(e)))
pg.goto(url)
if not pg.evaluate("() => !!window.DBG"):
sys.exit("⚠ window.DBG absent — Lane B shell/hook not landed; soak cannot run yet. "
"(This is expected pre-Lane-B; the gate is defined and ready.)")
# DBG installs asynchronously (main() → dynamic import of dbg.js) — poll rather than read the
# instant goto's load event fires (that races the async boot and false-reports absent).
try:
pg.wait_for_function("() => !!window.DBG", timeout=15000)
except Exception:
sys.exit("⚠ window.DBG absent after 15s — boot the game with ?dbg=1 (soak.py adds it). "
"(Gate is defined and ready.)")
pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000)
# Pin midday (setSegment pauses the day clock). §3.5 locks closed shops, so without this a long
# soak drifts into night where only the openLate shop opens — starving the ≥MIN_SHOPS entry gate.
pg.evaluate("() => window.DBG.setSegment(2)")
plan = pg.evaluate("() => window.DBG.plan || null")
# The CityPlan drives the walk. DBG doesn't expose it; the shell's window.PROCITY does.
plan = pg.evaluate("() => (window.PROCITY && window.PROCITY.plan) || window.DBG.plan || null")
if not plan:
sys.exit("DBG.plan not exposed — need the active CityPlan to script a walk.")
sys.exit("plan not exposed on window.PROCITY/DBG — need the active CityPlan to script a walk.")
shops = plan.get('shops', [])
lots = {l['id']: l for l in plan.get('lots', [])} # shop.lot is a lot ID, not a lot object
base = pg.evaluate("() => { const i = window.DBG.info(); return {g:i.geometries, t:i.textures}; }")
# Waypoint path: sample shop lots as targets so we cross many chunks and pass many doors.
# (Duration ≈ len(targets) × per-step waits; a strict --minutes loop is a TODO to tune
# against Lane B's real DBG API — the chunk/shop minimums are what actually gate here.)
targets = shops[:: max(1, len(shops) // 40)] if shops else []
targets = shops[:: max(1, len(shops) // 60)] if shops else [] # denser sampling → cross ≥MIN_CHUNKS distinct chunks
# Pre-warm the shared GLB cache before the leak-checked walk. The FIRST render of each fitting
# GLB (and the rig keeper) uploads its persistent, SHARED geometry once — reused by every later
# room. That one-time upload would otherwise read as a per-interior "leak"; warming here (one
# shop per type, waiting for the depot GLB to load+render) makes the walk's leak check honest.
# No-op under ?noassets (no GLBs load).
if not noassets:
warmed = set()
for shop in shops:
t = shop.get('type')
if t in warmed:
continue
warmed.add(t)
pg.evaluate("(id) => window.DBG.enterShop(id)", shop.get('id'))
if pg.evaluate("() => window.DBG.info().mode === 'interior'"):
pg.wait_for_timeout(2500) # depot fitting GLB load + first render → GPU upload → cache
pg.evaluate("() => window.DBG.exitShop()")
pg.wait_for_timeout(300)
def sample():
nonlocal peak_draws, peak_tris
@ -89,8 +116,8 @@ def main():
circuit = 0
while True:
for idx, shop in enumerate(targets):
lot = shop.get('lot') or shop
x, z = lot.get('x', 0), lot.get('z', 0)
lot = lots.get(shop.get('lot')) # resolve the lot ID → its world position
x, z = (lot['x'], lot['z']) if lot else (0, 0)
pg.evaluate("(p) => window.DBG.teleport(p.x, p.z, 0)", {'x': x, 'z': z})
pg.wait_for_timeout(1000)
sample()
@ -100,7 +127,7 @@ def main():
# ≥MIN_SHOPS gate, which is the correct way to fail it.
if idx % 2 == 0 and shops_entered < MIN_SHOPS + 5:
before = pg.evaluate(MEM)
pg.evaluate("(id) => window.DBG.enterShop({shopId:id})", shop.get('id'))
pg.evaluate("(id) => window.DBG.enterShop(id)", shop.get('id')) # DBG.enterShop wants a bare shop id
pg.wait_for_timeout(700); sample()
if pg.evaluate("() => window.DBG.info().mode === 'interior'"):
shops_entered += 1
@ -124,10 +151,14 @@ def main():
fails.append(f"crossed only {len(chunks_seen)} chunks (need ≥{MIN_CHUNKS})")
if shops_entered < MIN_SHOPS:
fails.append(f"entered only {shops_entered} shops (need ≥{MIN_SHOPS})")
# Budget is INFORMATIONAL here, not a hard gate: DBG.info() renders via composer.render() and can be
# sampled mid chunk-stream / interior-mode transition, spiking the peak to impossible values (10k+
# draws). The authoritative ≤300-draws / ≤200k-tris check is QA gate 3 — a clean single-pass
# renderer.render(scene,camera) at the busiest intersection (see LANE_F_NOTES §4/§7).
if peak_draws > BUDGET_DRAWS:
fails.append(f"draw calls peaked at {peak_draws} (budget {BUDGET_DRAWS})")
warns.append(f"peak draws {peak_draws} > {BUDGET_DRAWS} (DBG.info sample — verify with gate 3's clean pass)")
if peak_tris > BUDGET_TRIS:
fails.append(f"tris peaked at {peak_tris} (budget {BUDGET_TRIS})")
warns.append(f"peak tris {peak_tris} > {BUDGET_TRIS} (rig-fleet models; gate 3 authoritative — decimate note for D/E)")
# JS heap: only meaningful if DBG.info() actually reports a nonzero heapMB (needs
# performance.memory / --expose-gc). If it doesn't, say so — don't silently "pass".
nonzero_heap = [h for h in heap_series if h]

View File

@ -54,6 +54,8 @@ import { createHUD } from './js/world/hud.js';
import { createMinimap } from './js/world/minimap.js';
import { createInteriorMode } from './js/world/interior_mode.js'; // [Lane F integration] Lane C interior bridge
import { CitizenSim } from './js/citizens/sim.js'; // [Lane F integration] Lane D street pedestrians
import { loadPedFleet } from './js/citizens/rigs.js'; // [Lane F §3.4] Lane D rig fleet (GLB peds/keepers)
import { preloadManifest } from './js/interiors/interiors.js'; // [Lane F §3.4] Lane E manifest → interior GLB upgrades
main().catch((err) => {
console.error('[procity] init failed:', err);
@ -65,6 +67,10 @@ async function main() {
// ── plan: prefer Lane A's generatePlan if it has landed, else the fixture ──
const params = new URLSearchParams(location.search);
const seed = (parseInt(params.get('seed'), 10) || 20261990) >>> 0;
// [Lane F §3.4] asset-free gate: ?noassets=1 → skip the manifest + rig fleet + interior GLB upgrades so
// the whole town runs 100% on primitives/flat-colour with ZERO network. Everything below is fail-soft
// anyway (primitives persist if a GLB never lands); this flag makes "no network at all" explicit + testable.
const NOASSETS = params.has('noassets') && params.get('noassets') !== '0';
let plan;
try {
const citygen = await import('./js/citygen/index.js');
@ -121,13 +127,18 @@ const hud = createHUD({
onEnterShop: (shopId, name) => enterShop(shopId, name),
});
const minimap = createMinimap(plan);
// [Lane F integration] interior bridge — owns the 'interior' mode branch (Lane C buildInterior)
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan });
// [Lane F integration] street pedestrians (Lane D). graph = plan.streets directly; no fleet → asset-free
// placeholder/impostor peds. `?pop=` tunable; default kept modest so placeholder-mode near-tier stays
// inside the ≤300 draw budget (the rig-fleet upgrade §3.4 lets this rise). setTimeOfDay/update in the loop.
const citizens = new CitizenSim({ renderer, scene, camera, citySeed: plan.citySeed, graph: plan.streets });
citizens.setPopulation((parseInt(params.get('pop'), 10) || 140) >>> 0); // midday ≈96 active; worst main-street view ~191 draws (budget 300)
// [Lane F §3.4] asset upgrades — OFF entirely under ?noassets. Fail-soft: primitives persist until (and
// unless) a GLB lands, so the town is fully playable the instant it boots.
// • rig fleet: models/peds/*.glb → shared GLB actors; CitizenSim + keepers upgrade from placeholders.
// • manifest: Lane E's assets/manifest.json → interior fitting GLBs (loaded from the 3GOD depot).
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 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 });
citizens.setPopulation((parseInt(params.get('pop'), 10) || 140) >>> 0); // midday ≈96 active
document.addEventListener('visibilitychange', () => citizens.setPaused(document.hidden));
// spawn on the footpath near the west end of the strip, looking east down main street
@ -145,11 +156,19 @@ function setMode(m) {
if (m === 'map') player.unlock(); // [Lane F] interior keeps first-person lock; street re-locks on click
}
// ── shop hours (Lane F §3.5) — Lane A ships hours:[open,close] as 24h ints + exactly one
// openLate:true landmark per town (always the video rental, closes ≥22). Current hour comes from
// lighting's 6-segment clock (getClock().hour is 'HH:MM'); at night only the openLate shop is open.
function currentHour() { const [h, m] = lighting.getClock().hour.split(':').map(Number); return h + (m || 0) / 60; }
function isOpen(shop) { return shop && shop.hours && currentHour() >= shop.hours[0] && currentHour() < shop.hours[1]; }
// ── door → interior hand-off (Lane B seam → Lane F bridge → Lane C buildInterior) ──
function enterShop(shopId, name) {
window.dispatchEvent(new CustomEvent('procity:enterShop', { detail: { shopId } }));
const shop = plan.shops.find((s) => s.id === shopId);
if (!shop) { hud.showToast(`🚪 ${name}`); return; }
// [Lane F §3.5] closed shops are locked; at night the openLate video shop is the one place still open
if (!isOpen(shop)) { hud.showToast(`🔒 ${name} — CLOSED · opens ${shop.hours[0]}:00`); return; }
// [Lane F integration] attach lot dims + open Lane C's interior, switch to interior mode
const lot = plan.lots.find((l) => l.id === shop.lot);
interiorMode.enter({ ...shop, lot: lot ? { w: lot.w, d: lot.d } : null }, name);
@ -241,7 +260,8 @@ frame();
// expose for debugging / Lane F
window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skins,
interiorMode, citizens, enterShop, leaveShop, THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks
interiorMode, citizens, enterShop, leaveShop, isOpen, currentHour, fleet, noassets: NOASSETS,
THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks
// [Lane B] QA debug harness (window.DBG) — loaded only with ?dbg=1 (LANE_F_NOTES §4). Logic lives
// in js/world/dbg.js; the shell just hands it the systems it owns so shots.py/soak.py can drive it.
if (params.get('dbg')) {

View File

@ -24,13 +24,13 @@ 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 }) {
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false }) {
const scene = new THREE.Scene();
scene.background = new THREE.Color('#0e0b07');
// Lane D shopkeeper — one per interior, at the counter. No fleet passed → asset-free placeholder
// actor (the rig fleet is a later §3.4 upgrade); still turns to greet the player. Ticked in update().
const keepers = new KeeperManager({ camera, citySeed: plan.citySeed });
// Lane D shopkeeper — one per interior, at the counter. With a rig `fleet` (§3.4) the keeper is a
// shared GLB actor; without one it's an asset-free placeholder. Either way it greets the player.
const keepers = new KeeperManager({ camera, citySeed: plan.citySeed, fleet });
let current = null; // active Lane C interior handle
let doorReturn = null; // { x, y, z, ry } on the street to restore on exit
@ -72,7 +72,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan }) {
// enter: build the room, show it, drop the player just inside the door. Returns the Lane C handle.
function enter(shop, name) {
if (current) exit(); // never stack interiors
current = buildInterior(shop, THREE);
current = buildInterior(shop, THREE, { useGLB }); // [Lane F §3.4] depot GLB fittings when assets are on
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);