diff --git a/web/index.html b/web/index.html
index c19bbb5..1894d4a 100644
--- a/web/index.html
+++ b/web/index.html
@@ -187,7 +187,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/', { sit: !CLASSIC, look: !CLASSIC }); // [R16] sit.glb (the seated drummer) + [R29] look.glb (the street glance) on the default boot; ?classic keeps the zero-fetch-delta covenant
+const fleet = NOASSETS ? null : loadPedFleet('models/peds/', { sit: !CLASSIC, look: !CLASSIC, dance: !CLASSIC }); // [R16] sit.glb (the seated drummer) + [R29] look.glb (the street glance) on the default boot; ?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 R31 — THE DIG FLIP] The crate-riffle is DEFAULT-ON: `?dig=0` opts out, `?classic=1` forces it off
diff --git a/web/js/citizens/band.js b/web/js/citizens/band.js
index 0182f33..4dba040 100644
--- a/web/js/citizens/band.js
+++ b/web/js/citizens/band.js
@@ -138,7 +138,7 @@ export class GigCrew {
// one seeded rig (or placeholder) actor, planted + facing ry. `key` seeds identity per gig+slot.
// `seated` (R16): the drummer — use E's sit.glb pose (fleet.sitClip) + spawnRig's foot-replant + tag the
// fig for F's stature gate; falls back to the standing idle (caller sinks it via SEAT_DROP) if no sitClip.
- _make(target, key, { x, y, z, ry, height, pedIndex = null, seated = false }) {
+ _make(target, key, { x, y, z, ry, height, pedIndex = null, seated = false, dance = false }) {
const r = rng(this.citySeed, 'gig', key);
const h = height != null ? height : 1.6 + r() * 0.3;
let actor, kind, usedIndex = null;
@@ -146,7 +146,10 @@ export class GigCrew {
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
const rig = pk && this.fleet.all[pk.index];
const useSit = seated && !!this.fleet.sitClip; // real seated pose iff E's sit clip is loaded
- const sp = rig && spawnRig(rig, { ry, height: h, clip: useSit ? this.fleet.sitClip : this.fleet.idleClip, phase: r(), seated: useSit });
+ // dancers play a real dance clip (separate 'gigdance' stream keeps the pick off the existing draws)
+ const dcs = this.fleet.danceClips || [];
+ const danceClip = (dance && dcs.length) ? dcs[(rng(this.citySeed, 'gigdance', key)() * dcs.length) | 0] : null;
+ const sp = rig && spawnRig(rig, { ry, height: h, clip: useSit ? this.fleet.sitClip : (danceClip || this.fleet.idleClip), phase: r(), seated: useSit });
if (sp) { actor = sp; kind = 'rig'; usedIndex = pk.index; if (useSit) sp.fig.userData.procitySeated = true; }
}
if (!actor) { actor = makePlaceholder(rng(this.citySeed, 'gig-body', key), { height: h }); actor.fig.rotation.y = ry; kind = 'placeholder'; }
@@ -200,8 +203,8 @@ export class GigCrew {
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 });
+ who ? { x: w.x, y: 0, z: w.z, ry: w.ry, pedIndex: who.pedIndex, height: who.height, dance: !!w.dance }
+ : { x: w.x, y: 0, z: w.z, ry: w.ry, dance: !!w.dance });
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: [] });
@@ -284,10 +287,14 @@ export class GigCrew {
f.rotation.z = Math.sin(t * 3.1 + ph) * 0.055;
f.position.y = b.y + Math.abs(Math.sin(t * 3.1 + ph)) * 0.02;
}
- } else if (m.dance) { // dancer: bounce + sway + a little step
- f.position.y = b.y + Math.abs(Math.sin(t * 5.2 + ph)) * 0.075;
- f.rotation.z = Math.sin(t * 2.6 + ph) * 0.07;
- f.rotation.y = b.ry + Math.sin(t * 1.7 + ph) * 0.13;
+ } else if (m.dance) { // dancer
+ if (this.fleet && this.fleet.danceClips && this.fleet.danceClips.length) {
+ f.rotation.y = b.ry + Math.sin(t * 0.6 + ph) * 0.10; // real dance clip drives the body; just a slow facing drift
+ } else { // fallback (classic/noassets): procedural bounce + sway
+ f.position.y = b.y + Math.abs(Math.sin(t * 5.2 + ph)) * 0.075;
+ f.rotation.z = Math.sin(t * 2.6 + ph) * 0.07;
+ f.rotation.y = b.ry + Math.sin(t * 1.7 + ph) * 0.13;
+ }
} else { // stander: slow weight shift, feet planted
f.rotation.z = Math.sin(t * 0.9 + ph) * 0.022;
f.rotation.y = b.ry + Math.sin(t * 0.5 + ph) * 0.03;
diff --git a/web/js/citizens/rigs.js b/web/js/citizens/rigs.js
index c685bb1..c13dba7 100644
--- a/web/js/citizens/rigs.js
+++ b/web/js/citizens/rigs.js
@@ -56,10 +56,10 @@ function loadRig(ref) {
// sit.glb fetch would break the ?classic=1 zero-fetch-delta covenant — F passes sit:true on the gig/default
// boot, sit:false (default) under ?classic. Absent/off → fleet.sitClip stays null and the drummer falls
// back to the standing-sunk hack, so this never breaks a boot. Does NOT touch fleet.ready / determinism.
-export function loadPedFleet(base = 'models/peds/', { sit = false, look = false } = {}) {
+export function loadPedFleet(base = 'models/peds/', { sit = false, look = false, dance = false } = {}) {
const fleet = {
normal: [], comical: [], all: [], // all = normal ++ comical, stable index for the impostor atlas
- walkClip: null, idleClip: null, sitClip: null, lookClip: null,
+ walkClip: null, idleClip: null, sitClip: null, lookClip: null, danceClips: [],
ready: false, whenReady: null,
};
// DETERMINISM: fill fixed slots by PED_NAMES index, NOT push-on-resolve — otherwise the array
@@ -90,10 +90,19 @@ export function loadPedFleet(base = 'models/peds/', { sit = false, look = false
if (look) jobs.push(loadRig(`${base}look.glb`).then(r => {
const c = r && r.anims && r.anims[0];
if (c) { c.tracks.forEach(t => t.name = _canon(t.name)); fleet.lookClip = _rotOnly(c); } }));
+ // dance clips (mesh-free mixamorig): the club dancers play a REAL move instead of procedural sway.
+ // GATED like sit/look — ?classic fetches nothing so the zero-fetch-delta covenant holds. The per-dancer
+ // pick is deterministic (a separate seeded stream in _make), so 'same seed -> same crowd' is preserved.
+ const DANCE_CLIPS = ['dance_party', 'dance_medium', 'dance_drink', 'dance_sway'];
+ const danceSlots = new Array(DANCE_CLIPS.length).fill(null);
+ if (dance) DANCE_CLIPS.forEach((n, i) => jobs.push(loadRig(`${base}${n}.glb`).then(r => {
+ const c = r && r.anims && r.anims[0];
+ if (c) { c.tracks.forEach(t => t.name = _canon(t.name)); danceSlots[i] = _rotOnly(c); } })));
fleet.whenReady = Promise.all(jobs).then(() => {
fleet.normal = nSlots.filter(Boolean);
fleet.comical = cSlots.filter(Boolean);
fleet.all = [...fleet.normal, ...fleet.comical];
+ fleet.danceClips = danceSlots.filter(Boolean);
fleet.ready = fleet.all.length > 0 && !!fleet.walkClip;
return fleet;
});
diff --git a/web/models/peds/dance_drink.glb b/web/models/peds/dance_drink.glb
new file mode 100644
index 0000000..5eac524
Binary files /dev/null and b/web/models/peds/dance_drink.glb differ
diff --git a/web/models/peds/dance_medium.glb b/web/models/peds/dance_medium.glb
new file mode 100644
index 0000000..73d149f
Binary files /dev/null and b/web/models/peds/dance_medium.glb differ
diff --git a/web/models/peds/dance_party.glb b/web/models/peds/dance_party.glb
new file mode 100644
index 0000000..be14f94
Binary files /dev/null and b/web/models/peds/dance_party.glb differ
diff --git a/web/models/peds/dance_sway.glb b/web/models/peds/dance_sway.glb
new file mode 100644
index 0000000..4a7851e
Binary files /dev/null and b/web/models/peds/dance_sway.glb differ