diff --git a/F-progress.md b/F-progress.md index 7473abc..b56371f 100644 --- a/F-progress.md +++ b/F-progress.md @@ -12,8 +12,8 @@ C1 `?stock=real` (`3724a65`) all landed. F executes flip → strict harness → | task | status | |---|---| | **F1 — roster flip (prime-law amendment)** | ✅ **DONE + verified.** `index.html`: streamed roster is now the **default** (`chunkStream:{radius:2}` per D's spec); `?roster=v1` = escape hatch (fixed roster); `?roster=stream` still accepted (no-op). Hours-aware `setNightLivelyChunks` on the open-late block. **Same commit** re-pins the flags-off baseline: **old (163 draws/19k tris, roster-off) → new (164 draws/~66k tris, roster-on)** — draws barely move (E1's merge = ~1 draw/rig, the whole point), tris = the crowd (D worst-view 65,899; drift-WARN now draws-only, tris under the hard cap). **Plan goldens UNCHANGED (`0x3fa36874`).** Also fixed the harness's roster probe (was reading a nonexistent prop → blind) + flipped the assertion to require streamMode ON + added an escape-hatch check. Verified: default boot streamMode:true (146–150 active), `?roster=v1` streamMode:false (96), both render, 0 errors, ≤300 draws, harness GREEN. | -| **F2 — harness to strict** | ⏳ next: flags failures fail qa.sh; add `stock=real` + `weather=1` smokes (new flags at warn). | -| **F3 — tag v2.0-beta** | ⏳ after F2 + B weather / A osm-parity land (decision #6). | +| **F2 — harness to strict + new-flag smokes** | ✅ **DONE + verified.** `qa.sh` flags gate `warn_gate → run_gate` — the harness (flags-off regression + 4 existing flags + all-on combo) is now **STRICT** (a FAIL fails qa). Wired **`?stock=real`** (F owned the seam): shell preloads the record pack + passes `stockReal`; `interior_mode` builds `makeStockAdapter(getStockPack(shop.type))` and passes it to **both** `buildInterior` (static crate sleeves) and `dig.open` (riffle) — fail-soft to parody if the pack's absent. Added `stock=real` + `weather=1` smokes at **warn-level** (decision #4). Verified: crates show **real GODVERSE covers** (smoke: 7 real atlas sleeve meshes, non-parody batched); weather auto-skips (Lane B not landed); `qa.sh --strict` GREEN (5 gates). | +| **F3 — tag v2.0-beta** | ⏳ after B weather / A osm-parity land (decision #6). Flip + stock done; harness strict-green. | --- diff --git a/docs/shots/laneF/stock_real_dig.png b/docs/shots/laneF/stock_real_dig.png new file mode 100644 index 0000000..229ca6c Binary files /dev/null and b/docs/shots/laneF/stock_real_dig.png differ diff --git a/tools/flags_check.py b/tools/flags_check.py index 8bf0425..5f0cb5f 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -245,6 +245,54 @@ def interior_budget(p): finally: b.close() +def smoke_stock(p): + """NEW FLAG (warn-level, decision #4): ?stock=real → real GODVERSE sleeves in the crates. + Assert non-parody: real sleeves are batched atlas meshes tagged userData.isStock (Lane C recipe).""" + head('SMOKE: stock=real (?stock=real — real sleeves; new flag → warn-level)') + b, pg, errs = new_page(p) + try: + boot(pg, 'stock=real&dig=1') + ready = False + try: # the shell preloads the pack async — wait for getStockPack('record') to resolve + pg.wait_for_function( + "async () => { const m = await import('./js/interiors/interiors.js');" + " return !!(m.getStockPack && m.getStockPack('record')); }", timeout=12000) + ready = True + except Exception: + pass + res = pg.evaluate("""() => { + const P=window.PROCITY, D=window.DBG; D.setSegment(2); + const rec=(P.plan.shops||[]).find(s=>s.type==='record' && P.isOpen(s)); + if(!rec) return {err:'no open record shop'}; + D.enterShop(rec.id); + let n=0; P.interiorMode.current.group.traverse(o=>{ if(o.userData&&o.userData.isStock&&o.material&&o.material.map) n++; }); + return { shop: rec.name, isStock: n }; + }""") + if res.get('err'): WARN(f"stock=real: {res['err']}") + elif not ready: WARN(f"stock=real: pack didn't preload in 12s — parody fallback (fail-soft); {res['isStock']} isStock meshes") + elif res['isStock'] > 0: OK(f"stock=real: {res['isStock']} real atlas sleeve mesh(es) in '{res['shop']}' — non-parody, batched") + else: WARN(f"stock=real: pack loaded but 0 isStock atlas meshes in '{res['shop']}'") + if errs: WARN(f"stock=real: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + +def smoke_weather(p): + """NEW FLAG (warn-level): ?weather=1 (Lane B). Auto-skips if not landed yet.""" + b, pg, errs = new_page(p) + try: + boot(pg, 'weather=1') + w = pg.evaluate("() => { const P=window.PROCITY||{}; const w=P.weather||(P.lighting&&P.lighting.weather); " + "return w ? { active:!!w.active, particles:!!(w.particles||w.rain), wet:!!w.wet } : null; }") + if not w: + print("· weather=1 not landed (no weather subsystem) — skipping (Lane B)") + return + head('SMOKE: weather=1 (new flag → warn-level)') + if w['active'] and w['particles']: OK(f"weather=1 active with particle layer: {w}") + else: WARN(f"weather=1 present but incomplete: {w}") + if errs: WARN(f"weather=1: {len(errs)} console error(s); first: {errs[0][:140]}") + finally: + b.close() + def main(): srv = ensure_server() try: @@ -260,6 +308,8 @@ def main(): for fl in flags: smoke(p, fl.split('=')[0], fl) smoke(p, 'ALL-ON combo', '&'.join(flags), is_combo=True) + smoke_stock(p) # new flag (warn-level) — ?stock=real + smoke_weather(p) # new flag (warn-level) — ?weather=1 (auto-skips if not landed) finally: if srv: srv.terminate() diff --git a/tools/qa.sh b/tools/qa.sh index 48a414a..afbed3e 100755 --- a/tools/qa.sh +++ b/tools/qa.sh @@ -82,13 +82,15 @@ fi # ── Gate 4 (warn-level, round 6): v2 flag enforcement harness ──────────────── # Flags-off regression (prime-law) + per-flag + all-on-combo smokes. Needs the Playwright venv + -# a browser (self-contained server), auto-skips where absent. Non-blocking this round; strict in r7. +# a browser (self-contained server), auto-skips where absent. STRICT as of R7 (F2): a harness FAIL +# now fails qa (flags-off regression + the four existing flags + all-on combo). New flags (stock, +# weather) report at warn inside the harness, so they never trip this gate until they graduate. # Skip explicitly with --no-flags (fast determinism-only runs). FLAGS_SKIP=0; for a in "$@"; do [ "$a" = "--no-flags" ] && FLAGS_SKIP=1; done if [ "$FLAGS_SKIP" = 1 ]; then soft_skip "v2 flags harness" "--no-flags" elif [ -x tools/.venv/bin/python ] && tools/.venv/bin/python -c "import playwright" 2>/dev/null; then - warn_gate "v2 flags harness (flags-off regression · per-flag · all-on combo)" \ + run_gate "v2 flags harness (flags-off regression · per-flag · all-on combo · STRICT r7)" \ tools/.venv/bin/python tools/flags_check.py else soft_skip "v2 flags harness" "Playwright venv absent (python3 -m venv tools/.venv && …/pip install playwright)" diff --git a/web/index.html b/web/index.html index c8b2331..352cd80 100644 --- a/web/index.html +++ b/web/index.html @@ -55,7 +55,7 @@ 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 +import { preloadManifest, preloadStockPack } from './js/interiors/interiors.js'; // [Lane F] Lane E manifest + Lane C/E stock pack main().catch((err) => { console.error('[procity] init failed:', err); @@ -142,7 +142,11 @@ if (!NOASSETS) preloadManifest(); // prime the manifest so the first interior // [Lane F integration] interior bridge — owns the 'interior' branch (Lane C buildInterior + Lane D keeper) // [Lane F §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1. const DIG_ON = params.has('dig') && params.get('dig') !== '0'; -const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, dig: DIG_ON }); +// [Lane F §F2 stock=real] real GODVERSE sleeves in the crates (v2). Preload the record pack at boot so +// the first interior/dig can batch real covers; fail-soft (missing pack → parody canvas). Off under ?noassets. +const STOCK_REAL = params.get('stock') === 'real' && !NOASSETS; +if (STOCK_REAL) preloadStockPack('record'); +const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS, dig: DIG_ON, stockReal: STOCK_REAL }); // [Lane F §F2] the riffle is cursor-driven (DOM buttons + click-a-sleeve), so release pointer-lock while it's // open and re-lock (on click) after. digActive gates the unlock→leaveShop guard below so opening a bin // doesn't read as "walked out of the shop". diff --git a/web/js/world/interior_mode.js b/web/js/world/interior_mode.js index c8e3881..4aaaee2 100644 --- a/web/js/world/interior_mode.js +++ b/web/js/world/interior_mode.js @@ -15,7 +15,7 @@ // The interior renders into its OWN dark THREE.Scene (the street scene is frozen, not disposed — // CITY_SPEC L3 "pause, not dispose"), so returning to the street is instant. -import { buildInterior } from '../interiors/interiors.js'; +import { buildInterior, getStockPack, makeStockAdapter } from '../interiors/interiors.js'; import { KeeperManager } from '../citizens/keepers.js'; // Lane D — shopkeeper behind the counter import { createDig, binSeed } from '../interiors/dig.js'; // Lane C — crate-riffle (v2, gated on ?dig=1) @@ -25,7 +25,8 @@ 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, fleet = null, useGLB = false, dig: digEnabled = false }) { +export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false, + dig: digEnabled = false, stockReal = false }) { const scene = new THREE.Scene(); scene.background = new THREE.Color('#0e0b07'); @@ -35,6 +36,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null let current = null; // active Lane C interior handle let currentShop = null; // the shop record for the active interior (dig per-bin seeding) + let currentAdapter = null; // [F2 §stock] ?stock=real stockAdapter for this shop (null → parody fallback) let doorReturn = null; // { x, y, z, ry } on the street to restore on exit let exitArmed = false; // disarmed at spawn (spawn sits by the door); arms once player steps inside let dig = null; // [F2] Lane C crate-riffle instance, created lazily on first use (only if ?dig=1) @@ -78,6 +80,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null seed: binSeed((currentShop && currentShop.seed) || 1, key), count: 16, shopName: (current && current.recipe && current.recipe.label) || (currentShop && currentShop.name), shop: currentShop, + stockAdapter: currentAdapter, // [F2 §stock=real] real covers in the riffle (null → parody) onClose: () => window.dispatchEvent(new CustomEvent('procity:digClose')), }); window.dispatchEvent(new CustomEvent('procity:digOpen')); // shell releases pointer-lock for the cursor @@ -123,7 +126,10 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null // 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, { useGLB }); // [Lane F §3.4] depot GLB fittings when assets are on + // [F2 §stock=real] real GODVERSE sleeves in this shop's crates. makeStockAdapter(null) → null → + // Lane C fails soft to the parody canvas, so a missing/unpreloaded pack just shows placeholders. + currentAdapter = stockReal ? makeStockAdapter(getStockPack(shop.type)) : null; + current = buildInterior(shop, THREE, { useGLB, stockAdapter: currentAdapter }); // static room sleeves currentShop = shop; scene.add(current.group); // Lane D keeper at the counter — Lane C tags the stand pose on the counter interactable @@ -191,6 +197,7 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null current.dispose(); current = null; currentShop = null; + currentAdapter = null; banner.style.display = 'none'; if (doorReturn) { camera.position.set(doorReturn.x, doorReturn.y, doorReturn.z);