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:
parent
ab7a8d0edf
commit
a4946b7ca2
@ -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)
|
||||
|
||||
@ -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)"
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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],
|
||||
|
||||
Loading…
Reference in New Issue
Block a user