From a4946b7ca2495e07c8b44f3d704b7f9e8380731e Mon Sep 17 00:00:00 2001 From: m3ultra Date: Thu, 16 Jul 2026 13:20:43 +1000 Subject: [PATCH] Lane F R16 (v3.1 THE FLIP): defaults on + ?classic covenant + seated-stature gate (ledger #1, #3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The town shows its best by default. gigs / weather / winmap / tram flip ON (each keeps a ?flag=0 opt-out; ?classic=1 forces the exact v2 boot). The prime-law covenant MOVED from "flags off" to "?classic=1" — byte-identical forever (synthetic fingerprint 0x3fa36874). The default boot is now the full experience, deterministic to the gig golden 0xb1d48ea1 (A's R16 corner-poster re-pin). - index.html: CLASSIC + flagOn(); gigs/weather/tram default-on; loadPedFleet({sit:!CLASSIC}) for the seated drummer (?classic keeps the zero-fetch-delta covenant); a window.PROCITY.flags intent surface for the gates - buildings.js: winmap default-on + classic-aware (the one cross-lane flip line the flag placement requires) - flags_check.py: flags_off_regression -> classic_regression (?classic=1 byte-identical); a new default_boot_gate (all four flags on, gig layer present, gig golden, budget); every gig smoke re-pointed at the DEFAULT boot - seated-stature gate (ledger #3, published for D): interior_scale_check.py + flags_check.py accept fig.userData.procitySeated at stature [0.9,1.5] m (the drummer); standing figs stay [1.4,2.0] - qa.sh flags-harness gate name updated (classic regression + default-boot gate) Verified: ?classic byte-identical (0x3fa36874, gig layer absent); default boot = the full town (14 gigs, 15 posters, tram, gig golden green, 112 draws @ the venue block); the seated drummer passes at 1.32 m stature; district (3 venues / 3 genres), continuity, and cover all green on the default boot. Co-Authored-By: Claude Opus 4.8 --- tools/flags_check.py | 126 +++++++++++++++++++++++-------- tools/qa.sh | 2 +- tools/qa/interior_scale_check.py | 7 +- web/index.html | 31 +++++--- web/js/world/buildings.js | 3 +- 5 files changed, 123 insertions(+), 46 deletions(-) diff --git a/tools/flags_check.py b/tools/flags_check.py index e7b5127..e8db607 100644 --- a/tools/flags_check.py +++ b/tools/flags_check.py @@ -27,7 +27,8 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent PORT = 8130 HOST = f'http://127.0.0.1:{PORT}' SEED = 20261990 -GOLDEN_HASH = 0x3fa36874 # frozen determinism anchor (Lane A selfcheck) — UNCHANGED by the R7 flip +GOLDEN_HASH = 0x3fa36874 # frozen v2 determinism anchor (?classic boot) — UNCHANGED by the flips +GIG_GOLDEN = 0xb1d48ea1 # [R16] the DEFAULT boot's gig-augmented plan fingerprint (A's R16 corner-poster re-pin) # flags-off DBG.shot('street_noon') snapshot. RE-PINNED at the R7 roster flip (prime-law amendment): # old (v1.1, roster-off): 163 draws, 19000 tris # new (R7, roster default-on): 164 draws, ~66000 tris @@ -52,6 +53,12 @@ def WARN(m): warns.append(m); print(f" \033[33m! WARN\033[0m {m}") def OK(m): print(f" \033[32m✓\033[0m {m}") def head(m): print(f"\n\033[1m{m}\033[0m") +def stature_ok(f): + # [R16 ledger #3] seated figs (the drummer — fig.userData.procitySeated) pass the no-giants floor at a + # SEATED band [0.9,1.5] m; standing figs stay [1.4,2.0]. A seated human fails the standing floor by design. + lo, hi = (0.9, 1.5) if f.get('seated') else (1.4, 2.0) + return lo <= f['stature'] <= hi + def port_up(port): with socket.socket() as s: s.settimeout(0.4) @@ -89,7 +96,7 @@ def gigs_landed(p): """Boot ?gigs=1; landed iff Lane A's augmentation produced a schedule (else the flag is inert).""" b, pg, _ = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') return bool(pg.evaluate("() => !!(window.PROCITY.plan.gigs && window.PROCITY.plan.gigs.length)")) except Exception: return False @@ -110,11 +117,15 @@ def plansrc_landed(p): b.close() # ── the three checks ───────────────────────────────────────────────────────── -def flags_off_regression(p): - head('FLAGS-OFF REGRESSION (prime-law enforcement)') +def classic_regression(p): + # [R16 — THE FLIP] The prime-law covenant MOVED from "flags off" to "?classic=1": the classic boot must + # stay byte-identical to the frozen v2 baseline FOREVER — synthetic fingerprint 0x3fa36874, zero gig + # layer, no opt-in flags, the R7 streamed roster (which IS the v2.0 baseline). Same assertions as the old + # flags-off gate, now booted with ?classic=1 (the four flipped flags are off, everything else v2-default). + head('CLASSIC REGRESSION (?classic=1 → the frozen v2 covenant)') b, pg, errs = new_page(p) try: - boot(pg, '') + boot(pg, 'classic=1') state = pg.evaluate("""() => { const P=window.PROCITY, D=window.DBG; D.shot('street_noon'); // fixed pose → comparable draws/tris @@ -138,26 +149,26 @@ def flags_off_regression(p): if hash_ok and r.returncode == 0: OK(f'plan fingerprint == golden {hex(GOLDEN_HASH)} (selfcheck green)') else: FAIL(f'plan fingerprint != golden {hex(GOLDEN_HASH)} — determinism/prime-law broken') - # winmap + dig stay OPT-IN (off by default). The streamed roster is now the shipping DEFAULT - # (R7 flip / prime-law amendment) — so it must be ON, not off, on a no-flag boot. + # winmap flips OFF under ?classic (it's default-on now); dig stays opt-in. The streamed roster IS the + # v2.0 baseline (R7 flip / prime-law amendment) — so it must be ON under ?classic. if state['winmap'] or state['digActive']: - FAIL(f"an opt-in v2 flag is active on default boot (winmap/dig): {state}") - else: OK('opt-in flags off on default boot (winmap/dig)') + FAIL(f"a flipped/opt-in flag leaked into ?classic (winmap/dig): {state}") + else: OK('flipped + opt-in flags off under ?classic (winmap/dig)') if state['streamMode']: - OK('R7 roster flip in effect — streamed roster ACTIVE on default boot') - else: FAIL('roster flip REGRESSED — default boot is not streaming (expected streamMode:true)') + OK('R7 roster flip in effect — streamed roster ACTIVE under ?classic (the v2 baseline)') + else: FAIL('roster flip REGRESSED — ?classic is not streaming (expected streamMode:true)') - # [R12] gig layer absent flags-off (prime flag law for v3). Any of these leaking means ?gigs=1 - # has escaped its flag and the frozen v2 goldens are living on borrowed time. + # [R12/R16] gig layer wholly absent under ?classic. Any of these present means the gig layer escaped + # the classic gate and the frozen v2 covenant is broken. gig_leaks = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]] if not gig_leaks: - OK('v3 gig layer wholly absent flags-off (no state machine, no plan.gigs/posters, no pub, no venue geometry)') + OK('v3 gig layer wholly absent under ?classic (no state machine, no plan.gigs/posters, no pub, no venue geometry)') else: - FAIL(f"v3 gig layer LEAKED into the default boot: {[(k, state[k]) for k in gig_leaks]}") + FAIL(f"v3 gig layer LEAKED into ?classic: {[(k, state[k]) for k in gig_leaks]}") if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX: - OK(f"flags-off street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})") - else: FAIL(f"flags-off street view OVER budget: draws {state['draws']}, tris {state['tris']}") + OK(f"?classic street view within budget (draws {state['draws']}≤{STREET_DRAWS_MAX}, tris {state['tris']}≤{STREET_TRIS_MAX})") + else: FAIL(f"?classic street view OVER budget: draws {state['draws']}, tris {state['tris']}") # Drift WARN is DRAWS-ONLY — tris is crowd-variable post-flip (guarded by the hard cap above). dd = abs(state['draws'] - BASE_DRAWS) / BASE_DRAWS @@ -166,8 +177,8 @@ def flags_off_regression(p): else: WARN(f"draws drifted >40% from baseline ({state['draws']} vs {BASE_DRAWS}) — investigate") - if errs: FAIL(f"{len(errs)} console error(s) on default boot; first: {errs[0][:140]}") - else: OK('0 console errors on default boot') + if errs: FAIL(f"{len(errs)} console error(s) under ?classic; first: {errs[0][:140]}") + else: OK('0 console errors under ?classic') # R7 escape hatch: ?roster=v1 must restore the fixed roster (streamMode:false), render, no errors. b2, pg2, errs2 = new_page(p) @@ -183,6 +194,54 @@ def flags_off_regression(p): finally: b.close() + +def default_boot_gate(p): + # [R16 — THE FLIP] The enforcement arm of the DEFAULT law: the no-flag boot is now the full experience — + # all four flags on, the gig layer present, deterministic to the gig golden. This is the number every + # budget/smoke now means (its inverse is classic_regression above). + head('DEFAULT-BOOT GATE (the flip → the town shows its best by default)') + b, pg, errs = new_page(p) + try: + boot(pg, '') + state = pg.evaluate("""() => { + const P = window.PROCITY, D = window.DBG; + D.setSegment(5); D.shot('venue_night'); // NIGHT at a venue → the busiest default view (gigs+pools+winmap+tram) + const i = D.info(); + return { flags: P.flags, + gigState: !!P.gigs, planGigs: (P.plan.gigs||[]).length, posters: (P.plan.posters||[]).length, + pub: !!(P.plan.shops||[]).find(s => s.venue), venueGeo: !!P.scene.getObjectByName('venue-presentation'), + winmapActive: !!(P.winmap && P.winmap.active), tram: !!P.scene.getObjectByName('tram'), + draws: i.drawCalls, tris: i.tris }; + }""") + f = state['flags'] or {} + if f.get('gigs') and f.get('weather') and f.get('winmap') and f.get('tram') and not f.get('classic'): + OK('default boot flips all four ON (gigs · weather · winmap · tram), classic off') + else: + FAIL(f"default boot flag intent wrong: {f}") + present = [k for k in ('gigState', 'planGigs', 'posters', 'pub', 'venueGeo') if state[k]] + if len(present) == 5 and state['tram']: + OK(f"the town is live: gig layer present ({state['planGigs']} gigs, {state['posters']} posters, venue geo) · tram up (winmap is a window shader — its own smoke)") + else: + FAIL(f"default boot not fully live: gig={present}, tram={state['tram']}") + # The default boot's plan IS the gig-augmented plan; selfcheck pins its fingerprint to the gig golden + # (0xb1d48ea1) INTERNALLY and is ALL GREEN only if it matches. (selfcheck prints the base fingerprint + # in its summary but asserts the gig golden silently — so we gate on the green run, not a substring.) + r = subprocess.run(['node', 'web/js/citygen/selfcheck.js'], cwd=str(ROOT), capture_output=True, text=True) + if r.returncode == 0 and 'ALL GREEN' in r.stdout: + OK(f"default plan pins to the gig golden {hex(GIG_GOLDEN)} (selfcheck ALL GREEN — the gig-layer golden asserts pass)") + else: + FAIL(f"selfcheck not green — the gig golden {hex(GIG_GOLDEN)} or determinism broke: {r.stdout[-160:]}") + # budget on the DEFAULT boot at the busiest venue block (the number the budget law now means) + if state['draws'] <= STREET_DRAWS_MAX and state['tris'] <= STREET_TRIS_MAX: + OK(f"default-boot street budget @ venue block: {state['draws']} draws ≤ {STREET_DRAWS_MAX}, {state['tris']:,} tris ≤ {STREET_TRIS_MAX:,}") + else: + FAIL(f"default-boot street OVER budget @ venue block: {state['draws']} draws / {state['tris']:,} tris (Fable scope call — not silent-trim)") + if errs: FAIL(f"{len(errs)} console error(s) on the default boot; first: {errs[0][:140]}") + else: OK('0 console errors on the default boot') + finally: + b.close() + + def smoke(p, label, query, is_combo=False): head(f'SMOKE: {label} (?{query})') b, pg, errs = new_page(p) @@ -612,7 +671,7 @@ def smoke_gigs(p): for _ in range(2): b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') sigs.append(pg.evaluate("() => JSON.stringify({g: window.PROCITY.plan.gigs, p: window.PROCITY.plan.posters})")) if errs: FAIL(f"gigs boot: {len(errs)} console error(s); first: {errs[0][:140]}") finally: @@ -626,7 +685,7 @@ def smoke_gigs(p): # 2) the gig night itself: band · crowd · dance mix · budget · the live bed · no giants b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') pg.keyboard.press('Space') # gesture → AudioContext unlock (headless-safe, no pointer-lock) night = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3(); @@ -650,7 +709,8 @@ def smoke_gigs(p): (m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) { o.getWorldPosition(v); crown = Math.max(crown, v.y); } }); return { part: m.part, kind: m.kind, dance: !!m.dance, - crown: +crown.toFixed(3), stature: +(crown - (m.base.y || 0)).toFixed(3) }; + crown: +crown.toFixed(3), stature: +(crown - (m.base.y || 0)).toFixed(3), + seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3] }); // Force one clean interior frame rather than trusting whatever the last rAF left in // renderer.info — a throttled/backgrounded tab otherwise reports the last STREET frame @@ -693,7 +753,7 @@ def smoke_gigs(p): FAIL(f"gig-night interior budget: {night['draws']} draws > {GIG_DRAWS_MAX} (tris {night['tris']:,})") # no giants on the stage/crowd rigs (R10 gate, inherited by the gig path) rigs = [f for f in night['figs'] if f['kind'] == 'rig'] - bad = [f for f in rigs if not (1.4 <= f['stature'] <= 2.0) or f['crown'] >= night['H']] + bad = [f for f in rigs if not stature_ok(f) or f['crown'] >= night['H']] # [R16 #3] seated exemption if not rigs: WARN(f"no-giants: crew spawned as placeholders (no fleet) — rig check n/a ({len(night['figs'])} figs)") elif bad: @@ -719,7 +779,7 @@ def smoke_gigs(p): WARN(f"district: could not read manifest music keys ({e}) — gigKey-resolves check is JS-only") b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') pg.keyboard.press('Space') district = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG, THREE = P.THREE, v = new THREE.Vector3(); @@ -737,7 +797,8 @@ def smoke_gigs(p): let crown = 0; m.actor.fig.updateWorldMatrix(true, true); (m.actor.inner || m.actor.fig).traverse(o => { if (o.isBone && /head/i.test(o.name)) { o.getWorldPosition(v); crown = Math.max(crown, v.y); } }); - return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3) }; + return { part: m.part, kind: m.kind, stature: +(crown - (m.base.y || 0)).toFixed(3), + seated: !!(m.actor.fig.userData && m.actor.fig.userData.procitySeated) }; // [R16 #3] }); P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera); out.push({ id, kind: shop && shop.venueKind, genreKey: shop && shop.genreKey, @@ -767,7 +828,7 @@ def smoke_gigs(p): if d['band'] == 4: OK(f" {tag}: 4-piece band") else: FAIL(f" {tag}: expected a 4-piece, got {d['band']}") rigs = [f for f in d['figs'] if f['kind'] == 'rig'] - bad = [f for f in rigs if not (1.4 <= f['stature'] <= 2.0)] + bad = [f for f in rigs if not stature_ok(f)] # [R16 #3] seated exemption if rigs and bad: FAIL(f" {tag}: {len(bad)} giant rig(s) by stature: {bad[:2]}") if len(genres_seen) >= 2: OK(f"district: {len(genres_seen)} distinct genres verified live ({', '.join(sorted(genres_seen))})") @@ -803,7 +864,7 @@ def smoke_gigs(p): # NIGHT with the gig on, let the block stream in, and sample the settled composer frame. b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') st = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG; D.setSegment(5); // NIGHT — frontage lit, posters up @@ -832,7 +893,7 @@ def smoke_gigs(p): # 3) cover ruling, PAID night (seed 20261990 → $10): debit once · no re-charge on re-entry · broke = knockback b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') paid = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG; const id = P.gigs.venueShopIds[0]; // R15: no scalar alias — the primary venue by id @@ -879,7 +940,7 @@ def smoke_gigs(p): # 4) cover ruling, FREE night (seed 1234 → cover 0): walk straight in, wallet untouched b, pg, errs = new_page(p) try: - pg.goto(f'{HOST}/index.html?seed={FREE_GIG_SEED}&dbg=1&gigs=1') + pg.goto(f'{HOST}/index.html?seed={FREE_GIG_SEED}&dbg=1') pg.wait_for_function("window.DBG && window.DBG.ready === true", timeout=20000) pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }") free = pg.evaluate("""async () => { @@ -905,7 +966,7 @@ def smoke_gigs(p): if ('/assets/audio/' in r.url or r.url.endswith('.ogg') or r.url.endswith('.m4a') or 'custom_bands' in r.url or r.url.endswith('.glb')) else None) try: - boot(pg, 'gigs=1&noassets=1') + boot(pg, 'noassets=1') pg.keyboard.press('Space') na = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG; @@ -945,7 +1006,7 @@ def smoke_continuity(p): return b, pg, errs = new_page(p) try: - boot(pg, 'gigs=1') + boot(pg, '') pg.keyboard.press('Space') res = pg.evaluate("""async () => { const P = window.PROCITY, D = window.DBG; @@ -1023,7 +1084,8 @@ def main(): else: print("· plansrc=osm not landed (Lane A) — skipping that flag") - flags_off_regression(p) + classic_regression(p) # [R16] the frozen v2 covenant moved here: ?classic=1 → byte-identical + default_boot_gate(p) # [R16] the flip: the no-flag boot is the full experience (all four on) interior_budget(p) for fl in flags: smoke(p, fl.split('=')[0], fl) diff --git a/tools/qa.sh b/tools/qa.sh index 2669379..14d04bd 100755 --- a/tools/qa.sh +++ b/tools/qa.sh @@ -90,7 +90,7 @@ 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 - run_gate "v2 flags harness (flags-off regression · per-flag · all-on combo · STRICT r7)" \ + run_gate "flags harness (classic regression · default-boot gate · per-flag · smokes · STRICT · R16 flip)" \ 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/tools/qa/interior_scale_check.py b/tools/qa/interior_scale_check.py index fb479ed..4d45a6c 100644 --- a/tools/qa/interior_scale_check.py +++ b/tools/qa/interior_scale_check.py @@ -34,6 +34,7 @@ if '--seed' in sys.argv: SEED = int(sys.argv[sys.argv.index('--seed') + 1]) LO, HI = 1.4, 2.0 # human crown band (Round-10 gate) +SEAT_LO, SEAT_HI = 0.9, 1.5 # [R16 ledger #3] seated figs (the drummer): a seated human's feet→crown span, still < ceiling, feet planted FOOT_EPS = 0.05 # feet must plant at floor y=0 SAMPLE_TYPES = ['opshop', 'toy', 'video', 'record', 'book', 'milkbar', 'clothes', 'hardware'] @@ -76,7 +77,8 @@ MEASURE_JS = r""" (a.inner || a.fig).traverse(o => { if (o.isBone) { o.getWorldPosition(v); if (/head/i.test(o.name)) crown = Math.max(crown, v.y); foot = Math.min(foot, v.y); } }); return { kind: k.kind, browse: !!k.browse, crown: +crown.toFixed(3), foot: +foot.toFixed(3), - stature: +(crown - foot).toFixed(3) }; // [R13 debt #6] feet→crown span (platform-independent) + stature: +(crown - foot).toFixed(3), // [R13 debt #6] feet→crown span (platform-independent) + seated: !!(a.fig && a.fig.userData && a.fig.userData.procitySeated) }; // [R16 ledger #3] seated-fig tag }); return { H: +H.toFixed(2), figs }; } @@ -124,7 +126,8 @@ def main(): bad = [] for f in figs: total_figs += 1 - if not (LO <= f['stature'] <= HI): bad.append(f"stature {f['stature']}m out of [{LO},{HI}]") + lo, hi = (SEAT_LO, SEAT_HI) if f.get('seated') else (LO, HI) # [R16 #3] seated exemption + if not (lo <= f['stature'] <= hi): bad.append(f"stature {f['stature']}m out of [{lo},{hi}]{' seated' if f.get('seated') else ''}") elif not (f['crown'] < H): bad.append(f"crown {f['crown']}m >= ceiling {H}m") elif abs(f['foot']) >= FOOT_EPS: bad.append(f"feet at {f['foot']}m (not planted)") if bad: diff --git a/web/index.html b/web/index.html index c33b4b0..95817bd 100644 --- a/web/index.html +++ b/web/index.html @@ -82,12 +82,18 @@ const NOASSETS = params.has('noassets') && params.get('noassets') !== '0'; // LANE_A_NOTES F2). Zero network either way. Guarded: falls back to synthetic generatePlan, then fixture. const PLANSRC = params.get('plansrc') === 'osm' ? 'osm' : 'synthetic'; const TOWN = params.get('town') || undefined; // [Lane F R8] &town= selects an OSM town (A: melbourne|katoomba) -// [Lane F R12 — v3.0-alpha gig layer] ?gigs=1 turns on Lane A's post-hoc gig augmentation (the pub venue, -// plan.gigs, plan.posters) and everything F wires off it: the state machine, B's lit frontage + muffled -// spill, C's band room, D's band + crowd, the cover charge at the door. Default OFF ⇒ generatePlanFor -// returns the byte-identical v2 plan and NONE of the gig modules are ever constructed (prime flag law — -// the flags-off regression gate in tools/flags_check.py enforces exactly that). -const GIGS_ON = params.has('gigs') && params.get('gigs') !== '0'; +// [Lane F R16 — THE FLIP (v3.1)] The town shows its best by DEFAULT: gigs / weather / winmap / tram are ON +// unless individually opted out (?gigs=0, ?weather=0, …) or ?classic=1 forces the exact v2 boot. The prime +// law is REWRITTEN, not repealed: the byte-identical covenant (synthetic fingerprint 0x3fa36874, zero gig +// layer, zero fetch delta) moved from "flags off" to "?classic=1". `flagOn(name)`: default-on, `=0` opts +// out, classic is the master off. winmap flips the same way in buildings.js; the drummer's sit clip rides !CLASSIC. +const CLASSIC = params.has('classic') && params.get('classic') !== '0'; +const flagOn = (name) => !CLASSIC && params.get(name) !== '0'; +// [Lane F R12/16] The gig layer — now DEFAULT-ON: Lane A's post-hoc augmentation (venues, plan.gigs, +// plan.posters) + everything F wires off it (the state machine, B's frontage/spill, C's rooms, D's +// band/crowd/queue, the cover charge). ?gigs=0 or ?classic=1 ⇒ generatePlanFor returns the byte-identical +// v2 plan and NONE of the gig modules are constructed (the classic-regression gate enforces exactly that). +const GIGS_ON = flagOn('gigs'); // John's OPTIONAL band-name drop-in (web/assets/custom_bands.json — Lane A owns the file + schema; edit it // and reload, the names get priority on the town's posters). Absent file → pure generator, zero errors. // ZERO fetch when the flag is off or under ?noassets=1 (asset law). `no-cache` on purpose: this file exists @@ -166,7 +172,7 @@ const minimap = createMinimap(plan); // 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/'); +const fleet = NOASSETS ? null : loadPedFleet('models/peds/', { sit: !CLASSIC }); // [R16] sit.glb on the default boot (the seated drummer); ?classic keeps the zero-fetch-delta covenant 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) // [Lane F §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1. @@ -232,14 +238,16 @@ document.addEventListener('visibilitychange', () => citizens.setPaused(document. // [Lane F R8 — Lane B weather] ?weather=1 seeded (seed 20261990 default town rolls 'clear'); ?weather=rain| // overcast|clear forces a state. Self-contained module; flag-off never constructs it (byte-identical). D reads // window.PROCITY.weather (always a valid {state,intensity} — {clear,0} when off), it does NOT import weather.js. +// [Lane F R16 flip] weather DEFAULT-ON (seeded — the default town rolls its own state); ?weather=rain|overcast| +// clear forces one; ?weather=0 or ?classic=1 → off (byte-identical). Self-contained; D reads window.PROCITY.weather. const _wp = params.get('weather'); -const weather = (_wp && _wp !== '0') - ? createWeather({ scene, plan, skins, lighting, camera, force: /^(clear|overcast|rain)$/.test(_wp) ? _wp : null }) +const weather = flagOn('weather') + ? createWeather({ scene, plan, skins, lighting, camera, force: /^(clear|overcast|rain)$/.test(_wp || '') ? _wp : null }) : null; const weatherState = weather ? weather.state : { state: 'clear', intensity: 0 }; // [Lane F R9 — Lane B tram] ?tram=1: one seeded tram looping the main-street spine, door-dwell at the // bus shelters. Self-contained (2 draws); flag-off never constructs it (byte-identical). Non-blocking for v2.0. -const tram = (params.get('tram') && params.get('tram') !== '0') ? createTram({ scene, plan, camera, lighting }) : null; +const tram = flagOn('tram') ? createTram({ scene, plan, camera, lighting }) : null; // [R16 flip] default-on; ?tram=0 / ?classic=1 → off // [Lane F R12 — Lane B venue.js] the street side of the gig: E's poster skins at A's plan.posters (band name // overprinted like a shop sign) + the lit pub frontage, whose marquee/sign glow F ramps from the gig state. // Self-contained module; never constructed flags-off (byte-identical), like weather/tram. @@ -426,6 +434,9 @@ window.PROCITY = { plan, scene, camera, renderer, chunks, lighting, player, skin // decide there's no gig layer at all). `.state` is a live getter: quiet|doors|on|done venuePresentation, // [Lane F R12] Lane B frontage + posters (null flags-off) venueQueues, queueCountOf, // [Lane F R13] Lane D outdoor queue per venue + count accessor (F's smoke reads it) + // [Lane F R16 — THE FLIP] the flag-intent surface: what SHOULD be on this boot (the gates verify intent vs + // reality). Default boot = all four flipped flags true; ?classic=1 = all false + classic true; each `=0` opts out. + flags: { classic: CLASSIC, gigs: GIGS_ON, weather: flagOn('weather'), winmap: flagOn('winmap'), tram: flagOn('tram') }, THREE, get mode() { return MODE; } }; // [Lane F] bridge + drive hooks // [Lane B R11 audio] street WebAudio engine — self-unlocking on the first gesture; silent with // zero/blocked assets or ?mute=1; ?noassets=1 ⇒ no audio fetches. Exposes window.PROCITY.audio so diff --git a/web/js/world/buildings.js b/web/js/world/buildings.js index c5e596a..6713954 100644 --- a/web/js/world/buildings.js +++ b/web/js/world/buildings.js @@ -59,7 +59,8 @@ function windowMaterial() { // all windows (per-shop variation via aTint/aSeed attributes), same 1 draw/chunk as v1. Respects §3.5 // via the same uHour/uNight + aHours: closed-at-night rooms go dark, open rooms glow warm. Fully // procedural (no fetch). Flag-off path is untouched (v1 windowMaterial()). -const WINMAP = (() => { try { const p = new URLSearchParams(location.search); return p.has('winmap') && p.get('winmap') !== '0'; } catch (e) { return false; } })(); +// [Lane F R16 — THE FLIP] winmap DEFAULT-ON; ?winmap=0 opts out, ?classic=1 forces off (the v2 covenant). +const WINMAP = (() => { try { const p = new URLSearchParams(location.search); const classic = p.has('classic') && p.get('classic') !== '0'; return !classic && p.get('winmap') !== '0'; } catch (e) { return false; } })(); const ROOM_TINTS = [ // muted 90s interior wall colours (RGB 0..1), seeded per shop [0.56, 0.44, 0.33], [0.42, 0.47, 0.52], [0.52, 0.47, 0.37], [0.47, 0.42, 0.46], [0.54, 0.50, 0.41], [0.40, 0.45, 0.40], [0.58, 0.49, 0.40], [0.45, 0.48, 0.55],