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