Lane F R15 (v3.0): identity continuity — wire D's tonight-roster into the crowd (ledger #3)
The ped you followed into a venue now turns up in its crowd. D published the seam in R14; F wires the three touches:
- interior_mode.js: crew.spawn({ roster: rosterOf(shop.id) }) — the front crowd slots become tonight's roster
- index.html: rosterOf=(id)=>citizens.tonightRoster(id) into interior_mode; onAdmit=(e)=>citizens.recordVenueEntry(id,e)
on the queue (each admit relays into the roster); the clear is automatic on the existing setGig(id,false)
- flags_check.py: smoke_continuity — asserts the seam (crowd includes roster-cap, pedIndex in tonightRoster),
empty-roster injects nobody (shell side of D's byte-identical law), + ledger #5 settled-tris honesty
Verified: 2 queue admits carry into the crowd; empty roster 0-injected; settled interior 71k->182k tris
(C's finding — interior gate is draws <=54, no violation). 0 console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
146b5f3d2d
commit
687d23f654
@ -931,6 +931,88 @@ def smoke_gigs(p):
|
||||
b.close()
|
||||
|
||||
|
||||
def smoke_continuity(p):
|
||||
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
|
||||
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
|
||||
tonightRoster(id) → GigCrew.spawn({roster}) → the front crowd slots carry those identities. Asserts the
|
||||
seam on identities (crewInfo.fromRoster + member.pedIndex ∈ tonightRoster), the invariant crowd ⊇
|
||||
roster ∩ cap, and that an EMPTY roster injects nobody (D's byte-identical law, proven here on the shell
|
||||
side; D's sim soak proves the sim side). Also ledger #5 — reports SETTLED tris (after the async
|
||||
instrument GLBs land), not the pre-load number C caught the R13 smoke reporting."""
|
||||
head('SMOKE: continuity (R15 — the ped you followed is in the crowd · empty-roster clean · settled tris)')
|
||||
if not gigs_landed(p):
|
||||
print('· gigs=1 not landed (no plan.gigs) — skipping (Lane A)')
|
||||
return
|
||||
b, pg, errs = new_page(p)
|
||||
try:
|
||||
boot(pg, 'gigs=1')
|
||||
pg.keyboard.press('Space')
|
||||
res = pg.evaluate("""async () => {
|
||||
const P = window.PROCITY, D = window.DBG;
|
||||
const id = P.gigs.venueShopIds[0];
|
||||
const rosterOf = () => (P.citizens.tonightRoster(id) || []).map(e => e.pedIndex);
|
||||
// 1) EMPTY-ROSTER PATH — enter before any admit; crew must inject only what's in the roster
|
||||
// (usually 0). This is the shell side of D's "empty roster = byte-identical to R13" law.
|
||||
D.setSegment(5); // NIGHT — the gig is on
|
||||
const rosterAtEmpty = rosterOf();
|
||||
D.enterShop(id); await new Promise(r => setTimeout(r, 550));
|
||||
const ei = P.interiorMode.crewInfo;
|
||||
const emptyFromRoster = ei ? ei.fromRoster : -1, emptyCrowd = ei ? ei.crowd : 0;
|
||||
D.exitShop(); await new Promise(r => setTimeout(r, 250));
|
||||
// 2) POPULATED PATH — let the street loop spawn the queue, then admit a few EXPLICITLY (rAF
|
||||
// auto-drain is throttled in automation, per B's note). Each admit fires onAdmit → recordVenueEntry.
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const q = P.venueQueues && P.venueQueues.get(id);
|
||||
const admitted = [];
|
||||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) { const e = q.admitOne(); if (e) admitted.push(e.pedIndex); }
|
||||
const roster = rosterOf();
|
||||
D.enterShop(id); await new Promise(r => setTimeout(r, 700)); // build + crew spawn
|
||||
const info = P.interiorMode.crewInfo, room = P.interiorMode.current;
|
||||
const crowd = P.interiorMode.crew ? P.interiorMode.crew.members.filter(m => m.part === 'crowd') : [];
|
||||
const rosterCrowd = crowd.filter(m => m.fromRoster).map(m => m.pedIndex);
|
||||
// 3) SETTLED TRIS (ledger #5) — immediately after build vs after the instrument GLBs + rig atlas land
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const trisImmediate = P.renderer.info.render.triangles, draws = P.renderer.info.render.calls;
|
||||
await new Promise(r => setTimeout(r, 1600));
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const trisSettled = P.renderer.info.render.triangles, drawsSettled = P.renderer.info.render.calls;
|
||||
return { rosterAtEmpty, emptyFromRoster, emptyCrowd, admitted, roster, rosterCrowd,
|
||||
fromRoster: info ? info.fromRoster : -1, crowd: info ? info.crowd : 0,
|
||||
watchPoints: room ? room.watchPoints.length : 0,
|
||||
trisImmediate, draws, trisSettled, drawsSettled };
|
||||
}""")
|
||||
# empty-roster path: crew injects exactly what's in the roster at that moment (0 unless surge beat us)
|
||||
want_empty = min(len(res['rosterAtEmpty']), res['emptyCrowd'])
|
||||
if res['emptyFromRoster'] == want_empty:
|
||||
OK(f"empty roster: crew injected {res['emptyFromRoster']} from a {len(res['rosterAtEmpty'])}-entry roster "
|
||||
f"(crowd {res['emptyCrowd']} — no phantom continuity, R13-identical path)")
|
||||
else:
|
||||
FAIL(f"empty roster: fromRoster {res['emptyFromRoster']} != roster∩cap {want_empty} (roster leaked)")
|
||||
# the queue admits were recorded into tonightRoster (the onAdmit → recordVenueEntry relay)
|
||||
roster, admitted, rosterCrowd = res['roster'], res['admitted'], res['rosterCrowd']
|
||||
if admitted and all(a in roster for a in admitted):
|
||||
OK(f"relay: {len(admitted)} queue admits → tonightRoster ({admitted} ⊆ roster of {len(roster)})")
|
||||
else:
|
||||
FAIL(f"relay: queue admits {admitted} not all in tonightRoster {roster} (onAdmit→recordVenueEntry broke)")
|
||||
# THE SEAM — crowd ⊇ roster ∩ cap, and every roster-crowd member's pedIndex is genuinely in the roster
|
||||
cap = res['crowd']
|
||||
want = min(len(roster), cap)
|
||||
subset_ok = set(rosterCrowd).issubset(set(roster))
|
||||
if res['fromRoster'] == want and want > 0 and subset_ok and set(roster[:cap]).issubset(set(rosterCrowd)):
|
||||
OK(f"continuity seam: {res['fromRoster']} of {cap} crowd came from the roster "
|
||||
f"(crowd ⊇ roster∩cap ✓, all pedIndex ∈ tonightRoster ✓) — the ped you followed is in the crowd")
|
||||
else:
|
||||
FAIL(f"continuity seam: fromRoster {res['fromRoster']} vs roster∩cap {want}; rosterCrowd {rosterCrowd} "
|
||||
f"vs roster {roster} (subset={subset_ok})")
|
||||
# ledger #5 — settled-tris honesty (report both; interior gate is draws, which stays low)
|
||||
OK(f"settled tris: {res['trisImmediate']:,} pre-load → {res['trisSettled']:,} settled "
|
||||
f"(instrument GLBs; draws {res['draws']}→{res['drawsSettled']} ≤ {GIG_DRAWS_MAX} gate)")
|
||||
if errs: FAIL(f"continuity: {len(errs)} console error(s); first: {errs[0][:140]}")
|
||||
else: OK('continuity: 0 console errors')
|
||||
finally:
|
||||
b.close()
|
||||
|
||||
|
||||
def main():
|
||||
srv = ensure_server()
|
||||
try:
|
||||
@ -955,7 +1037,8 @@ def main():
|
||||
smoke_shelfbuy(p) # new (warn) — buy-anywhere (book/toy shelves)
|
||||
smoke_tram(p) # new (warn) — tram (auto-skips if not landed)
|
||||
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
|
||||
smoke_gigs(p) # R12 — v3.0-alpha gig layer (schedule · band · crowd · budget · cover · noassets)
|
||||
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
|
||||
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
|
||||
@ -188,7 +188,8 @@ const gigState = (GIGS_ON && plan.gigs && plan.gigs.length) ? createGigState({ p
|
||||
// [Lane F R9] occupancyOf closure defers to `citizens` (declared just below); only invoked at enter()
|
||||
// time (door click), long after citizens is initialized — so the forward reference is safe.
|
||||
const interiorMode = createInteriorMode({ THREE, renderer, camera, plan, fleet, useGLB: !NOASSETS,
|
||||
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id), gigState });
|
||||
dig: DIG_ON, stockReal: STOCK_REAL, wallet, occupancyOf: (id) => citizens.occupancyOf(id),
|
||||
rosterOf: (id) => citizens.tonightRoster(id), gigState }); // [R15] identity continuity: the crowd IS tonight's roster
|
||||
// [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".
|
||||
@ -385,7 +386,10 @@ function frame() {
|
||||
if (open && !q) {
|
||||
q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
||||
q.spawn(scene, { queueZone: venuePresentation && venuePresentation.queueZoneFor(id),
|
||||
gigId: (gigState.gigOf(id) || {}).gigId | 0 });
|
||||
gigId: (gigState.gigOf(id) || {}).gigId | 0,
|
||||
// [R15] identity continuity: relay each queue admit → D's tonight roster, so the
|
||||
// punter who queued turns up in the interior crowd (clear is automatic at setGig(id,false)).
|
||||
onAdmit: (entry) => citizens.recordVenueEntry(id, entry) });
|
||||
venueQueues.set(id, q);
|
||||
} else if (!open && q) { q.disposeAll(); venueQueues.delete(id); }
|
||||
if (q) q.update(dt);
|
||||
|
||||
@ -38,7 +38,7 @@ const WALLA_KEY = 'crowd-walla'; // [R13] manifest.ambience — crowd murmur l
|
||||
|
||||
export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null, useGLB = false,
|
||||
dig: digEnabled = false, stockReal = false, wallet = null,
|
||||
occupancyOf = null, gigState = null }) {
|
||||
occupancyOf = null, gigState = null, rosterOf = null }) {
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#0e0b07');
|
||||
|
||||
@ -228,15 +228,18 @@ export function createInteriorMode({ THREE, renderer, camera, plan, fleet = null
|
||||
seedKey: `${shop.id}#${i}`,
|
||||
}));
|
||||
}
|
||||
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: 3 on the deck, up to 8 at the watch
|
||||
// points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at watchPoints.length
|
||||
// and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these rigs too.
|
||||
// Instruments are D's primitives this round — E's instrument GLBs landed but nothing wires them yet
|
||||
// (D's `opts.instrumentFor` seam is open); asset law says a primitive band is still a gig. → LANE_F_NOTES §12.
|
||||
// [Lane F R12 — the gig itself] Lane D's GigCrew at Lane C's poses: a 4-piece on the deck, up to 8/12 at
|
||||
// the watch points (~⅓ seeded to dance — John's "a bit of both"). D's own spawn caps the crowd at
|
||||
// watchPoints.length and reuses the gate-protected spawnRig path, so the R10 no-giants gate covers these
|
||||
// rigs too; E's instrument GLBs ride in via D's instrumentFor seam.
|
||||
// [Lane F R15 — identity continuity (ledger #3)] pass D's TONIGHT ROSTER so the front crowd slots ARE the
|
||||
// punters who came in (surge occupants + queue admits, keyed by venue). D fills the rest with seeded
|
||||
// strangers; an EMPTY roster is byte-identical to R13 (D's law), which is what a fresh/quiet venue gets.
|
||||
crewInfo = null;
|
||||
if (gigOn && current.stage && current.watchPoints && current.watchPoints.length) {
|
||||
if (!crew) crew = new GigCrew({ citySeed: plan.citySeed, fleet });
|
||||
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints, gig: tonight });
|
||||
crewInfo = crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints,
|
||||
gig: tonight, roster: rosterOf ? rosterOf(shop.id) : [] });
|
||||
}
|
||||
doorReturn = { x: camera.position.x, y: camera.position.y, z: camera.position.z, ry: camera.rotation.y };
|
||||
camera.position.set(current.spawn.x, EYE, current.spawn.z);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user