Ledger #2 — drummer sits. E's sit.glb is wired into the shared rig stack: - FINDING: Fable's prescribed _rotWithHips (keep Hips.quaternion) MANGLES the pose in-shell (head folds to hip height / laid flat) — the fleet rigs' bind Hips orientation is incompatible with the Mixamo clip's, exactly what _rotOnly documents. So the sit clip rides _rotOnly (drops Hips.quaternion) like every clip; _rotWithHips removed. The seated read comes from the leg bends + a new spawnRig({seated}) foot-replant (opt-in; standing spawns skip it). - Drummer uses fleet.sitClip when present (retires SEAT_DROP), tags fig.userData.procitySeated=true (F's gate). Stature ~1.1-1.3m in [0.9,1.5]; reads seated from the crowd POV (kit hides the legs). Graceful SEAT_DROP fallback when sitClip absent (?noassets / before F wires {sit}). - BYTE-IDENTICAL proof (load-bearing): with sitClip null AND loaded, keepers are standing rigs (stature 1.718, innerRotY pi, replanted:false) and walkers 188/188 face travel. Parallel/opt-in path; _rotOnly + makeActor untouched. - F seam: loadPedFleet(base, {sit}) — pass sit:<gig active> (false under ?classic → no sit.glb fetch, classic covenant intact). Default off, graceful. Ledger #5 — backline unification. _addBackline plants E's guitar_amp at C's stage.backline[0] (== D's R15 pose, 0.75m bass clearance) instead of a hardcoded pose. C dropped its primitive ampStacks, so D owns the backline: placeholder- persists (primitive amp → GLB swap) with the primitive as the ?noassets fallback. Verified: GLB swaps, primitive under noassets, no crash, 0 fetch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
b322c998db
commit
ab7a8d0edf
@ -6,6 +6,56 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
|
||||
|
||||
---
|
||||
|
||||
## ROUND 16 — v3.1 THE FLIP: the drummer finally sits + backline unification
|
||||
|
||||
*PROCITY-D, 2026-07-16. Ledger #2 (sit-clip wiring) + #5 (amp at C's `backline[]`). Files `rigs.js`,
|
||||
`band.js`. Verified against C's landed `stage.backline[]` + E's `sit.glb`; F's flip/gate pending (I built to
|
||||
F's published seated-stature contract). **Byte-identical proof holds** (the load-bearing requirement).*
|
||||
|
||||
### The drummer sits (ledger #2) — and Fable's prescribed `_rotWithHips` is WRONG for these rigs
|
||||
The R14 recon + F's brief prescribed a `_rotWithHips` variant (keep `Hips.quaternion` to preserve the
|
||||
pelvic tilt). **In-shell it MANGLES the pose** — head folds to hip height, body laid flat — because the
|
||||
fleet rigs' bind-pose Hips orientation differs from the Mixamo clip's, exactly the failure `_rotOnly`'s own
|
||||
comment documents ("a different-orientation source lays it flat"). So the sit clip **drops `Hips.quaternion`
|
||||
like every other clip** (rides the existing `_rotOnly`); the seated read comes from the leg-bone bends +
|
||||
a new **seated foot-replant** in `spawnRig`. `_rotWithHips` is removed.
|
||||
- **`spawnRig({seated})`** (OPT-IN): after the clip plays, tick the mixer once and re-plant the POSED
|
||||
lowest bone to y=0 so the drummer's feet sit on the deck (recovers the hip descent). Standing spawns
|
||||
skip it entirely.
|
||||
- **The drummer**: uses `fleet.sitClip` when present (retiring the R14 `SEAT_DROP` sink — it sits on the
|
||||
riser and the re-plant seats it), tags `fig.userData.procitySeated = true` (F's gate). Reads seated from
|
||||
the **crowd POV** (the kit hides the lower body); measured stature **~1.1–1.3 m** (F's seated gate
|
||||
[0.9,1.5] ✓). Falls back to the `SEAT_DROP` standing-sunk hack if `sitClip` is absent (?noassets, or
|
||||
before F wires `{sit}`), so it never breaks a boot.
|
||||
- **Honest limit**: without the pelvic tilt (Hips.quaternion unusable), it's an upright bent-leg sit — a
|
||||
bit stiff from a hard side/overhead angle, hidden by the kit from the front. A cleaner pose is a manual
|
||||
pelvic-lean pass (v3.2). It's a genuine improvement over the standing-sunk hack (real bent legs, feet on
|
||||
the deck, passes the seated gate) — good enough for the flip.
|
||||
|
||||
### Byte-identical proof (keepers / browsers / walkers) — the load-bearing part
|
||||
The changes are a PARALLEL/opt-in path: `_rotOnly` untouched (walk/idle unchanged), `spawnRig`'s re-plant
|
||||
is `seated`-gated (default off), `fleet.sitClip` is additive, `makeActor` (walkers) untouched. Verified
|
||||
in-shell with `fleet.sitClip` **null AND loaded**: streamed walkers **188/188** face travel, keeper is a
|
||||
standing rig (stature **1.718**, `innerRotY:π`, `replanted:false` — the seated re-plant does NOT run). No
|
||||
existing consumer moves.
|
||||
|
||||
### → Lane F: the `{sit}` load seam (classic-safe by construction)
|
||||
`loadPedFleet(base, { sit })` — pass **`sit: <gig-layer active>`** (true on the default boot, **false under
|
||||
`?classic=1`**). Gated because a raw `sit.glb` fetch would break the classic zero-fetch-delta covenant;
|
||||
default is `sit:false`, and absent/off → `fleet.sitClip` null → the drummer falls back gracefully. My code
|
||||
is committed graceful, so it lands safely before F's commit; the drummer only sits once F passes `sit:true`
|
||||
(same commit as F's [0.9,1.5] seated-stature gate).
|
||||
|
||||
### Amp at C's `stage.backline[]` (ledger #5)
|
||||
`_addBackline` now plants at **C's `stage.backline[0]`** (`{x,z,ry,y}`, up-stage, clear of every bandPose
|
||||
+ watchPoint) instead of D's R15 hardcoded pose — C confirmed `backline[0]` == that pose, so nothing moves
|
||||
on screen (pub 1.44, 0.32, −3.27, ry=π; **0.75 m bass clearance**). Since C dropped its primitive
|
||||
ampStacks, D now owns the whole backline: **placeholder-persists** (a primitive amp shows immediately, the
|
||||
`guitar_amp` GLB swaps in), and the primitive is the **`?noassets` fallback** (fleet-gated, no fetch).
|
||||
Verified: GLB swaps in, primitive under `?noassets`, no crash, 0 fetch.
|
||||
|
||||
---
|
||||
|
||||
## ROUND 15 — v3.0 close: the amp one-liner (ledger #4)
|
||||
|
||||
*PROCITY-D, 2026-07-16. Sole task: move `band.js _addBackline` to C's §3 pose. Everything else (continuity,
|
||||
|
||||
@ -59,6 +59,17 @@ function micStandPrim() {
|
||||
g.position.set(0, 0, -0.26); // stands just in front of the singer (fig-local -Z)
|
||||
return tagOwn(g);
|
||||
}
|
||||
// R16: the backline amp fallback (C dropped its primitive ampStacks; this stands in when the guitar_amp
|
||||
// GLB is absent / ?noassets). A room prop (not fig-local): base at y=0, grille on the +Z (crowd) face.
|
||||
function ampPrim() {
|
||||
const g = new THREE.Group();
|
||||
const cab = new THREE.MeshStandardMaterial({ color: AMP, roughness: 0.7, metalness: 0.05 });
|
||||
const cloth = new THREE.MeshStandardMaterial({ color: AMPCLOTH, roughness: 0.9 });
|
||||
const box = new THREE.Mesh(new THREE.BoxGeometry(0.5, 0.6, 0.29), cab); box.position.y = 0.3;
|
||||
const grille = new THREE.Mesh(new THREE.BoxGeometry(0.42, 0.46, 0.02), cloth); grille.position.set(0, 0.3, 0.15);
|
||||
g.add(box, grille);
|
||||
return tagOwn(g);
|
||||
}
|
||||
// a cheap kit: kick drum + snare + a floor tom + two cymbals on stands. Reads as "drum kit" behind the
|
||||
// seated drummer; the GLB replaces it when present. Sits on the riser (y = SEAT_DROP compensates the sink).
|
||||
function drumKitPrim() {
|
||||
@ -125,15 +136,18 @@ export class GigCrew {
|
||||
}
|
||||
|
||||
// one seeded rig (or placeholder) actor, planted + facing ry. `key` seeds identity per gig+slot.
|
||||
_make(target, key, { x, y, z, ry, height, pedIndex = null }) {
|
||||
// `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 }) {
|
||||
const r = rng(this.citySeed, 'gig', key);
|
||||
const h = height != null ? height : 1.6 + r() * 0.3;
|
||||
let actor, kind, usedIndex = null;
|
||||
if (this.fleet && this.fleet.ready) {
|
||||
const pk = (pedIndex != null && this.fleet.all[pedIndex]) ? { index: pedIndex } : pickRig(this.fleet, r());
|
||||
const rig = pk && this.fleet.all[pk.index];
|
||||
const sp = rig && spawnRig(rig, { ry, height: h, clip: this.fleet.idleClip, phase: r() });
|
||||
if (sp) { actor = sp; kind = 'rig'; usedIndex = pk.index; } // usedIndex: the fleet ped shown — F's continuity smoke reads it
|
||||
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 });
|
||||
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'; }
|
||||
actor.fig.position.set(x, y, z);
|
||||
@ -158,9 +172,12 @@ export class GigCrew {
|
||||
stage.bandPoses.forEach((p, i) => {
|
||||
const seated = p.role === 'drums' || !!p.seated;
|
||||
const poseY = p.y != null ? p.y : (stage.deckY || 0); // C tags per-pose y (front line deckY, drums riser)
|
||||
const y = seated ? poseY - SEAT_DROP : poseY; // sink the drummer so the kit hides the legs
|
||||
// R16: a real sit clip seats the rig on the riser (spawnRig re-plants the feet) → no sink; only the
|
||||
// no-clip fallback still sinks SEAT_DROP so the kit hides the standing legs.
|
||||
const seatedRig = seated && !!(this.fleet && this.fleet.sitClip);
|
||||
const y = seated ? poseY - (seatedRig ? 0 : SEAT_DROP) : poseY;
|
||||
const { actor, kind, pedIndex } = this._make(roomGroup, `band:${gid}:${p.role}:${i}`,
|
||||
{ x: p.x, y, z: p.z, ry: p.ry, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
|
||||
{ x: p.x, y, z: p.z, ry: p.ry, seated, pedIndex: distinct.length ? distinct[i % distinct.length] : null,
|
||||
height: 1.68 + rng(this.citySeed, 'gigh', `${gid}:${i}`)() * 0.2 });
|
||||
const member = { actor, kind, pedIndex, part: 'band', role: p.role, dance: false, seated,
|
||||
base: { x: p.x, y, z: p.z, ry: p.ry }, phase: rng(this.citySeed, 'gigp', `b${gid}:${i}`)() * Math.PI * 2, extra: [] };
|
||||
@ -218,24 +235,31 @@ export class GigCrew {
|
||||
});
|
||||
}
|
||||
|
||||
// A guitar amp as backline dressing, planted on the deck just off the riser (the classic pub backline).
|
||||
// Dressing, not an instrument — planted in the room group (this.props), GLB-only (skipped under ?noassets).
|
||||
// The backline amp, planted at C's up-stage `stage.backline[0]` (R16 ledger #5 — C dropped its primitive
|
||||
// ampStacks; D owns the whole backline now). Placeholder-persists: a primitive amp shows immediately (the
|
||||
// ?noassets fallback too), then E's guitar_amp GLB swaps in when it resolves. Falls back to the R15
|
||||
// hardcoded up-stage-right pose if C's backline isn't present. Room-local coords; grille to the crowd.
|
||||
_addBackline(roomGroup, stage, gen) {
|
||||
if (!this.fleet || !this.fleet.ready) return;
|
||||
const slot = (stage.backline && stage.backline[0]) ||
|
||||
{ x: stage.x + (stage.w || 3) * 0.36, z: stage.z - (stage.d || 3) * 0.22, ry: 0 };
|
||||
const place = (obj) => {
|
||||
obj.position.set(slot.x, stage.deckY || 0, slot.z);
|
||||
if (slot.ry != null) obj.rotation.y = slot.ry;
|
||||
roomGroup.add(obj); this.props.push(obj); return obj;
|
||||
};
|
||||
const prim = place(ampPrim()); // primitive amp immediately (asset-free backline)
|
||||
if (!this.fleet || !this.fleet.ready) return; // ?noassets → primitive only, no fetch
|
||||
loadManifest().then((mani) => {
|
||||
if (!mani || !mani.fittings || this._gen !== gen) return;
|
||||
const entry = mani.fittings.guitar_amp;
|
||||
if (!entry || !entry.file) return;
|
||||
return loadGLB(`depot:${entry.file}`).then((gltf) => {
|
||||
if (!gltf || this._gen !== gen) return;
|
||||
if (!gltf || this._gen !== gen || !prim.parent) return; // room rebuilt / disposed mid-flight
|
||||
const amp = gltf.scene.clone(true);
|
||||
amp.traverse((o) => { if (o.isMesh) { o.castShadow = false; o.frustumCulled = false; } });
|
||||
// R15 (C's §3 ruling): up-stage-right behind the front line — clears the bass rig and offsets from
|
||||
// C's primitive ampStacks (at ±w·0.28, z−d·0.18), so GLB + primitive read as a flanking backline.
|
||||
// The old z+0.15 was down-stage and interpenetrated the bassist. Room-local coords; grille to the crowd.
|
||||
amp.position.set(stage.x + (stage.w || 3) * 0.36, (stage.deckY || 0), stage.z - (stage.d || 3) * 0.22);
|
||||
roomGroup.add(amp);
|
||||
this.props.push(amp);
|
||||
place(amp); // GLB swaps in over the primitive
|
||||
const i = this.props.indexOf(prim); if (i >= 0) this.props.splice(i, 1);
|
||||
disposePrim(prim); // free the primitive's own geo/mats
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -277,7 +301,7 @@ export class GigCrew {
|
||||
for (const ex of m.extra) disposePrim(ex); // GLB clones (shared geo/mats) are skipped inside
|
||||
m.actor.dispose && m.actor.dispose();
|
||||
}
|
||||
for (const p of this.props) { if (p.parent) p.parent.remove(p); } // GLB dressing — cached geo/mats, detach only
|
||||
for (const p of this.props) disposePrim(p); // GLB dressing → detach only; the ?noassets primitive amp → freed (ownGeo)
|
||||
this.members.length = 0;
|
||||
this.props.length = 0;
|
||||
this._gen++; // invalidate any GLB load still in flight
|
||||
|
||||
@ -36,6 +36,12 @@ function canonRig(r) {
|
||||
// flat). The character keeps its own upright bind root; the game translates walkers itself.
|
||||
const _rotOnly = c => new THREE.AnimationClip(c.name, c.duration,
|
||||
c.tracks.filter(t => t.name.endsWith('.quaternion') && !/Hips\.quaternion$/i.test(t.name)));
|
||||
// R16 sit clip note: the sit pose ALSO rides `_rotOnly`. R16 first tried keeping `Hips.quaternion` (a
|
||||
// `_rotWithHips` variant, per the R14 recon) to preserve the pelvic tilt — but the fleet rigs' bind-pose
|
||||
// Hips orientation differs from the Mixamo source, so keeping the sit Hips.quaternion LAYS THE BODY FLAT
|
||||
// (head folded to hip height — verified in-shell), exactly the failure the comment above documents for
|
||||
// walk/idle. So the sit clip drops Hips.quaternion like every other clip; the seated read comes from the
|
||||
// leg-bone bends + spawnRig's seated foot-replant. Reads seated from the crowd POV (the kit hides the legs).
|
||||
|
||||
// core loadGLB returns the whole gltf (scene + animations); adapt to the {scene, anims} rig shape.
|
||||
function loadRig(ref) {
|
||||
@ -45,10 +51,14 @@ function loadRig(ref) {
|
||||
// loadPedFleet(base) → a live Fleet object, returned immediately (ready:false). Arrays fill in as
|
||||
// GLBs land; fleet.whenReady resolves when every load settled. The town runs on placeholders until
|
||||
// then, then upgradeStreetPeople swaps them (see sim.js). Missing files just leave the fleet smaller.
|
||||
export function loadPedFleet(base = 'models/peds/') {
|
||||
// `opts.sit` (R16): also load sit.glb → fleet.sitClip (the seated-drummer pose). GATED because a raw
|
||||
// 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 } = {}) {
|
||||
const fleet = {
|
||||
normal: [], comical: [], all: [], // all = normal ++ comical, stable index for the impostor atlas
|
||||
walkClip: null, idleClip: null,
|
||||
walkClip: null, idleClip: null, sitClip: null,
|
||||
ready: false, whenReady: null,
|
||||
};
|
||||
// DETERMINISM: fill fixed slots by PED_NAMES index, NOT push-on-resolve — otherwise the array
|
||||
@ -69,6 +79,9 @@ export function loadPedFleet(base = 'models/peds/') {
|
||||
jobs.push(loadRig(`${base}idle.glb`).then(r => {
|
||||
const c = r && r.anims && r.anims[0];
|
||||
if (c) { c.tracks.forEach(t => t.name = _canon(t.name)); fleet.idleClip = _rotOnly(c); } }));
|
||||
if (sit) jobs.push(loadRig(`${base}sit.glb`).then(r => { // R16, gig/default boot only
|
||||
const c = r && r.anims && r.anims[0];
|
||||
if (c) { c.tracks.forEach(t => t.name = _canon(t.name)); fleet.sitClip = _rotOnly(c); } }));
|
||||
fleet.whenReady = Promise.all(jobs).then(() => {
|
||||
fleet.normal = nSlots.filter(Boolean);
|
||||
fleet.comical = cSlots.filter(Boolean);
|
||||
@ -169,14 +182,28 @@ function _disposeInner(inner) {
|
||||
}
|
||||
|
||||
// ---- spawnRig: single-clip figure (keepers, impostor baking). Height baked in. ----
|
||||
export function spawnRig(rig, { ry = 0, clip = null, height = 1.75, phase = 0 } = {}) {
|
||||
export function spawnRig(rig, { ry = 0, clip = null, height = 1.75, phase = 0, seated = false } = {}) {
|
||||
if (!rig || !rig.scene) return null;
|
||||
const { fig, inner, head } = buildFigure(rig, height);
|
||||
fig.rotation.y = ry;
|
||||
const mixer = new THREE.AnimationMixer(inner);
|
||||
const chosen = clip || rig.anims.find(c => /idle/i.test(c.name)) || rig.anims[1] || rig.anims[0];
|
||||
const act = _action(mixer, inner, chosen);
|
||||
if (act) { act.time = phase * (act.getClip().duration || 0); act.play(); }
|
||||
if (act) {
|
||||
act.time = phase * (act.getClip().duration || 0); act.play();
|
||||
// R16 SEATED re-plant (OPT-IN — standing spawns skip this, so keepers/browsers/band are byte-identical).
|
||||
// buildFigure planted the BIND pose; a sit clip lowers the hips + swings the legs, so re-plant the POSED
|
||||
// feet to y=0: tick the mixer once to bake the pose, find the lowest bone, drop the figure so the soles
|
||||
// return to the deck. One-time (a drummer's feet don't travel); this recovers the hip descent that
|
||||
// _rotWithHips leaves out by dropping the (scale-unsafe) Hips.position track.
|
||||
if (seated) {
|
||||
mixer.update(0);
|
||||
inner.updateWorldMatrix(true, true);
|
||||
let m = Infinity;
|
||||
inner.traverse(o => { if (o.isBone) { o.getWorldPosition(_wp); m = Math.min(m, _wp.y); } });
|
||||
if (isFinite(m)) inner.position.y -= m;
|
||||
}
|
||||
}
|
||||
function dispose() { mixer.stopAllAction(); mixer.uncacheRoot(inner); _disposeInner(inner); }
|
||||
return { fig, inner, mixer, height, head, dispose };
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user