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:
parent
b3394027be
commit
046668e5af
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
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.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.z = Math.sin(t * 2.6 + ph) * 0.07;
|
||||||
f.rotation.y = b.ry + Math.sin(t * 1.7 + ph) * 0.13;
|
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;
|
||||||
|
|||||||
@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
BIN
web/models/peds/dance_drink.glb
Normal file
BIN
web/models/peds/dance_drink.glb
Normal file
Binary file not shown.
BIN
web/models/peds/dance_medium.glb
Normal file
BIN
web/models/peds/dance_medium.glb
Normal file
Binary file not shown.
BIN
web/models/peds/dance_party.glb
Normal file
BIN
web/models/peds/dance_party.glb
Normal file
Binary file not shown.
BIN
web/models/peds/dance_sway.glb
Normal file
BIN
web/models/peds/dance_sway.glb
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user