Compare commits

...

2 Commits

Author SHA1 Message Date
m3ultra
046668e5af 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>
2026-07-20 02:11:55 +10:00
m3ultra
b3394027be feat(citizens): +5 trellis DJ bodies in the ped roster
Same 5 Mixamo-rigged DJ characters as 90sDJsim, converted FBX->skinned GLB +
decimated to 4k tris (<=5k ped law) via MESHGOD scripts/fbx_to_ped.py — mixamorig:
skeleton + skin preserved. Added to PED_NAMES (rigs.js). Shares the proven bind:
identical _canon/_rotOnly + the same GLBs verified animating in 90sDJsim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 01:37:10 +10:00
12 changed files with 29 additions and 12 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.
// • 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

View File

@ -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;

View File

@ -18,7 +18,8 @@ export const PED_NAMES = {
normal: ['man_worker_hivis_01', 'man_suit_01', 'man_casual_01', 'man_casual_02', 'man_casual_03',
'man_grunge_01', 'man_elder_01', 'man_athletic_01', 'man_bouncer_01', 'man_soldier_ww2_01',
'man_dj_streetwear_01', 'woman_casual_01', 'woman_casual_02', 'woman_business_01',
'woman_athleisure_01', 'woman_smart_01', 'woman_chef_01'],
'woman_athleisure_01', 'woman_smart_01', 'woman_chef_01',
'woman_dj_01', 'woman_dj_02', 'woman_dj_03', 'dj_techno_01', 'dj_phrtt_01'], // trellis'd DJ bodies (fbx_to_ped, 4k tris)
comical: ['comical_luchador_01', 'comical_boy_01'],
};
@ -55,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
@ -89,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;
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.