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>
@ -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 (A–E) 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
|
||||
|
||||
@ -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 1–4,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 1–6; 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 ≤~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).
|
||||
|
||||
1
docs/shots/contact.html
Normal 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>
|
||||
BIN
docs/shots/v1_tour/arcade.png
Normal file
|
After Width: | Height: | Size: 428 KiB |
BIN
docs/shots/v1_tour/crossroads_busy.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docs/shots/v1_tour/market_square.png
Normal file
|
After Width: | Height: | Size: 620 KiB |
BIN
docs/shots/v1_tour/milkbar_dusk.png
Normal file
|
After Width: | Height: | Size: 708 KiB |
BIN
docs/shots/v1_tour/night_neon.png
Normal file
|
After Width: | Height: | Size: 359 KiB |
BIN
docs/shots/v1_tour/opshop_interior.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docs/shots/v1_tour/record_interior.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docs/shots/v1_tour/residential_collar.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docs/shots/v1_tour/street_noon.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
docs/shots/v1_tour/warehouse_fringe.png
Normal file
|
After Width: | Height: | Size: 485 KiB |
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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')) {
|
||||
|
||||
@ -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);
|
||||
|
||||