feat(gig): club dancers play real dance clips, not procedural sway

loadPedFleet gains dance:!CLASSIC (mirrors the sit/look gate) → fleet.danceClips
= 4 mesh-free mixamorig club dances (party/medium/drink/sway). _make picks one
per dancer off a fresh gigdance seeded stream (zero impact on existing draws /
the same-seed-same-crowd covenant), spawnRig plays it, and update() drops the
fake bounce for real-clip dancers (procedural sway stays as the classic/noassets
fallback). ?classic fetches nothing, byte-identical.

Verified: default boot fetches all 4 dance GLBs (gated load runs), both modules
parse + export clean, page boots. Same clips + _canon/_rotOnly/spawnRig proven
animating in 90sDJsim (its crowd/rigs.js is this file ported).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
m3ultra 2026-07-20 02:11:55 +10:00
parent b3394027be
commit 046668e5af
7 changed files with 27 additions and 11 deletions

View File

@ -187,7 +187,7 @@ const minimap = createMinimap(plan);
// unless) a GLB lands, so the town is fully playable the instant it boots. // 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. // • 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). // • 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 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 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 // [Lane F R31 — THE DIG FLIP] The crate-riffle is DEFAULT-ON: `?dig=0` opts out, `?classic=1` forces it off

View File

@ -138,7 +138,7 @@ export class GigCrew {
// one seeded rig (or placeholder) actor, planted + facing ry. `key` seeds identity per gig+slot. // 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 // `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. // 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 r = rng(this.citySeed, 'gig', key);
const h = height != null ? height : 1.6 + r() * 0.3; const h = height != null ? height : 1.6 + r() * 0.3;
let actor, kind, usedIndex = null; 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 pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
const rig = pk && this.fleet.all[pk.index]; 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 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 (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'; } 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) => { pts.forEach((w, i) => {
const who = roster[i] || null; const who = roster[i] || null;
const { actor, kind, pedIndex } = this._make(roomGroup, `crowd:${gid}:${w.slotIndex}`, 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 } 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 }); : { x: w.x, y: 0, z: w.z, ry: w.ry, dance: !!w.dance });
if (who) fromRoster++; if (who) fromRoster++;
this.members.push({ actor, kind, pedIndex, part: 'crowd', role: 'fan', dance: !!w.dance, seated: false, fromRoster: !!who, 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: [] }); 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.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; 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 } else if (m.dance) { // dancer
f.position.y = b.y + Math.abs(Math.sin(t * 5.2 + ph)) * 0.075; if (this.fleet && this.fleet.danceClips && this.fleet.danceClips.length) {
f.rotation.z = Math.sin(t * 2.6 + ph) * 0.07; f.rotation.y = b.ry + Math.sin(t * 0.6 + ph) * 0.10; // real dance clip drives the body; just a slow facing drift
f.rotation.y = b.ry + Math.sin(t * 1.7 + ph) * 0.13; } 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 } else { // stander: slow weight shift, feet planted
f.rotation.z = Math.sin(t * 0.9 + ph) * 0.022; f.rotation.z = Math.sin(t * 0.9 + ph) * 0.022;
f.rotation.y = b.ry + Math.sin(t * 0.5 + ph) * 0.03; f.rotation.y = b.ry + Math.sin(t * 0.5 + ph) * 0.03;

View File

@ -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 // 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 // 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. // 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 = { const fleet = {
normal: [], comical: [], all: [], // all = normal ++ comical, stable index for the impostor atlas 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, ready: false, whenReady: null,
}; };
// DETERMINISM: fill fixed slots by PED_NAMES index, NOT push-on-resolve — otherwise the array // 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 => { if (look) jobs.push(loadRig(`${base}look.glb`).then(r => {
const c = r && r.anims && r.anims[0]; const c = r && r.anims && r.anims[0];
if (c) { c.tracks.forEach(t => t.name = _canon(t.name)); fleet.lookClip = _rotOnly(c); } })); 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.whenReady = Promise.all(jobs).then(() => {
fleet.normal = nSlots.filter(Boolean); fleet.normal = nSlots.filter(Boolean);
fleet.comical = cSlots.filter(Boolean); fleet.comical = cSlots.filter(Boolean);
fleet.all = [...fleet.normal, ...fleet.comical]; fleet.all = [...fleet.normal, ...fleet.comical];
fleet.danceClips = danceSlots.filter(Boolean);
fleet.ready = fleet.all.length > 0 && !!fleet.walkClip; fleet.ready = fleet.all.length > 0 && !!fleet.walkClip;
return fleet; return fleet;
}); });

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.