Lane D R13 (v3.0-beta): 4-piece + instruments, outdoor queue, multi-venue surge, RY fix
- RY audit (debt #4): the ped GLB fleet's visual front is +Z but the whole system assumed -Z (sim heading math, C's pose ry, placeholder, impostor). Fixed at the source (rigs.js buildFigure normalises every rig to -Z front) and dropped band.js RY_FLIP. Corrects keepers/browsers (faced the back wall) and streamed walkers (moon-walked): 223/223 gig + 188/188 flags-off now face travel. Visual only, no golden move. keepers.js/placeholder.js/impostor.js unchanged (fixed for free). Canonical convention -> LANE_D_NOTES for C. - Instruments (debt #2): E's manifest GLBs swap over the primitives async (placeholder-persists, fleet-gated -> 0 fetch under ?noassets), guitar/bass held, mic + kit planted, guitar_amp dressing. - Drummer (debt #3): 4-piece, seated at C's drums pose (sink + kit occlusion; reads from the front/crowd POV - a sit clip is the polish). - Outdoor queue (new queue.js): seeded 2-6 line at the frontage, drains on admitOne() + auto-drain, consumes B's queueZone or a frontage-hugging door fallback. F wiring seam documented in LANE_D_NOTES. - Multi-venue surge (sim.js): setGig -> Set of concurrent venues. - Interior gig 56 draws <=350; ?noassets+?gigs placeholders, silent-and-fine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
5dac2cac5a
commit
fefbb4b806
@ -6,6 +6,101 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
`pubrock` / rsl #404 `covers` / band_room #490 `grunge`, 14 gigs / 14 posters) and C's landed 4-pose
|
||||
stage (guitar/vocal/bass + a seated `drums` on the riser). Files: `rigs.js`, `band.js`, new `queue.js`,
|
||||
`sim.js`. `keepers.js` / `placeholder.js` / `impostor.js` UNCHANGED — the RY fix at the source corrects
|
||||
them for free.*
|
||||
|
||||
### Debt #4 — the RY audit: fixed once at the source (⚠ this changes the flags-off street VISUAL)
|
||||
**Finding (confirmed in-scene):** the imported ped GLBs' visual front is local **+Z**, but *everything
|
||||
downstream assumes −Z* — `sim.js` heading math (`facing = atan2(-dx,-dz)`), C's pose `ry`
|
||||
(counter.stand / bandPoses / watchPoints / browse points), the placeholder (`placeholder.js:61` builds
|
||||
toes to −Z *on purpose*, "like the GLB rigs"), and the impostor bake/pick convention. So the GLB was the
|
||||
lone outlier: **keepers faced the back wall, browsers faced away, and every streamed rig walker moon-walked
|
||||
(faced anti-travel)** — all subtly, for ~12 rounds, because near+mid were consistently-wrong-together
|
||||
(seamless) and peds are mostly seen from behind. R12's band carried a local `RY_FLIP=π` that patched only
|
||||
the gig.
|
||||
|
||||
**Fix:** one line in `rigs.js buildFigure` — `inner.rotation.y = Math.PI` — normalises every rig's front
|
||||
to −Z, matching the rest of the system. Then `RY_FLIP` is deleted from `band.js` (band/crowd use C's `ry`
|
||||
verbatim). Because the impostor atlas bakes this same `buildFigure`/`spawnRig` output and the layer's
|
||||
yaw-pick already uses the −Z convention, near↔mid stays seamless (both flip together). It's **purely
|
||||
visual** — a Y-rotation leaves every bone's world-Y untouched, so head-height / feet-plant, the no-giants
|
||||
gate, and all seeded identity/determinism are byte-unchanged (**no golden move**).
|
||||
|
||||
**Verified:** keeper now faces the shop floor (was the back wall); **223/223** gig-mode + **188/188**
|
||||
flags-off streamed walkers face their edge-travel direction (avg dot **1.000**; was −1.0); band faces the
|
||||
crowd + crowd faces the stage with `RY_FLIP` gone; `?noassets` placeholder band (which the old flip left
|
||||
facing the backdrop) is now correct too.
|
||||
|
||||
- **→ Lane C (pose contract, please land in LANE_C_PUB.md):** *"Rig visual front is canonical **local −Z**
|
||||
(`rigs.js buildFigure` normalises the GLB's native +Z). All pose `ry` (counter.stand, bandPoses,
|
||||
watchPoints, browse points) are the −Z-front heading the rig should face — no lane compensates; the R12
|
||||
`RY_FLIP` is gone."* Your existing `ry` values were already correct under this — nothing to change.
|
||||
- **→ Lane F / Fable (flags-off flag):** the fix is in the shared rig stack, so the **default (flags-off)
|
||||
street crowd now faces travel** instead of moon-walking. Determinism + the golden plan hash are
|
||||
untouched (visual only), but if any regression snapshots *pixels* it will move — it's a bug fix, re-pin
|
||||
if needed (improvement, not a leak).
|
||||
|
||||
### Debt #2 — instrument GLBs wired (placeholder-persists, fail-soft, fleet-gated)
|
||||
`band.js` keeps its primitive instruments as the immediate/`?noassets` fallback, then swaps E's manifest
|
||||
GLBs in async when they resolve (same pattern as `interiors/glb.js`). Role → fitting: `guitar`→
|
||||
`electric_guitar`, `bass`→`bass_guitar`, `vocal`→`mic_stand` (planted), `drums`→`drum_kit` (planted at the
|
||||
riser), plus one `guitar_amp` as backline dressing. Manifest is **self-fetched** (promise-cached) so band.js
|
||||
stays decoupled from C's glb.js, and the whole upgrade is **gated on `fleet.ready`** → under `?noassets`
|
||||
(fleet null) it never fetches (proven: fresh-tab network shows **0** `procity_fit_*` / `manifest`
|
||||
requests). Disposal-guarded by a generation counter; GLB clones share the cached geo/mats (detach-only,
|
||||
never dispose — only the `ownGeo`-tagged primitives are freed). Orientation lives in one `GLB_FIT` table
|
||||
(guitars need a +Z roll to sling; the kit is scaled 0.66 to fit C's tight 0.73 m vocal↔drums gap).
|
||||
|
||||
### Debt #3 — the drummer (4-piece, Fable's ruling)
|
||||
`bandPoses.forEach` already picks up C's 4th `drums` pose (`seated:true`, riser `y`). The drummer sinks
|
||||
`SEAT_DROP` (0.52 m) so the kit hides the straight legs and it reads seated **from the front — the crowd
|
||||
POV / money shot**; from a hard side angle the sink shows (a standing rig faked as seated). A true seated
|
||||
pose is a clip job — see the wishlist. Build is **defensive**: if John had vetoed to a trio, C simply
|
||||
omits the pose and the band is 3 with zero D change.
|
||||
|
||||
### The outdoor queue (charter item — new `web/js/citizens/queue.js`)
|
||||
`VenueQueue`: a seeded 2–6 line on the same gate-protected rig path, idle-animated, that drains as punters
|
||||
are admitted and empties by close. Consumes B's `venue.queueZone` when present, else a straight line off
|
||||
the door — the fallback trails along the **frontage tangent** so it hugs the building instead of spilling
|
||||
onto the road. Verified: spawn (5 rigs, human-sized, hug the frontage, face the venue), `admitOne()` drains
|
||||
the head + shuffles everyone up to target, and a seeded auto-drain safety. **F seam** (wire per venue off
|
||||
the street-side gig state):
|
||||
```js
|
||||
const q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
||||
q.spawn(streetGroup, { door, queueZone, gigId }); // at 'doors' (+ 'on' overflow); door = {x,z,ry}, ry = facing the entrance
|
||||
q.admitOne(); // on each admitted punter (cover-charge / entry hook)
|
||||
q.update(dt); // each street frame — idle + lerp + slow auto-drain
|
||||
q.disposeAll(); // at 'done' / venue out of view
|
||||
```
|
||||
**→ Lane B:** publish `venue.queueZone = { x, z, ry, len }` — `(x,z)` = head by the door, `ry` = the
|
||||
heading a punter faces (toward the entrance), `len` = line length; the line trails single-file opposite the
|
||||
facing, so pick `ry` to run it along the verandah.
|
||||
|
||||
### Multi-venue surge (`sim.js`)
|
||||
`setGig(venueShopId, on)` widened from a single `_gigVenue` to a `_gigVenues` **Set** so several concurrent
|
||||
gigs each pull their own block; `_nearestOpenShop` now prefers the nearest gig venue in `GIG_RANGE` across
|
||||
all of them. F's R12 single-venue call is a subset — still works. Non-gig patronage path is byte-identical.
|
||||
|
||||
### Measured budgets (seed 20261990, "Boolarra Heads", The Exchange Hotel #116)
|
||||
| thing | number | budget |
|
||||
|---|---|---|
|
||||
| gig-night interior, rig fleet + GLB instruments | **56 draws** · 183k tris | ≤350 draws ✓ |
|
||||
| — of which instruments (5 GLBs @ ~14k, E's ≤15k target) | ~70k tris | (no interior tri cap) |
|
||||
| band / crowd | **4** on the deck (3 front + seated drummer) · **8** at 8 watch points | crowd ≤ watchPoints ✓ |
|
||||
| outdoor queue | seeded **2–6** rigs (~1 draw each) at the frontage | stated |
|
||||
| `?noassets`+`?gigs` | band+queue placeholders, **0** GLB/manifest fetch | silent-and-fine ✓ |
|
||||
| flags-off street walkers face travel | 188/188 (dot 1.0) | determinism byte-unchanged ✓ |
|
||||
|
||||
*RSL (#404) fields the crowd-cap stress case: `CROWD_CAP` is 12, real cap = `watchPoints.length` per venue
|
||||
(≤ always holds). Re-measure the RSL interior once its archetype dressing lands.*
|
||||
|
||||
---
|
||||
|
||||
## ROUND 9 — interior presence: occupancy truth + browser rigs (→ Lane F handoff below)
|
||||
|
||||
**Follow a ped into a shop and find them browsing.** D owns occupancy truth; C owns the browse points;
|
||||
@ -493,7 +588,10 @@ bind directly through the canonical `mixamorig` skeleton exactly like walk/idle,
|
||||
data task, not new rig work. **Verify each exists before requesting; never re-download.**
|
||||
|
||||
Wanted for richer loiter/keeper behaviour, in priority order:
|
||||
1. `sit` — bench-sit at benches/verandah steps (sim loiter already has the hook; needs the clip).
|
||||
1. `sit` — bench-sit at benches/verandah steps (sim loiter has the hook; needs the clip). **R13: the gig
|
||||
drummer also wants this (or a bespoke `drum`/`play-drums` loop)** — the 4th band member is currently a
|
||||
standing rig sunk `SEAT_DROP` behind the kit, which reads seated only from the front. A real seated clip
|
||||
retires the sink hack (band.js `SEAT_DROP` + the `m.seated` branch in `update`).
|
||||
2. `lean` — loiter against a shopfront (window-shopping stops).
|
||||
3. `look-around` / `idle-look` — window-shopping variety + a better keeper greet than the body-turn.
|
||||
4. `talk` / `gesture` — pairs chatting on the footpath (spawn in seeded 2-groups).
|
||||
|
||||
@ -1,30 +1,39 @@
|
||||
// PROCITY Lane D — the gig band + crowd (v3.0-alpha, behind ?gigs=1).
|
||||
// PROCITY Lane D — the gig band + crowd (v3.0-beta, behind ?gigs=1).
|
||||
//
|
||||
// A Friday night at the pub: three musicians on Lane C's stage deck and a mixed crowd at C's
|
||||
// watch points — some standing and watching, some dancing (John's "a bit of both"). Built on the
|
||||
// same gate-protected rig path as keepers/browsers (spawnRig → buildFigure, so no giants), with a
|
||||
// placeholder fallback when the fleet is absent (?noassets — a gig with no GLBs is still a gig).
|
||||
// A Friday night at the venue: a 4-piece band on Lane C's stage (guitar / vocal / bass across the
|
||||
// front, a seated drummer on the up-stage riser) and a mixed crowd at C's watch points — some
|
||||
// standing, some dancing (John's "a bit of both"). Built on the gate-protected rig path
|
||||
// (spawnRig → buildFigure, so no giants), with a placeholder fallback when the fleet is absent
|
||||
// (?noassets — a gig with no GLBs is still a gig).
|
||||
//
|
||||
// Instruments: primitives show immediately (asset law), then E's manifest GLBs swap in async where
|
||||
// present (placeholder-persists, like interiors/glb.js). guitar/bass held at the chest, mic stand +
|
||||
// drum kit planted, an amp as backline dressing. Gated on fleet.ready so ?noassets never fetches.
|
||||
//
|
||||
// Seam: F builds one GigCrew per venue interior and calls spawn() when the gig state is doors/on,
|
||||
// update(dt) each interior frame, disposeAll() on gig-off / room exit. C owns the stage + watch
|
||||
// points (room.stage / room.watchPoints); A owns the gig (plan.gigs); E's instrument GLBs replace
|
||||
// the primitives here when present (pass opts.instrumentFor). Deterministic from (citySeed, gigId).
|
||||
// points (room.stage / room.watchPoints); A owns the gig (plan.gigs). Deterministic from (citySeed, gigId).
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { rng, shuffle } from '../core/prng.js';
|
||||
import { pickRig, spawnRig } from './rigs.js';
|
||||
import { makePlaceholder } from './placeholder.js';
|
||||
import { loadGLB } from '../core/loaders.js';
|
||||
|
||||
const CROWD_CAP = 8; // ≤ watchPoints; F's smoke asserts crowd ≤ watchPoints
|
||||
const MIC = 0x222226, POLE = 0x9a9a9e;
|
||||
// C's watchPoints/bandPoses `ry` point the fleet mesh 180° from where spawnRig plants it (verified
|
||||
// in-scene: ry=π faced the band at the backdrop, ry→stage faced the crowd at the door). Flip on spawn
|
||||
// so the band faces the audience and the crowd faces the stage. (Flagged to C/F to reconcile the seam.)
|
||||
const RY_FLIP = Math.PI;
|
||||
const CROWD_CAP = 12; // hard ceiling; the real cap is watchPoints.length per venue (≤ always
|
||||
// holds — F's smoke asserts it). RSL fields the most (8–12 watch points).
|
||||
const MIC = 0x222226, POLE = 0x9a9a9e, KIT = 0x8f2b24, CHROME = 0xc8c8cc, AMP = 0x15151a, AMPCLOTH = 0x3a3020;
|
||||
const SEAT_DROP = 0.52; // the drummer sits: sink the rig so the kit hides the straight legs.
|
||||
// Reads seated from the front (the crowd POV / money shot); a true
|
||||
// sit is a clip job — see the LANE_D_NOTES clip wishlist.
|
||||
|
||||
// ---- primitive instruments (E's GLBs swap in over these; a gig with no GLBs is still a gig) ----
|
||||
// Each returns an Object3D in the actor's OUTER-fig local frame. After the R13 facing-normalise the
|
||||
// rig's front (its chest) is fig-local -Z, so instruments sit on the -Z side and ride the sway.
|
||||
// Primitives own their geometry/material (tagged ownGeo) so disposeAll frees them; GLB clones share
|
||||
// the cached gltf's geo/mats and are only detached.
|
||||
function tagOwn(g) { g.userData.ownGeo = true; return g; }
|
||||
|
||||
// ---- primitive instruments (E's GLBs replace these; footprint-compatible, held at the chest) ----
|
||||
// Each returns an Object3D positioned in the actor's OUTER-fig space (origin at the feet, +Z front),
|
||||
// so it rides the performer's procedural sway. Cheap shared-nothing meshes (3 instruments per gig).
|
||||
function guitarPrim(bodyCol, big = false) {
|
||||
const g = new THREE.Group();
|
||||
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyCol, roughness: 0.5, metalness: 0.1 });
|
||||
@ -34,10 +43,9 @@ function guitarPrim(bodyCol, big = false) {
|
||||
const neck = new THREE.Mesh(new THREE.BoxGeometry(0.05, big ? 0.85 : 0.72, 0.03), neckMat);
|
||||
neck.position.set(bw * 0.1, bh * 0.5 + (big ? 0.42 : 0.36), 0);
|
||||
g.add(body, neck);
|
||||
// the fleet mesh faces its local +Z (verified in-scene), so the chest — and the instrument — is +Z.
|
||||
g.position.set(0.02, 1.02, 0.19); // across the chest, toward the audience
|
||||
g.rotation.set(0.15, 0, big ? 0.7 : 0.62); // slung diagonally, body angled out to the crowd
|
||||
return g;
|
||||
g.position.set(0.02, 1.02, -0.19); // across the chest (fig-local -Z = the audience-facing front)
|
||||
g.rotation.set(0.15, 0, big ? -0.7 : -0.62); // slung diagonally, body angled out to the crowd
|
||||
return tagOwn(g);
|
||||
}
|
||||
function micStandPrim() {
|
||||
const g = new THREE.Group();
|
||||
@ -48,26 +56,75 @@ function micStandPrim() {
|
||||
new THREE.MeshStandardMaterial({ color: MIC, roughness: 0.6 }));
|
||||
head.position.y = 1.44;
|
||||
g.add(pole, head);
|
||||
g.position.set(0, 0, 0.26); // stands just in front of the singer (+Z = audience side)
|
||||
return g;
|
||||
g.position.set(0, 0, -0.26); // stands just in front of the singer (fig-local -Z)
|
||||
return tagOwn(g);
|
||||
}
|
||||
// a cheap kit: kick drum + snare + a floor tom + two cymbals on stands. Reads as "drum kit" behind the
|
||||
// seated drummer; the GLB replaces it when present. Sits on the riser (y = SEAT_DROP compensates the sink).
|
||||
function drumKitPrim() {
|
||||
const g = new THREE.Group();
|
||||
const shell = new THREE.MeshStandardMaterial({ color: KIT, roughness: 0.4, metalness: 0.1 });
|
||||
const chrome = new THREE.MeshStandardMaterial({ color: CHROME, roughness: 0.3, metalness: 0.7 });
|
||||
const skin = new THREE.MeshStandardMaterial({ color: 0xe8e4d8, roughness: 0.6 });
|
||||
const kick = new THREE.Mesh(new THREE.CylinderGeometry(0.30, 0.30, 0.42, 16), shell);
|
||||
kick.rotation.x = Math.PI / 2; kick.position.set(0, 0.30, 0);
|
||||
const kickHead = new THREE.Mesh(new THREE.CircleGeometry(0.29, 16), skin);
|
||||
kickHead.position.set(0, 0.30, -0.215); kickHead.rotation.y = Math.PI;
|
||||
const snare = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.16, 0.16, 14), chrome);
|
||||
snare.position.set(-0.32, 0.58, -0.10);
|
||||
const tom = new THREE.Mesh(new THREE.CylinderGeometry(0.19, 0.19, 0.22, 14), shell);
|
||||
tom.position.set(0.34, 0.52, -0.06);
|
||||
const mkCymbal = (x, y, r) => {
|
||||
const c = new THREE.Group();
|
||||
const stand = new THREE.Mesh(new THREE.CylinderGeometry(0.012, 0.012, y, 6), chrome);
|
||||
stand.position.y = y / 2;
|
||||
const disc = new THREE.Mesh(new THREE.CylinderGeometry(r, r, 0.01, 16), chrome);
|
||||
disc.position.y = y;
|
||||
c.add(stand, disc); c.position.x = x; return c;
|
||||
};
|
||||
g.add(kick, kickHead, snare, tom, mkCymbal(-0.44, 0.86, 0.18), mkCymbal(0.50, 0.94, 0.20));
|
||||
g.position.set(0, SEAT_DROP, -0.60); // on the riser, in front of the seated drummer (fig-local -Z)
|
||||
return tagOwn(g);
|
||||
}
|
||||
function instrumentFor(role) {
|
||||
if (role === 'guitar') return guitarPrim(0xb23b2e); // sunburst-ish red
|
||||
if (role === 'bass') return guitarPrim(0x2f3a52, true); // dark blue, bigger body + longer neck
|
||||
if (role === 'guitar') return guitarPrim(0xb23b2e); // sunburst-ish red
|
||||
if (role === 'bass') return guitarPrim(0x2f3a52, true); // dark blue, bigger body + longer neck
|
||||
if (role === 'vocal') return micStandPrim();
|
||||
if (role === 'drums') return drumKitPrim();
|
||||
return null;
|
||||
}
|
||||
|
||||
// role → E's manifest fitting id + its transform in the actor's fig-local frame (front/chest = -Z).
|
||||
// GLBs are base-origin metres (glb_law): guitar/bass stand upright (long axis +Y = neck, thin face along
|
||||
// Z), so a +Z roll slings them diagonally across the chest; the mic stand is upright as-is; the kit is
|
||||
// scaled to fit C's tight riser gap (vocalist 0.73 m in front of the drummer) and planted in front of the
|
||||
// sunk drummer. Tuned on-screen. Body-origin → position is where the strap-button / base sits.
|
||||
const GLB_FIT = {
|
||||
guitar: { id: 'electric_guitar', pos: [0.14, 0.60, -0.15], rot: [0.22, 0.0, 0.72], scale: 1 },
|
||||
bass: { id: 'bass_guitar', pos: [0.14, 0.58, -0.15], rot: [0.22, 0.0, 0.66], scale: 1 },
|
||||
vocal: { id: 'mic_stand', pos: [0.00, 0.00, -0.30], rot: [0, 0, 0], scale: 0.9 },
|
||||
drums: { id: 'drum_kit', pos: [0.00, SEAT_DROP, -0.30], rot: [0, Math.PI, 0], scale: 0.66 },
|
||||
};
|
||||
|
||||
// manifest (self-fetched, promise-cached, fail-soft). ONLY called behind a fleet.ready gate, so under
|
||||
// ?noassets (fleet === null) this never runs → zero asset fetch, primitives everywhere. Decoupled from
|
||||
// Lane C's interiors/glb.js on purpose (band.js is asset-law self-contained).
|
||||
let _manifestP = null;
|
||||
function loadManifest() {
|
||||
return (_manifestP ||= fetch('assets/manifest.json').then(r => (r.ok ? r.json() : null)).catch(() => null));
|
||||
}
|
||||
|
||||
export class GigCrew {
|
||||
constructor({ citySeed = 20261990, fleet = null } = {}) {
|
||||
this.citySeed = citySeed >>> 0;
|
||||
this.fleet = fleet;
|
||||
this.members = []; // { actor, kind:'rig'|'placeholder', part:'band'|'crowd', role, dance, base:{x,y,z,ry}, phase, extra:[] }
|
||||
this.members = []; // { actor, kind, part, role, dance, seated, base:{x,y,z,ry}, phase, extra:[] }
|
||||
this.props = []; // planted dressing added straight to the room group (amp) — freed on disposeAll
|
||||
this._gen = 0; // bumped every spawn/dispose; async GLB swaps bail if the gen moved (room gone)
|
||||
this.t = 0;
|
||||
}
|
||||
|
||||
// one seeded rig (or placeholder) actor, planted + facing ry. `key` seeds identity per gig+slot.
|
||||
// pedIndex forces a specific fleet ped (band members are picked distinct); else a seeded pickRig.
|
||||
_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;
|
||||
@ -84,40 +141,91 @@ export class GigCrew {
|
||||
return { actor, kind };
|
||||
}
|
||||
|
||||
// spawn(roomGroup, { stage, watchPoints, gig }) — band trio on the deck + crowd at the watch points.
|
||||
// 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 } = {}) {
|
||||
this.disposeAll();
|
||||
const gid = gig.gigId | 0;
|
||||
this._gen++;
|
||||
const gen = this._gen;
|
||||
|
||||
// ── band: 3 performers on the deck (C's bandPoses: guitar / vocal / bass) ──
|
||||
// ── band: C's bandPoses (guitar / vocal / bass across the front, + a seated `drums` on the riser) ──
|
||||
if (stage && stage.bandPoses) {
|
||||
// distinct fleet peds for the trio, seeded per gig (a band with two identical members reads wrong)
|
||||
// distinct fleet peds per gig (a band with two identical members reads wrong)
|
||||
const distinct = (this.fleet && this.fleet.ready)
|
||||
? shuffle(rng(this.citySeed, 'gigband', gid), this.fleet.all.map((_, i) => i)) : [];
|
||||
stage.bandPoses.forEach((p, i) => {
|
||||
const ry = p.ry + RY_FLIP;
|
||||
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}`,
|
||||
{ x: p.x, y: stage.deckY || 0, z: p.z, ry, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
|
||||
{ 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 extra = [];
|
||||
const member = { actor, kind, 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); extra.push(inst); }
|
||||
this.members.push({ actor, kind, part: 'band', role: p.role, dance: false,
|
||||
base: { x: p.x, y: stage.deckY || 0, z: p.z, ry }, phase: rng(this.citySeed, 'gigp', `b${gid}:${i}`)() * Math.PI * 2, extra });
|
||||
if (inst) { actor.fig.add(inst); member.extra.push(inst); }
|
||||
this.members.push(member);
|
||||
this._upgradeInstrument(member, gen); // async GLB swap over the primitive (fleet-gated)
|
||||
});
|
||||
this._addBackline(roomGroup, stage, gen); // amp dressing beside the riser (async, dressing)
|
||||
}
|
||||
|
||||
// ── crowd: stand-and-watch or dance at C's watch points ──
|
||||
const pts = watchPoints.slice(0, CROWD_CAP);
|
||||
const pts = watchPoints.slice(0, Math.min(CROWD_CAP, watchPoints.length));
|
||||
pts.forEach((w) => {
|
||||
const ry = w.ry + RY_FLIP;
|
||||
const { actor, kind } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, { x: w.x, y: 0, z: w.z, ry });
|
||||
this.members.push({ actor, kind, part: 'crowd', role: 'fan', dance: !!w.dance,
|
||||
base: { x: w.x, y: 0, z: w.z, ry }, phase: rng(this.citySeed, 'gigp', `c${gid}:${w.slotIndex}`)() * Math.PI * 2, extra: [] });
|
||||
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,
|
||||
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 };
|
||||
}
|
||||
|
||||
// Swap a band member's primitive instrument for E's manifest GLB, placeholder-persists style. Fail-soft
|
||||
// at every step (no manifest / no entry / depot unreachable → the primitive stays), and generation-guarded
|
||||
// so a GLB that resolves after the room is gone (or re-spawned) is dropped, never attached to a dead rig.
|
||||
_upgradeInstrument(member, gen) {
|
||||
if (!this.fleet || !this.fleet.ready) return; // ?noassets → primitive only, no fetch
|
||||
const cfg = GLB_FIT[member.role];
|
||||
if (!cfg) return;
|
||||
loadManifest().then((mani) => {
|
||||
if (!mani || !mani.fittings || this._gen !== gen) return;
|
||||
const entry = mani.fittings[cfg.id];
|
||||
if (!entry || !entry.file) return;
|
||||
return loadGLB(`depot:${entry.file}`).then((gltf) => {
|
||||
if (!gltf || this._gen !== gen || !member.actor.fig.parent) return; // room rebuilt / disposed mid-flight
|
||||
const inst = gltf.scene.clone(true); // shares cached geo/mats — detach only, never dispose
|
||||
inst.position.set(...cfg.pos);
|
||||
inst.rotation.set(...cfg.rot);
|
||||
inst.scale.setScalar(cfg.scale || 1);
|
||||
inst.traverse((o) => { if (o.isMesh) { o.castShadow = false; o.frustumCulled = false; } });
|
||||
for (const ex of member.extra) { if (ex.parent) ex.parent.remove(ex); disposePrim(ex); }
|
||||
member.extra.length = 0;
|
||||
member.actor.fig.add(inst);
|
||||
member.extra.push(inst);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// A guitar amp as backline dressing, planted on the deck just off the riser (the classic pub backline).
|
||||
// Dressing, not an instrument — planted in the room group (this.props), GLB-only (skipped under ?noassets).
|
||||
_addBackline(roomGroup, stage, gen) {
|
||||
if (!this.fleet || !this.fleet.ready) return;
|
||||
loadManifest().then((mani) => {
|
||||
if (!mani || !mani.fittings || this._gen !== gen) return;
|
||||
const entry = mani.fittings.guitar_amp;
|
||||
if (!entry || !entry.file) return;
|
||||
return loadGLB(`depot:${entry.file}`).then((gltf) => {
|
||||
if (!gltf || this._gen !== gen) return;
|
||||
const amp = gltf.scene.clone(true);
|
||||
amp.traverse((o) => { if (o.isMesh) { o.castShadow = false; o.frustumCulled = false; } });
|
||||
// off to stage-right of the drum riser, on the deck, facing the crowd (world +Z). Room-local coords.
|
||||
amp.position.set(stage.x + (stage.w || 3) * 0.32, (stage.deckY || 0), stage.z + 0.15);
|
||||
roomGroup.add(amp);
|
||||
this.props.push(amp);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// procedural motion layered on the idle clip (seeded phase → the crowd never metronomes in sync).
|
||||
update(dt) {
|
||||
this.t += dt;
|
||||
@ -127,7 +235,10 @@ export class GigCrew {
|
||||
else m.actor.tick && m.actor.tick(dt, false); // placeholder idle
|
||||
const f = m.actor.fig, b = m.base, ph = m.phase;
|
||||
if (m.part === 'band') {
|
||||
if (m.role === 'vocal') { // singer: gentle sway toward the mic
|
||||
if (m.seated) { // drummer: seated bob + a little shoulder work, planted
|
||||
f.position.y = b.y + Math.abs(Math.sin(t * 3.4 + ph)) * 0.03;
|
||||
f.rotation.z = Math.sin(t * 3.4 + ph) * 0.04;
|
||||
} else if (m.role === 'vocal') { // singer: gentle sway toward the mic
|
||||
f.rotation.z = Math.sin(t * 2.0 + ph) * 0.035;
|
||||
f.position.z = b.z + Math.sin(t * 1.3 + ph) * 0.02;
|
||||
} else { // guitar / bass: rhythmic strum-sway + tiny bob
|
||||
@ -149,10 +260,21 @@ export class GigCrew {
|
||||
disposeAll() {
|
||||
for (const m of this.members) {
|
||||
if (m.actor.fig.parent) m.actor.fig.parent.remove(m.actor.fig);
|
||||
for (const ex of m.extra) ex.traverse(o => { if (o.isMesh) { o.geometry.dispose(); o.material.dispose && o.material.dispose(); } });
|
||||
for (const ex of m.extra) disposePrim(ex); // GLB clones (shared geo/mats) are skipped inside
|
||||
m.actor.dispose && m.actor.dispose();
|
||||
}
|
||||
for (const p of this.props) { if (p.parent) p.parent.remove(p); } // GLB dressing — cached geo/mats, detach only
|
||||
this.members.length = 0;
|
||||
this.props.length = 0;
|
||||
this._gen++; // invalidate any GLB load still in flight
|
||||
this.t = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Free a primitive instrument's own geometry/materials. GLB clones share the cached gltf's geo/mats,
|
||||
// so they are detached from the scene by disposeAll but NEVER disposed here (tagged: no ownGeo).
|
||||
function disposePrim(obj) {
|
||||
if (obj.parent) obj.parent.remove(obj);
|
||||
if (!obj.userData || !obj.userData.ownGeo) return; // GLB clone → shared resources, leave them cached
|
||||
obj.traverse((o) => { if (o.isMesh) { o.geometry.dispose(); o.material.dispose && o.material.dispose(); } });
|
||||
}
|
||||
|
||||
158
web/js/citizens/queue.js
Normal file
158
web/js/citizens/queue.js
Normal file
@ -0,0 +1,158 @@
|
||||
// PROCITY Lane D — the outdoor gig queue (v3.0-beta charter item, behind ?gigs=1).
|
||||
//
|
||||
// While the doors are open, a short line of punters waits at the venue frontage and drains into the
|
||||
// room as people are admitted — gone by closing (`done`). Street-side companion to the interior
|
||||
// GigCrew: same gate-protected rig path (spawnRig → buildFigure, so no giants; rigs face -Z after the
|
||||
// R13 facing-normalise), same placeholder fallback under ?noassets (a queue of boxes is still a queue).
|
||||
//
|
||||
// Seam (Lane F wires it from the street-side gig state, per venue):
|
||||
// const q = new VenueQueue({ citySeed: plan.citySeed, fleet });
|
||||
// q.spawn(streetGroup, { door, queueZone, gigId }); // at 'doors' (and 'on' overflow)
|
||||
// q.admitOne(); // when a punter is admitted (F's cover-charge / entry hook)
|
||||
// q.update(dt); // each street frame — idle shuffle + lerp + slow auto-drain
|
||||
// q.disposeAll(); // at 'done' / venue out of view
|
||||
//
|
||||
// Geometry contract (consistent with the post-R13 -Z rig front):
|
||||
// door = { x, z, ry } ry = the heading a punter at the door FACES to look at the entrance.
|
||||
// queueZone = { x, z, ry, len } | null Lane B's marker: (x,z) = head by the door, ry = that same facing,
|
||||
// len = line length (m). Absent → a straight line trailing the door.
|
||||
// The line trails BEHIND the head (opposite the facing), punters spaced along it, everyone facing the door.
|
||||
|
||||
import * as THREE from 'three';
|
||||
import { rng } from '../core/prng.js';
|
||||
import { pickRig, spawnRig } from './rigs.js';
|
||||
import { makePlaceholder } from './placeholder.js';
|
||||
|
||||
const SPACING = 0.85; // m between punters down the line
|
||||
const QUEUE_MIN = 2, QUEUE_MAX = 6;
|
||||
const DRAIN_MIN = 3.5, DRAIN_MAX = 7.0; // s between auto-admissions (seeded) — the safety drain if F
|
||||
// doesn't couple admitOne() to real entries; empties over the night.
|
||||
const STEP_EASE = 4.0; // how fast a punter slides up into a vacated slot
|
||||
|
||||
export class VenueQueue {
|
||||
constructor({ citySeed = 20261990, fleet = null } = {}) {
|
||||
this.citySeed = citySeed >>> 0;
|
||||
this.fleet = fleet;
|
||||
this.members = []; // { actor, kind, slot, target:{x,z}, base:{ry}, phase }
|
||||
this.parent = null;
|
||||
this.gid = 0;
|
||||
this._gen = 0;
|
||||
this.t = 0;
|
||||
this._drainAt = 0;
|
||||
}
|
||||
|
||||
// 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 } = {}) {
|
||||
this.disposeAll();
|
||||
this._gen++;
|
||||
this.gid = gigId | 0;
|
||||
this.parent = parent;
|
||||
const anchor = this._anchor(door, queueZone);
|
||||
if (!anchor) return { queue: 0 };
|
||||
this.anchor = anchor;
|
||||
const r = rng(this.citySeed, 'queuecount', this.gid);
|
||||
const n = count != null ? Math.max(0, count | 0) : QUEUE_MIN + Math.floor(r() * (QUEUE_MAX - QUEUE_MIN + 1));
|
||||
for (let i = 0; i < n; i++) this._spawnAt(i);
|
||||
this._drainAt = this.t + this._nextDrainGap();
|
||||
return { queue: this.members.length };
|
||||
}
|
||||
|
||||
// Resolve the head + facing + trail direction from B's zone, else a straight line off the door.
|
||||
// Punters always face `ry` (toward the entrance; post-R13 rigs face world (-sin ry, -cos ry)). B's
|
||||
// zone trails single-file the opposite way (B lays it down the verandah via x/z/ry/len). The door-only
|
||||
// fallback instead trails along the frontage TANGENT, so the line hugs the building rather than spilling
|
||||
// into the road — the "along the frontage" read, best-effort until B's queueZone lands.
|
||||
_anchor(door, queueZone) {
|
||||
const src = queueZone || door;
|
||||
if (!src || src.x == null) return null;
|
||||
const ry = src.ry || 0;
|
||||
const trail = queueZone
|
||||
? { x: Math.sin(ry), z: Math.cos(ry) } // opposite the facing → classic single file
|
||||
: { x: Math.cos(ry), z: -Math.sin(ry) }; // perpendicular to facing → along the frontage
|
||||
return { x: src.x, z: src.z, ry, trail, len: queueZone && queueZone.len ? queueZone.len : null };
|
||||
}
|
||||
|
||||
_spawnAt(slot) {
|
||||
const a = this.anchor;
|
||||
// even spacing; if B gave a finite zone length, don't overrun it (compress to fit)
|
||||
const gap = (a.len && this.members ? Math.min(SPACING, a.len / Math.max(1, QUEUE_MAX - 1)) : SPACING);
|
||||
const tx = a.x + a.trail.x * gap * slot;
|
||||
const tz = a.z + a.trail.z * gap * slot;
|
||||
const key = `${this.gid}:${slot}`;
|
||||
const r = rng(this.citySeed, 'queueh', key);
|
||||
const h = 1.58 + r() * 0.34;
|
||||
let actor, kind;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = pickRig(this.fleet, r());
|
||||
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'; }
|
||||
}
|
||||
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 });
|
||||
}
|
||||
|
||||
_gap() {
|
||||
const a = this.anchor;
|
||||
return (a && a.len ? Math.min(SPACING, a.len / Math.max(1, QUEUE_MAX - 1)) : SPACING);
|
||||
}
|
||||
_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).
|
||||
admitOne() {
|
||||
if (!this.members.length) return 0;
|
||||
// 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();
|
||||
// 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) {
|
||||
m.slot = Math.max(0, m.slot - 1);
|
||||
m.target.x = a.x + a.trail.x * gap * m.slot;
|
||||
m.target.z = a.z + a.trail.z * gap * m.slot;
|
||||
}
|
||||
this._drainAt = this.t + this._nextDrainGap();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// idle shuffle + slide up into vacated slots. The seeded auto-drain is a safety so the line empties
|
||||
// over the night even if F doesn't couple admitOne() to real entries; call disposeAll() at `done`.
|
||||
update(dt, { autoDrain = true } = {}) {
|
||||
this.t += dt;
|
||||
const t = this.t;
|
||||
if (autoDrain && this.members.length && t >= this._drainAt) this.admitOne();
|
||||
for (const m of this.members) {
|
||||
if (m.actor.mixer) m.actor.mixer.update(dt);
|
||||
else m.actor.tick && m.actor.tick(dt, false);
|
||||
const f = m.actor.fig, ph = m.phase;
|
||||
// ease toward the (possibly just-vacated) slot
|
||||
f.position.x += (m.target.x - f.position.x) * Math.min(1, dt * STEP_EASE);
|
||||
f.position.z += (m.target.z - f.position.z) * Math.min(1, dt * STEP_EASE);
|
||||
const moving = Math.hypot(m.target.x - f.position.x, m.target.z - f.position.z) > 0.03;
|
||||
if (m.actor.setMoving) m.actor.setMoving(moving);
|
||||
// waiting-punter fidget: slow weight shift + the odd glance, feet planted
|
||||
f.rotation.z = Math.sin(t * 0.8 + ph) * 0.02;
|
||||
f.rotation.y = m.base.ry + Math.sin(t * 0.4 + ph) * 0.05;
|
||||
}
|
||||
return this.members.length;
|
||||
}
|
||||
|
||||
count() { return this.members.length; }
|
||||
|
||||
disposeAll() {
|
||||
for (const m of this.members) {
|
||||
if (m.actor.fig.parent) m.actor.fig.parent.remove(m.actor.fig);
|
||||
m.actor.dispose && m.actor.dispose();
|
||||
}
|
||||
this.members.length = 0;
|
||||
this._gen++;
|
||||
this.t = 0;
|
||||
}
|
||||
}
|
||||
@ -100,6 +100,17 @@ const _bs = new THREE.Vector3();
|
||||
export function buildFigure(rig, height = 1.75) {
|
||||
const inner = skeletonClone(rig.scene);
|
||||
inner.traverse(o => { if (o.isMesh) { o.frustumCulled = false; o.castShadow = false; } });
|
||||
// FACING NORMALISE (R13 debt #4 — the RY audit). The imported GLB fleet's visual front is local +Z,
|
||||
// but EVERYTHING downstream assumes -Z: the sim's heading math (sim.js atan2(-dx,-dz)), C's pose ry
|
||||
// (counter.stand / bandPoses / watchPoints / browse points), the placeholder (toes point -Z on
|
||||
// purpose, placeholder.js:61), and the impostor bake/pick (impostor.js -Z convention). So the GLB was
|
||||
// the lone outlier: keepers faced the back wall, browsers faced away, streamed walkers moon-walked
|
||||
// (anti-travel), and band.js carried a local RY_FLIP=π to patch just the gig. Rotate the rig's own
|
||||
// mesh 180° here so its front becomes -Z like the rest of the system — one place, everyone correct,
|
||||
// near↔mid stays seamless (the impostor bakes this same figure), and the flip is purely visual (a
|
||||
// Y-rotation leaves every bone's world-Y untouched → head-height/feet-plant + the no-giants gate and
|
||||
// all seeded identity/determinism are byte-unchanged). See LANE_D_NOTES R13 + LANE_C_PUB.md pose contract.
|
||||
inner.rotation.y = Math.PI;
|
||||
const fig = new THREE.Group();
|
||||
fig.add(inner);
|
||||
fig.updateWorldMatrix(true, true);
|
||||
|
||||
@ -123,7 +123,7 @@ export class CitizenSim {
|
||||
// R8 shop patronage (default-on for the streamed roster; ?patronage=0 off). No-op until setShops.
|
||||
this.shopsByChunk = null; // chunkKey → [{ x, z, hours:[open,close], shopId }] door points
|
||||
this.patronageOn = true;
|
||||
this._gigVenue = null; // R12: venueShopId while a gig is on (F sets via setGig) → surge
|
||||
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._dropKeys = []; // scratch: chunks to drop this frame
|
||||
@ -333,10 +333,15 @@ export class CitizenSim {
|
||||
// map: chunkKey → [{ x, z, hours:[open,close] }]. Without it, patronage is inert.
|
||||
setShops(shopsByChunk) { this.shopsByChunk = shopsByChunk; }
|
||||
setPatronage(on) { this.patronageOn = !!on; }
|
||||
// R12 gig-night surge (?gigs=1): while a gig is on, F points patronage at the venue so the street
|
||||
// Gig-night surge (?gigs=1): while a gig is on, F points patronage at the venue so the street
|
||||
// converges on the pub — it pulls from further (GIG_RANGE) and ducks in harder (GIG_SURGE). Occupants
|
||||
// drain at close per A's closing-time ruling (no special handling — _openAt + the dwell timer do it).
|
||||
setGig(venueShopId, on = true) { this._gigVenue = on && venueShopId != null ? venueShopId : null; }
|
||||
// R13 the district: per-venue, so several concurrent gigs each pull their own block. F calls this once
|
||||
// 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);
|
||||
}
|
||||
setWeather(w) { if (w && typeof w.state === 'string') this.weather = { state: w.state, intensity: +w.intensity || 0 }; }
|
||||
|
||||
_openAt(hours) { if (!hours) return true; const h = (this.timeOfDay % 1) * 24; return h >= hours[0] && h < hours[1]; }
|
||||
@ -365,14 +370,14 @@ export class CitizenSim {
|
||||
const list = this.shopsByChunk.get(this.chunkKeyAt(c.x, c.z));
|
||||
if (!list || !list.length) return null;
|
||||
let best = null, bd = PATRON_RANGE;
|
||||
for (const s of list) {
|
||||
let gigBest = null, gbd = GIG_RANGE; // R13: the nearest gig venue in range — it wins over ordinary
|
||||
for (const s of list) { // shops (follow the sound), and across concurrent venues.
|
||||
if (!this._openAt(s.hours)) continue;
|
||||
const d = Math.hypot(s.x - c.x, s.z - c.z);
|
||||
// gig night: the venue reaches further and wins over a nearer shop (follow the sound to the pub)
|
||||
if (this._gigVenue != null && s.shopId === this._gigVenue && d < GIG_RANGE) return s;
|
||||
if (this._gigVenues.has(s.shopId) && d < gbd) { gbd = d; gigBest = s; }
|
||||
if (d < bd) { bd = d; best = s; }
|
||||
}
|
||||
return best;
|
||||
return gigBest || best;
|
||||
}
|
||||
_beginVisit(c, shop) {
|
||||
c._savedWalk = { edge: c.edge, forward: c.forward, s: c.s }; // resume the footpath walk on the way out
|
||||
@ -519,7 +524,7 @@ export class CitizenSim {
|
||||
if (c._patronDist >= PATRON_STRIDE) {
|
||||
c._patronDist = 0;
|
||||
const shop = this._nearestOpenShop(c);
|
||||
const chance = (shop && this._gigVenue != null && shop.shopId === this._gigVenue)
|
||||
const chance = (shop && this._gigVenues.has(shop.shopId))
|
||||
? Math.max(this._patronChance(), GIG_SURGE) : this._patronChance();
|
||||
if (shop && c.patronRng() < chance) this._beginVisit(c, shop);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user