Lane D R14 (v3.0): identity continuity — the ped you followed is in the crowd
The last charter promise. A per-venue tonight roster (sim.js _venueRoster) of
who entered a gig venue tonight — surge occupants self-record in _enter, queue
admits relay via citizens.recordVenueEntry — is consumed by GigCrew.spawn: the
crowd's front slots BECOME the roster identities (same pedIndex, same height),
seeded strangers fill the rest.
- No golden move: both goldens are plan-gen hashes; each crowd slot is an
independently-keyed rng stream, so a roster override touches only that slot and
an EMPTY roster is byte-identical to R13 (verified). Roster heights 1.55-1.95m
stay inside the no-giants [1.4,2.0] gate. Pure bookkeeping: 0 rng, 0 GPU.
- Roster persists across interior exit/re-enter, bounded, auto-clears on the
setGig(id,false) transition (per-night). Deduped by stable key.
- queue.js: admitOne() returns {pedIndex,height,key} + fires an onAdmit callback
(auto-drain AND explicit) so F relays every walked-in punter to the roster;
_spawnAt stashes the identity. band.js: _make returns pedIndex; members carry
it for F's continuity smoke; crew.spawn is now null-gig-safe.
- Verified: surge 8/8 + queue 4/4 real identities land in the crowd; empty-roster
== R13; 7-night x 3-venue soak leak geo0/tex0, roster bounded, no identity
churn; interior 64 draws <=350 (0 new rigs); ?noassets placeholders, 0 fetch.
- F seam in LANE_D_NOTES: crew.spawn({roster}) + queue onAdmit relay + the
tonightRoster(venueShopId) accessor.
- Debt #2 (sit clip): investigated, deferred to v3.1 — the clip converts+binds
(tailnet Sitting_Idle.fbx -> 394KB sit.glb) but a real seated pose is ~1.2m
stature (fails F's no-giants floor) and needs a _rotWithHips loader + replant
(shared rig-stack) + an F smoke exemption; not a freeze-week change. SEAT_DROP
hack kept (reads seated from the money-shot front).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
eee60e05a5
commit
9a65f51a88
@ -6,6 +6,79 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
|
||||
|
||||
---
|
||||
|
||||
## ROUND 14 — v3.0 release: identity continuity (the ped you followed is in the crowd)
|
||||
|
||||
*PROCITY-D, 2026-07-16. The last charter promise. Behind `?gigs=1`; files `sim.js`, `band.js`, `queue.js`.
|
||||
**No golden moves** — both goldens are plan-gen hashes (independent of runtime crowd identity), and an
|
||||
empty roster is byte-identical to R13. Verified in-shell against A's 3-venue plan.*
|
||||
|
||||
### The tonight roster (sim.js) — who came to the gig
|
||||
A per-venue **Map<key,{pedIndex,height}>** (`_venueRoster`) of the identities who entered a gig venue
|
||||
**tonight**, from two sources:
|
||||
- **Surge occupants** — a streamed ped that patronage steers into a gig venue self-records in `_enter`
|
||||
(gated `this._gigVenues.has(shopId)`), keyed by the ped's stable `c.id` so re-entry doesn't dup. Unlike
|
||||
R9 occupancy (which clears on emerge), the roster **persists past the ped's dwell** — "who came tonight".
|
||||
- **Queue admits** — F relays each `VenueQueue.admitOne()` via `citizens.recordVenueEntry(venueShopId, {pedIndex,height,key})`.
|
||||
|
||||
It's **bounded + per-night**: cleared automatically on the `setGig(id, false)` on→off transition (the gig
|
||||
ends → the crowd disperses), so it survives interior exit/re-enter (gig stays on) but resets each night.
|
||||
Pure bookkeeping — **no rng draw, no GPU** (draw/tri delta 0).
|
||||
|
||||
### The crowd consumes it (band.js) — the front slots become the roster
|
||||
`GigCrew.spawn(roomGroup, { …, roster })`: crowd slot `i` fills from `roster[i]` (its `{pedIndex,height}`),
|
||||
seeded strangers fill the remainder. This is the release-round's load-bearing safety: each crowd slot is
|
||||
an **independently-keyed rng stream** (`gig`/`crowd:<gid>:<slot>`), and `_make`'s existing override branches
|
||||
already skip a slot's own height+pickRig draws when identity is supplied — so a roster override touches
|
||||
**only that slot**, never another slot / the sway-phase stream / the band / either golden. An **empty
|
||||
roster is byte-identical to R13** (verified). Roster heights are the sim's 1.55–1.95 m, inside the
|
||||
no-giants [1.4,2.0] gate. `crew.spawn` returns `{ band, crowd, fromRoster }` and each member carries
|
||||
`member.pedIndex` (for F's continuity smoke — assert on the seam, not pixels).
|
||||
|
||||
### → Lane F: the seam (three touches; the clear is automatic)
|
||||
```js
|
||||
// 1. interior — pass the venue's roster into the crowd (interior_mode.js:239):
|
||||
crew.spawn(current.group, { stage: current.stage, watchPoints: current.watchPoints,
|
||||
gig: tonight, roster: citizens.tonightRoster(shop.id) });
|
||||
// 2. street — relay each queue admit into the roster (index.html per-venue loop ~380-393):
|
||||
q.spawn(scene, { queueZone, gigId, onAdmit: (id) => citizens.recordVenueEntry(venueShopId, id) });
|
||||
// 3. nothing — the roster auto-clears on your existing setGig(id, false) at the gig's end.
|
||||
```
|
||||
Accessor for your smoke: `window.PROCITY.citizens.tonightRoster(venueShopId) → [{pedIndex, height}]`
|
||||
(insertion-ordered, deduped, bounded). Assert **crowd ⊇ roster ∩ cap** via `crew.crewInfo.fromRoster` +
|
||||
`member.pedIndex ∈ roster`. `admitOne()` now returns `{pedIndex,height,key}` (was `1`) — truthy, so your
|
||||
existing `if (q.admitOne())` still holds. Also: `crew.spawn` is now null-`gig`-safe (quiet-night guard).
|
||||
|
||||
### Verified in-shell (seed 20261990, The Exchange Hotel #116)
|
||||
| check | result |
|
||||
|---|---|
|
||||
| roster API — record / dedup-by-key / invalid-pedIndex-ignore / per-venue / clear-on-gig-off | all ✓ |
|
||||
| **surge continuity** — real peds ducked in → roster → crowd | **8/8** (same pedIndex + height) |
|
||||
| **queue continuity** — admits relayed via `onAdmit` → roster | **4/4** |
|
||||
| consume — front slots = roster, tail = seeded strangers unchanged | ✓ |
|
||||
| **determinism** — empty-roster crowd == R13 baseline (pedIndex + stature per slot) | byte-identical ✓ |
|
||||
| **soak** — 7 nights × 3 venues, enter/exit + re-spawn | leak **geo Δ0 / tex Δ0**, roster **bounded** (≤ cap, cleared per night), **no identity churn** |
|
||||
| budget | interior gig **64 draws ≤350**, **0 new rigs** (continuity is bookkeeping) |
|
||||
| `?noassets=1&gigs=1` | crowd = placeholders using roster heights, **no crash, 0 fetch** |
|
||||
|
||||
### Debt #2 (the drummer sit clip) — INVESTIGATED, deferred to v3.1 (real blockers, not a punt)
|
||||
The clip **does exist** — `~/Documents/mixamo-fetch/out/` is NOT on m3ultra (Fable's path is stale; the
|
||||
bank lives on the tailnet source box `johnking@100.91.239.7`). `Sitting_Idle.fbx` there converts cleanly
|
||||
via Blender to a 394 KB **mesh-free `sit.glb`** that binds (65/65 bones; the fleet's `_canon` keeps the
|
||||
colon, `mixamorig:Hips`, so raw Mixamo naming is already canonical). **But wiring it is a v3.1 change, not
|
||||
a release-week one, for two real reasons:**
|
||||
1. **`_rotOnly` breaks a seated pose** — it drops `Hips.quaternion` (pelvic tilt) + all positions (hip
|
||||
descent), giving a "levitating L-sit". Correct wiring needs a parallel `_rotWithHips` filter + a
|
||||
post-pose foot re-plant in `spawnRig` — a change to the **shared** rig stack (keepers/browsers/band/crowd).
|
||||
2. **A genuinely seated figure is ~1.2 m crown-above-feet**, which **fails F's no-giants stature floor
|
||||
[1.4,2.0]** (`flags_check.py:695`) — it needs an F smoke exemption for the seated band member.
|
||||
Both land a qa-red change pending an F handshake, in the freeze round, for a polish item — the drummer
|
||||
already reads seated from the crowd POV (money shot) via the `SEAT_DROP` hack. **Rides Fable's existing
|
||||
v3.1 item "bench-sit loiter from the same `sit` clip".** Recipe (for v3.1): scp Sitting_Idle.fbx → Blender
|
||||
`--background --python` import/export GLB (no bone rename needed) → `web/models/peds/sit.glb` → `_rotWithHips`
|
||||
loader + `spawnRig` re-plant + F stature exemption. See LANE_D_NOTES wishlist #1.
|
||||
|
||||
---
|
||||
|
||||
## ROUND 13 — v3.0-beta the district: the RY fix, instruments/drummer, the queue, multi-venue surge
|
||||
|
||||
*PROCITY-D, 2026-07-15. Behind `?gigs=1`; verified in-shell against A's landed 3-venue plan (pub #116
|
||||
|
||||
@ -128,23 +128,25 @@ export class GigCrew {
|
||||
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
|
||||
const r = rng(this.citySeed, 'gig', key);
|
||||
const h = height != null ? height : 1.6 + r() * 0.3;
|
||||
let actor, kind;
|
||||
let actor, kind, usedIndex = null;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
|
||||
const rig = pk && this.fleet.all[pk.index];
|
||||
const sp = rig && spawnRig(rig, { ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||
if (sp) { actor = sp; kind = 'rig'; }
|
||||
if (sp) { actor = sp; kind = 'rig'; usedIndex = pk.index; } // usedIndex: the fleet ped shown — F's continuity smoke reads it
|
||||
}
|
||||
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'gig-body', key), { height: h }); actor.fig.rotation.y = ry; kind = 'placeholder'; }
|
||||
actor.fig.position.set(x, y, z);
|
||||
target.add(actor.fig);
|
||||
return { actor, kind };
|
||||
return { actor, kind, pedIndex: usedIndex };
|
||||
}
|
||||
|
||||
// spawn(roomGroup, { stage, watchPoints, gig }) — the band on the deck + crowd at the watch points.
|
||||
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, instrumentFor: instFor = instrumentFor } = {}) {
|
||||
// spawn(roomGroup, { stage, watchPoints, gig, roster }) — the band on the deck + crowd at the watch points.
|
||||
// roster (R14): [{pedIndex,height}] of the punters who entered this venue tonight (D's citizens.tonightRoster);
|
||||
// the crowd's front slots become them (continuity), seeded strangers fill the rest.
|
||||
spawn(roomGroup, { stage = null, watchPoints = [], gig = { gigId: 0 }, roster = [], instrumentFor: instFor = instrumentFor } = {}) {
|
||||
this.disposeAll();
|
||||
const gid = gig.gigId | 0;
|
||||
const gid = (gig && gig.gigId) | 0; // null-safe (a quiet-night call would pass no gig)
|
||||
this._gen++;
|
||||
const gen = this._gen;
|
||||
|
||||
@ -157,10 +159,10 @@ export class GigCrew {
|
||||
const seated = p.role === 'drums' || !!p.seated;
|
||||
const poseY = p.y != null ? p.y : (stage.deckY || 0); // C tags per-pose y (front line deckY, drums riser)
|
||||
const y = seated ? poseY - SEAT_DROP : poseY; // sink the drummer so the kit hides the legs
|
||||
const { actor, kind } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
|
||||
const { actor, kind, pedIndex } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
|
||||
{ x: p.x, y, z: p.z, ry: p.ry, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
|
||||
height: 1.68 + rng(this.citySeed, 'gigh', `${gid}:${i}`)() * 0.2 });
|
||||
const member = { actor, kind, part: 'band', role: p.role, dance: false, seated,
|
||||
const member = { actor, kind, pedIndex, part: 'band', role: p.role, dance: false, seated,
|
||||
base: { x: p.x, y, z: p.z, ry: p.ry }, phase: rng(this.citySeed, 'gigp', `b${gid}:${i}`)() * Math.PI * 2, extra: [] };
|
||||
const inst = instFor(p.role);
|
||||
if (inst) { actor.fig.add(inst); member.extra.push(inst); }
|
||||
@ -170,14 +172,24 @@ export class GigCrew {
|
||||
this._addBackline(roomGroup, stage, gen); // amp dressing beside the riser (async, dressing)
|
||||
}
|
||||
|
||||
// ── crowd: stand-and-watch or dance at C's watch points ──
|
||||
// ── crowd: R14 identity continuity — the front slots become tonight's roster (the punters who came in
|
||||
// via the queue / surge), seeded strangers fill the remainder. The roster override passes {pedIndex,height}
|
||||
// for that slot only; _make's existing branches then skip that slot's own height+pickRig draws. Because
|
||||
// every crowd slot is an INDEPENDENTLY keyed stream (`gig`/`crowd:<gid>:<slot>`), this can't shift any
|
||||
// other slot, the sway-phase stream (`gigp`), or the band — and an EMPTY roster is byte-identical to R13
|
||||
// (no override → the exact R13 draws). Roster heights are the sim's 1.55–1.95 m, inside the [1.4,2.0] gate.
|
||||
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
|
||||
pts.forEach((w) => {
|
||||
const { actor, kind } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, { x: w.x, y: 0, z: w.z, ry: w.ry });
|
||||
this.members.push({ actor, kind, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false,
|
||||
let fromRoster = 0;
|
||||
pts.forEach((w, i) => {
|
||||
const who = roster[i] || null;
|
||||
const { actor, kind, pedIndex } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`,
|
||||
who ? { x: w.x, y: 0, z: w.z, ry: w.ry, pedIndex: who.pedIndex, height: who.height }
|
||||
: { x: w.x, y: 0, z: w.z, ry: w.ry });
|
||||
if (who) fromRoster++;
|
||||
this.members.push({ actor, kind, pedIndex, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false, fromRoster: !!who,
|
||||
base: { x: w.x, y: 0, z: w.z, ry: w.ry }, phase: rng(this.citySeed, 'gigp', `c${gid}:${w.slotIndex}`)() * Math.PI * 2, extra: [] });
|
||||
});
|
||||
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length };
|
||||
return { band: this.members.filter(m => m.part === 'band').length, crowd: pts.length, fromRoster };
|
||||
}
|
||||
|
||||
// Swap a band member's primitive instrument for E's manifest GLB, placeholder-persists style. Fail-soft
|
||||
|
||||
@ -42,11 +42,14 @@ export class VenueQueue {
|
||||
}
|
||||
|
||||
// Build the seeded line. `count` overrides the seeded 2..6 (F may cap to the room's spare capacity).
|
||||
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null } = {}) {
|
||||
// onAdmit (R14): fired with {pedIndex,height,key} on EVERY admit (auto-drain or explicit), so F can relay
|
||||
// the punter who walked in into the sim's tonight roster (citizens.recordVenueEntry) → they join the crowd.
|
||||
spawn(parent, { door = null, queueZone = null, gigId = 0, count = null, onAdmit = null } = {}) {
|
||||
this.disposeAll();
|
||||
this._gen++;
|
||||
this.gid = gigId | 0;
|
||||
this.parent = parent;
|
||||
this.onAdmit = onAdmit;
|
||||
const anchor = this._anchor(door, queueZone);
|
||||
if (!anchor) return { queue: 0 };
|
||||
this.anchor = anchor;
|
||||
@ -81,9 +84,10 @@ export class VenueQueue {
|
||||
const key = `${this.gid}:${slot}`;
|
||||
const r = rng(this.citySeed, 'queueh', key);
|
||||
const h = 1.58 + r() * 0.34;
|
||||
let actor, kind;
|
||||
let actor, kind, pedIndex = null;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = pickRig(this.fleet, r());
|
||||
pedIndex = pk ? pk.index : null; // R14: remember the ped so admitOne can hand it to the crowd
|
||||
const rig = pk && this.fleet.all[pk.index];
|
||||
const sp = rig && spawnRig(rig, { ry: a.ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||
if (sp) { actor = sp; kind = 'rig'; }
|
||||
@ -91,8 +95,8 @@ export class VenueQueue {
|
||||
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'queuebody', key), { height: h }); actor.fig.rotation.y = a.ry; kind = 'placeholder'; }
|
||||
actor.fig.position.set(tx, 0, tz);
|
||||
this.parent.add(actor.fig);
|
||||
this.members.push({ actor, kind, slot, target: { x: tx, z: tz }, base: { ry: a.ry },
|
||||
phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
||||
this.members.push({ actor, kind, slot, pedIndex, height: h, key, // key = `${gid}:${slot}` — stable per punter (slot shuffles, key doesn't)
|
||||
target: { x: tx, z: tz }, base: { ry: a.ry }, phase: rng(this.citySeed, 'queuep', key)() * Math.PI * 2 });
|
||||
}
|
||||
|
||||
_gap() {
|
||||
@ -102,15 +106,17 @@ export class VenueQueue {
|
||||
_nextDrainGap() { return DRAIN_MIN + rng(this.citySeed, 'queuedrain', `${this.gid}:${this.members.length}:${this.t | 0}`)() * (DRAIN_MAX - DRAIN_MIN); }
|
||||
|
||||
// Admit the head of the line (nearest the door): it walks in, everyone else shuffles up one slot.
|
||||
// Returns the admitted count so far (F can stop when the room is at watchPoint cap).
|
||||
// R14: returns the admitted punter's identity {pedIndex, height, key} (or null when empty) and fires
|
||||
// onAdmit(identity) — F relays it to citizens.recordVenueEntry so the punter joins the interior crowd.
|
||||
admitOne() {
|
||||
if (!this.members.length) return 0;
|
||||
if (!this.members.length) return null;
|
||||
// the head = the smallest slot (closest to the anchor)
|
||||
let headIdx = 0;
|
||||
for (let i = 1; i < this.members.length; i++) if (this.members[i].slot < this.members[headIdx].slot) headIdx = i;
|
||||
const head = this.members.splice(headIdx, 1)[0];
|
||||
if (head.actor.fig.parent) head.actor.fig.parent.remove(head.actor.fig);
|
||||
head.actor.dispose && head.actor.dispose();
|
||||
const identity = { pedIndex: head.pedIndex, height: head.height, key: head.key };
|
||||
// shuffle everyone forward one slot → new target one step closer to the door
|
||||
const a = this.anchor, gap = this._gap();
|
||||
for (const m of this.members) {
|
||||
@ -119,7 +125,8 @@ export class VenueQueue {
|
||||
m.target.z = a.z + a.trail.z * gap * m.slot;
|
||||
}
|
||||
this._drainAt = this.t + this._nextDrainGap();
|
||||
return 1;
|
||||
if (this.onAdmit) this.onAdmit(identity); // relay the walked-in punter → the crowd roster (auto or explicit)
|
||||
return identity;
|
||||
}
|
||||
|
||||
// idle shuffle + slide up into vacated slots. The seeded auto-drain is a safety so the line empties
|
||||
|
||||
@ -126,6 +126,10 @@ export class CitizenSim {
|
||||
this._gigVenues = new Set(); // R13: venueShopIds with a gig on (F sets per-venue via setGig) → multi-venue surge
|
||||
this.weather = { state: 'clear', intensity: 0 }; // Lane B's PROCITY.weather contract (shell feeds it)
|
||||
this._occupancy = new Map(); // R9 shopId → [{ seed, enteredAt, pedIndex }] — the interior-presence truth
|
||||
this._venueRoster = new Map(); // R14 identity continuity: venueShopId → Map<key,{pedIndex,height}> — who
|
||||
// entered a gig venue TONIGHT (surge occupants + F-relayed queue admits).
|
||||
// GigCrew consumes it so the crowd IS the people who came in. Cleared when
|
||||
// the gig ends (setGig off). Pure bookkeeping — no rng, no GPU.
|
||||
this._dropKeys = []; // scratch: chunks to drop this frame
|
||||
this._activeList = []; // this frame's active citizens (both modes) — test page reads it
|
||||
this.timeOfDay = 0.5; // noon
|
||||
@ -164,14 +168,14 @@ export class CitizenSim {
|
||||
if (this.streamMode) return;
|
||||
this.roster.forEach(c => this._releaseActor(c)); this.roster.length = 0; // tear down the v1 roster
|
||||
if (!this.chunkEdges) this._buildChunkIndex();
|
||||
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear();
|
||||
this.chunkRosters.clear(); this._dropKeys.length = 0; this._encountered.clear(); this._occupancy.clear(); this._venueRoster.clear();
|
||||
this.streamRadius = radius; this.streamPerChunk = perChunk;
|
||||
this.streamMode = true; this._hookDriven = false;
|
||||
}
|
||||
disableStream() {
|
||||
if (!this.streamMode) return;
|
||||
for (const arr of this.chunkRosters.values()) arr.forEach(c => this._releaseActor(c));
|
||||
this.chunkRosters.clear(); this._occupancy.clear(); this.streamMode = false;
|
||||
this.chunkRosters.clear(); this._occupancy.clear(); this._venueRoster.clear(); this.streamMode = false;
|
||||
}
|
||||
|
||||
// rasterise each edge's centreline to the 64m chunks it passes through → which edges seed each chunk
|
||||
@ -340,7 +344,11 @@ export class CitizenSim {
|
||||
// per venue off its per-venue state; the alpha single-venue call (one id) is a subset — still works.
|
||||
setGig(venueShopId, on = true) {
|
||||
if (venueShopId == null) return;
|
||||
if (on) this._gigVenues.add(venueShopId); else this._gigVenues.delete(venueShopId);
|
||||
if (on) this._gigVenues.add(venueShopId);
|
||||
// gig ends (on→off transition) → disperse tonight's roster so the next night starts fresh + it stays
|
||||
// bounded. Persists across interior exit/re-enter (the gig stays "on" through those) — only the real
|
||||
// close clears it. .delete() returns true only on the transition, so re-calls while quiet are no-ops.
|
||||
else if (this._gigVenues.delete(venueShopId)) this._venueRoster.delete(venueShopId);
|
||||
}
|
||||
setWeather(w) { if (w && typeof w.state === 'string') this.weather = { state: w.state, intensity: +w.intensity || 0 }; }
|
||||
|
||||
@ -393,8 +401,18 @@ export class CitizenSim {
|
||||
let occ = this._occupancy.get(id); if (!occ) this._occupancy.set(id, occ = []);
|
||||
occ.push({ seed: c.id, enteredAt: this.timeOfDay, pedIndex: c.pedIndex });
|
||||
c._occShop = id;
|
||||
// R14 continuity: a surge occupant who ducked into a GIG venue joins tonight's roster (the crowd
|
||||
// becomes the people who came in). The roster persists past this ped's short dwell (unlike occupancy,
|
||||
// which clears on emerge) — "who came tonight", keyed by the ped's stable id so re-entry doesn't dup.
|
||||
if (this._gigVenues.has(id)) this._recordRoster(id, c.id, c.pedIndex, c.height);
|
||||
}
|
||||
}
|
||||
// record one entered identity into a venue's tonight roster (deduped by key, insertion-ordered, no rng).
|
||||
_recordRoster(venueShopId, key, pedIndex, height) {
|
||||
let m = this._venueRoster.get(venueShopId);
|
||||
if (!m) this._venueRoster.set(venueShopId, m = new Map());
|
||||
if (!m.has(key)) m.set(key, { pedIndex, height }); // first entry wins
|
||||
}
|
||||
_emerge(c) {
|
||||
if (c._occShop != null) { this._removeOccupant(c._occShop, c.id); c._occShop = null; }
|
||||
const w = c._savedWalk;
|
||||
@ -415,6 +433,20 @@ export class CitizenSim {
|
||||
const occ = this._occupancy.get(shopId) || [];
|
||||
return { count: occ.length, occupants: occ.map(o => ({ ...o })) };
|
||||
}
|
||||
// R14 identity continuity — the D→F seam.
|
||||
// recordVenueEntry: F relays a queue admit (VenueQueue.admitOne() → {pedIndex, height, key}) so the punter
|
||||
// who walked in the door joins the crowd. Surge occupants self-record in _enter; this is the queue side.
|
||||
recordVenueEntry(venueShopId, entry) {
|
||||
if (venueShopId == null || !entry || entry.pedIndex == null) return;
|
||||
const key = entry.key != null ? `q:${entry.key}` : `q:${entry.pedIndex}:${(+entry.height || 0).toFixed(3)}`;
|
||||
this._recordRoster(venueShopId, key, entry.pedIndex, entry.height);
|
||||
}
|
||||
// tonightRoster: the night's entered identities, for GigCrew.spawn (F passes it in) + F's continuity
|
||||
// smoke (assert crowd ⊇ roster ∩ cap). Insertion-ordered (entry order), deduped, bounded, pure read.
|
||||
tonightRoster(venueShopId) {
|
||||
const m = this._venueRoster.get(venueShopId);
|
||||
return m ? [...m.values()].map(e => ({ pedIndex: e.pedIndex, height: e.height })) : [];
|
||||
}
|
||||
// stable identity signature of the active set — immutable spawn identity, NOT live position, so it
|
||||
// holds while citizens walk (the determinism gate: same seed → same crowd, twice). Both modes.
|
||||
identitySignature() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user