Lane F R16 (v3.1 THE FLIP): defaults on + ?classic covenant + seated-stature gate (ledger #1, #3)

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 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-16 13:20:43 +10:00
parent ab7a8d0edf
commit a4946b7ca2
5 changed files with 123 additions and 46 deletions

View File

@ -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)

View File

@ -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)"

View File

@ -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:

View File

@ -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=<key> 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

View File

@ -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],