Compare commits

...

2 Commits

Author SHA1 Message Date
m3ultra
a80ad55f47 Lane F R29 wave 0: the pose gate gains its subject — v6's first clip is in the game
R28's gate had no subject and SKIPPED with its reason printed (look.glb was published, verified,
sha1-proven off the depot — and nothing bound it). D bound it in wave 0. The gate is now
permanent, and flags_check is GREEN:

  ✓ default boot: look.glb BOUND — 6.5s, 64 tracks
  ✓ the glance REACHES THE GAME — peak 46 of 396 peds glancing · 0 console errors
  ✓ ?classic=1: look.glb NEVER FETCHED — the zero-fetch-delta covenant holds
  ✓ ?classic=1: glance inert at the source — 0 of 269 peds, AND the rng stream still draws

That last line is the design, and it is D's cleverness: THE DETERMINISM IS IN THE DRAW, NOT THE
OUTCOME. D takes from a dedicated glanceRng stream always — so the seeded sequence is identical
whether or not a clip exists — and only flips the flag when fleet.lookClip is real. Under
?classic the roll still happens, the flag never sets, nothing is fetched, the covenant stays
byte-identical. A behaviour added without touching a hash.

The gate asserts the clip's ABSENCE under classic, not merely "0 glancing" — because "0
glancing" alone passes on a fleet that failed to load at all. Present-on-default + absent-under-
classic is the pair a broken fleet cannot satisfy.

F'S OWN GATE CRASHED THE SUITE FIRST: the subject check used pg.goto() on a .glb, which starts a
download, aborts the navigation, and took flags_check down before one assertion ran. A gate that
DIES looking for its subject is worse than one that skips — it reports nothing at all and takes
the suite with it. Now it reads the disk. The vacuous-gate law has a sibling: a gate must survive
its own subject being absent.

No tag — wave 0 rides Spike 2's close, gated on John's footage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 20:15:20 +10:00
m3ultra
10025d4082 Lane D R29 (v6 Spike 1): look.glb bound and wired — peds glance, feet planted (wave 0)
The spike reaches the game. E's look.glb (mesh-free, 65 joints, 6.5s 'Look') is bound
through the fleet gate; street peds now glance around during window-shop stops. `lean`
is absent and never existed (E's R28 measurement). Goldens untouched — clips sit outside
every plan hash; the synthetic anchor stays R27's amended 0x5f76e76.

WIRED: loadPedFleet(base, {sit, look}) -> fleet.lookClip, the exact twin of the R16 sit
gate; the shell passes look:!CLASSIC so ?classic fetches NOTHING (a raw fetch would have
breached the zero-fetch-delta covenant — E flagged it, the gate is the answer).
makeActor gains lookA, played only on setLooking(true) — crossfaded (0.3s), not snapped
like sit, since idle->idle-variant reads naturally and there's no replant for a fade to
fight. sim.js gains a dedicated `glance` rng stream, rolled ALWAYS (deterministic) but
only flipped when a clip exists => classic inert at the source. Mutually exclusive with
bench-sit (sit rolls first at 35%, glance takes 40% of the rest). RigPool threads
lookClip; release snaps the glance off so no rig is pooled mid-look.

THE FINDING — my own prediction was wrong, and the measurement caught it. I wrote that a
STANDING clip needs no foot replant ("no hip descent to recover — that trap was sit's
alone"). Measured over the full loop: idle keeps the lowest bone within 4cm of the ground
(-0.04..0.00); look.glb ranges -0.009..+0.205 — the ped HOVERS up to 20.5cm, drifting
through the loop. Same trap, different coat: _rotOnly drops the Hips POSITION track for
rig-height independence, so the clip's authored weight-shift dip can't move the hips and
comes out as the FEET RISING. Sit's one-time replant can't fix it — the float varies per
frame.

FIX: plantFeet() — the per-frame twin of sit's replant. Reset to bind plant -> measure
posed feet -> drop inner by float/scale. ABSOLUTE (reset->measure->correct), never
incremental, so it cannot drift. Reproduces the authored motion exactly: the hips dip
relative to the ground instead of the feet leaving it. Scale-aware like sit's. Foot bones
cached once; cost paid only by glancing peds, on frames their mixer ran (both mixer sites
— the drummer's post-mix lean taught that coupling rule).

PROOFS: foot float 0.205m -> 0.000m (pinned); head yaw sweep span 1.735 (-0.85..+0.89) so
the glance still animates; ?classic — no look fetch, no Look action on any rig, glance
never flips (0); identity signature unchanged, no glance term; sit AND glance never
simultaneous (0); 0 NaN; goldens 0x5f76e76 untouched. A glancing ped is NOT tagged
seated/posed — standing pose at standing stature, so F's no-giants gate needs no arm.

D->F: the pose gate HAS ITS SUBJECT — un-SKIP it. Fresh ?localdepot=1 boot shows a ped
glancing on the footpath, head sweeping, feet planted.
Spike 2 readiness: plantFeet() is the general machinery the brief asked for ("data, not
new code") — riffle and wall-lean should land as clip + roll; the lean's wall-contact
placement is the only genuinely new part.
FLAGGED (A's file, A rests): selfcheck.js:301 still says "the synthetic golden above stays
0x3fa36874" while line 267 pins the amended 0x5f76e76. A stale comment, not a rule — but
the same species as the epoch's five holds, so: flagged, not touched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:44:07 +10:00
6 changed files with 249 additions and 9 deletions

View File

@ -6,6 +6,70 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
---
## ROUND 29 — v6 SPIKE 1: `look.glb` bound and wired — peds glance (wave 0)
*PROCITY-D, 2026-07-17. The spike finally reaches the game: E's `look.glb` (mesh-free, 65 joints, 6.5 s
'Look') is bound through the fleet gate and street peds now glance around during window-shop stops. `lean`
is not here and never was — E measured the stash (R28). Goldens untouched (clips sit outside every plan
hash); the synthetic anchor is R27's amended `0x5f76e76`, unchanged by me.*
### What shipped
- **`rigs.js``loadPedFleet(base, { sit, look })`** → `fleet.lookClip`, the exact twin of the R16 sit gate.
The shell passes `look: !CLASSIC`, so **`?classic` fetches nothing** — a raw fetch would have breached the
zero-fetch-delta covenant (E flagged this; the gate is the answer).
- **`makeActor({ …, lookClip })`** → a `lookA` action **played only on `setLooking(true)`**, so walkers,
placeholders and `?classic` never advance it. `setLooking` **crossfades** (0.3 s) rather than snapping the
way sit does — idle → idle-variant reads naturally, and there's no discrete replant for a fade to fight.
- **`sim.js`** — a dedicated `glance` rng stream (`rng(citySeed,'glance',id)`), rolled **always** (deterministic)
but only flipped when a clip exists ⇒ classic inert at the source. **Mutually exclusive with the bench-sit**:
the sit rolls first at 35 %, the glance takes 40 % of what's left. `RigPool` threads `lookClip`; release
snaps the glance off so no rig is pooled mid-look.
### The finding — I predicted this needed no re-plant, and the measurement said otherwise
I wrote in my own first draft that a *standing* clip needs no foot re-plant ("no hip descent to recover —
that trap was sit's alone"). **Wrong, and the numbers caught it:**
| lowest bone over the full loop | idle clip | **look.glb, no plant** |
|---|---|---|
| min → max | 0.04 → 0.00 (span **4 cm**) | 0.009 → **+0.205** (span **21 cm**) |
The ped **hovers up to 20.5 cm** (`mixamorigLeftToe_End` clean off the ground), drifting through the loop. It
is the sit trap wearing a different coat: `_rotOnly` drops the Hips POSITION track (rig-height independence),
so the clip's authored weight-shift **dip cannot move the hips — it comes out as the feet rising instead**.
Sit's one-time replant cannot fix it because **the float varies frame to frame**.
**Fix: `plantFeet()` — the per-frame twin of sit's replant.** Reset to the bind plant → measure the posed
feet → drop `inner` by float / scale. **Absolute (reset→measure→correct), never incremental, so it cannot
drift.** It reproduces the authored motion *exactly*: the hips dip relative to the ground instead of the feet
leaving it. Scale-aware like sit's. Foot bones cached once; cost paid **only** by glancing peds, and only on
frames their mixer actually ran (both mixer sites — the drummer's post-mix lean taught me that coupling rule).
### Proofs
| | before | after |
|---|---|---|
| foot float over the loop | **0.205 m** | **0.000 m** (pinned) |
| head yaw sweep (the glance itself) | — | **span 1.735** (0.85 → +0.89) |
| `?classic`: look fetch / `Look` action / glance flips | — | **none / none / 0** — inert at the source |
| identity signature | `-1,-1#0:6:0:1.777:1.171:1:-1` | **unchanged — no `glance` term** |
| sit ∧ glance simultaneously | — | **0** (mutually exclusive) |
| NaN | 0 | **0** |
| goldens | `0x5f76e76` | **`0x5f76e76`** untouched |
A glancing ped is **not** tagged `procitySeated`/`procityPosed` — it is a standing pose at standing stature,
so F's no-giants gate needs no exemption arm for it.
### Handshakes
- **D → F:** **the pose gate has its subject** — un-SKIP it. A fresh `?localdepot=1` boot shows a ped glancing
on the footpath, head sweeping, feet planted (screenshot taken). Classic/`?noassets` untouched.
- **Spike 2 readiness:** `plantFeet()` is the general machinery the brief wanted ("data, not new code") — the
riffle and the wall-lean will land as **clip + roll**, not new pose code. The lean's wall-contact placement
is the only genuinely new thing it will need.
- **Stale line spotted (A's file, A rests):** `selfcheck.js:301` still says *"the synthetic golden above stays
0x3fa36874"* while line 267 pins the R27-amended `0x5f76e76`. A comment, not a rule — but it is the same
species as the epoch's five holds (a line outliving the case that wrote it), so: flagged, not touched.
---
## ROUND 28 — v5.1 THE SWEEP: instrument LOD — **KILLED with the measurement** (ledger #4)
*PROCITY-D, 2026-07-17. The oldest tri item on the books (v3.1-era). The brief attached the wind-sway rule:

View File

@ -1413,3 +1413,37 @@ precious. **What ships is the sweep, and the tag says exactly that:**
**Filed for R29 (D, one opt):** `rigs.js` gains `look` the way it gates `sit`; F passes it from the shell.
Then the pose gate has a subject and F will run it. **v6 has not begun in-game — the tag must not say it
has.**
---
## 29. Round 29 wave 0 — **the pose gate gains its subject. v6's first clip is in the game.**
R28's gate had **no subject** and skipped with the reason printed. D bound the clip in wave 0, so the
gate binds too — and it is now permanent, not a hand-proof:
| assertion | result |
|---|---|
| default boot: `look.glb` **bound** | **6.5 s, 64 tracks** |
| **the glance reaches the game** | **peak 46 of 396 peds glancing** · 0 console errors |
| `?classic=1`: the clip | **never fetched** — the zero-fetch-delta covenant holds |
| `?classic=1`: the behaviour | **inert at the source** — 0 of 269 peds, **and the rng stream still draws** |
**That last row is the whole design, and it's D's cleverness worth naming: the determinism lives in the
DRAW, not the outcome.** D takes from a dedicated `glanceRng` stream *always* — so the seeded sequence is
identical whether or not a clip exists — and only flips the flag when `fleet.lookClip` is real. Under
`?classic` the roll still happens, the flag never sets, nothing is fetched, and the covenant is
byte-identical. Add a behaviour without touching a hash.
**And the gate asserts the clip's ABSENCE under classic, not just "0 glancing."** "0 glancing" alone
passes on a fleet that failed to load entirely — the vacuous form of this exact check. Present on the
default boot + absent under classic is the pair that can't both be satisfied by a broken fleet.
### ⚠ F's gate crashed the suite before it ran — worth recording
The first cut checked for its subject with `pg.goto('.../look.glb')`. Navigating to a `.glb` starts a
**download**, Playwright aborts the navigation, and the exception took `flags_check` down with it —
before a single assertion ran. **A gate that dies looking for its subject is worse than one that skips:
it doesn't report "no subject", it reports nothing at all, and takes the suite with it.** The
subject-presence check now reads the disk. *The vacuous-gate law has a sibling: a gate must survive its
own subject being absent.*
**No tag** — wave 0 rides Spike 2's close, which is gated on John's footage. Nothing else is owed on m3.

View File

@ -1369,6 +1369,75 @@ def smoke_tier2(p):
_gv_start(REAL_DB) # leave the box as we found it: the REAL POS serving
def smoke_glance(p):
"""R29 Spike 1 — THE POSE GATE, un-SKIPPED. v6's first thread, and the first clip to reach the game.
R28 wrote this gate and it had NO SUBJECT: `look.glb` was converted, verified, published, and
sha1-proven by fetching the bytes back off the depot and nothing bound it. Every check E ran was
real and passed; none could see that no consumer existed (a gate on the artifact cannot see the wire
the R17 pattern's fifth outing). It SKIPPED with its reason printed rather than report green over
nothing. D bound it in R29 wave 0, so the gate binds too.
Asserts the two things that matter, and they pull in opposite directions:
· the glance REACHES THE GAME peds actually enter the state, on the default boot;
· `?classic=1` never fetches the clip and never glances while STILL DRAWING THE ROLL, because
D takes from a dedicated rng stream always and only flips the flag when a clip exists. That is
what keeps the covenant byte-identical while the behaviour is gated: the determinism is in the
draw, not in the outcome. Asserting "0 glancing" alone would pass on a fleet that failed to load
so we assert the clip is ABSENT under classic and PRESENT on the default boot."""
head('SMOKE: the glance (R29 Spike 1 — v6\'s first clip in the game · classic still zero-fetch)')
b, pg, errs = new_page(p)
try:
# Check the subject on DISK, not via pg.goto — navigating to a .glb starts a download and
# Playwright aborts the navigation, which crashes the gate instead of skipping it. A gate that
# dies looking for its subject is worse than one that skips: it takes the suite with it.
if not os.path.exists(os.path.join(str(ROOT), 'web', 'models', 'peds', 'look.glb')):
print('· SKIP: look.glb not on disk — subject absent, nothing asserted (Lane E)'); return
boot(pg, 'plansrc=osm&town=redhill_godverse')
r = pg.evaluate("""async () => {
const P = window.PROCITY, c = P.citizens;
let peak = 0;
for (let i = 0; i < 9000; i++) { c.update(1/60);
if (i % 30 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
const p = (c._activeList||[]).concat(c.roster||[]);
return { bound: !!P.fleet.lookClip, dur: P.fleet.lookClip ? P.fleet.lookClip.duration : 0,
tracks: P.fleet.lookClip ? P.fleet.lookClip.tracks.length : 0,
peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
}""")
OK(f"default boot: look.glb BOUND — {r['dur']:.1f}s, {r['tracks']} tracks") \
if r['bound'] else FAIL('default boot: look.glb is NOT bound — the R28 gap is back')
OK(f"the glance REACHES THE GAME — peak {r['peak']} of {r['pop']} peds glancing") \
if r['peak'] > 0 else FAIL(f"no ped ever glanced across 9000 ticks ({r['pop']} peds) — clip bound, behaviour absent")
if errs: FAIL(f'glance: {len(errs)} console error(s); first: {errs[0][:120]}')
else: print(' · 0 console errors')
finally:
b.close()
b, pg, errs = new_page(p)
try:
boot(pg, 'classic=1')
r = pg.evaluate("""async () => {
const P = window.PROCITY, c = P.citizens;
let peak = 0;
for (let i = 0; i < 6000; i++) { c.update(1/60);
if (i % 40 === 0) { const p = (c._activeList||[]).concat(c.roster||[]);
const g = p.filter(x => x && x.glance === true).length; if (g > peak) peak = g; } }
const p = (c._activeList||[]).concat(c.roster||[]);
return { bound: !!P.fleet.lookClip, peak, pop: p.length, rng: p.some(x => x && !!x.glanceRng) };
}""")
# NOT bound is the assertion: ?classic must not fetch the clip at all (zero-fetch-delta covenant)
OK('?classic=1: look.glb NEVER FETCHED — the zero-fetch-delta covenant holds') \
if not r['bound'] else FAIL('?classic=1 FETCHED look.glb — the covenant is breached')
OK(f"?classic=1: glance inert at the source — 0 of {r['pop']} peds, and the rng stream still draws "
f"({r['rng']}) so determinism is in the DRAW, not the outcome") \
if (r['peak'] == 0 and r['rng']) \
else FAIL(f"?classic=1: peak {r['peak']} glancing / rng stream present={r['rng']}")
if errs: FAIL(f'glance/classic: {len(errs)} console error(s); first: {errs[0][:120]}')
finally:
b.close()
def smoke_continuity(p):
"""R15 identity continuity (ledger #3): the ped who queued/surged into a venue turns up in its crowd.
Exercises F's real relay end-to-end — VenueQueue.admitOne() → onAdmit → citizens.recordVenueEntry →
@ -1478,6 +1547,7 @@ def main():
smoke_audio(p) # R11 — audio house-law (silent-happy · pre-gesture · mute · noassets · leak)
smoke_gigs(p) # R12/13 — the district gig layer (schedule · band · crowd · budget · cover · noassets)
smoke_continuity(p) # R15 — identity continuity (the ped you followed is in the crowd · settled tris)
smoke_glance(p) # R29 — Spike 1: v6's first clip in the game (classic zero-fetch)
smoke_tier2(p) # R27 — the kill-the-server gate (charter law #1)
smoke_real_crate(p) # R24 — THE REAL CRATE (per-shop atlas identity · soft fall · no-op off the path)
finally:

View File

@ -186,7 +186,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 }); // [R16] sit.glb on the default boot (the seated drummer); ?classic keeps the zero-fetch-delta covenant
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
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 §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1.

View File

@ -55,10 +55,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 } = {}) {
export function loadPedFleet(base = 'models/peds/', { sit = false, look = false } = {}) {
const fleet = {
normal: [], comical: [], all: [], // all = normal ++ comical, stable index for the impostor atlas
walkClip: null, idleClip: null, sitClip: null,
walkClip: null, idleClip: null, sitClip: null, lookClip: null,
ready: false, whenReady: null,
};
// DETERMINISM: fill fixed slots by PED_NAMES index, NOT push-on-resolve — otherwise the array
@ -82,6 +82,13 @@ export function loadPedFleet(base = 'models/peds/', { sit = false } = {}) {
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); } }));
// [R29 Spike 1] E's look.glb (mesh-free, 65 joints, 6.5 s 'Look'). Same gate as sit — the shell passes
// `look: !CLASSIC`, so ?classic fetches nothing and the zero-fetch-delta covenant holds. `_rotOnly` is
// correct here for the same reason as walk/idle: it's a STANDING clip, so there's no hip descent to
// recover and no foot re-plant needed (that trap was sit's alone — R16).
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); } }));
fleet.whenReady = Promise.all(jobs).then(() => {
fleet.normal = nSlots.filter(Boolean);
fleet.comical = cSlots.filter(Boolean);
@ -223,7 +230,7 @@ export function seatedLean(spine, angle = 0.34) {
// ---- makeActor: near-tier pedestrian with walk↔idle crossfade (built at nominal height; the sim
// scales the outer Group per-citizen so pooled actors are height-agnostic and reusable). ----
export function makeActor(rig, { walkClip, idleClip, sitClip = null, nominalHeight = 1.75 } = {}) {
export function makeActor(rig, { walkClip, idleClip, sitClip = null, lookClip = null, nominalHeight = 1.75 } = {}) {
if (!rig || !rig.scene) return null;
const { fig, inner, head, nominalHeight: nom } = buildFigure(rig, nominalHeight);
const bindPlantY = inner.position.y; // R17 bench-sit: the standing feet-plant, to restore after a sit
@ -233,9 +240,16 @@ export function makeActor(rig, { walkClip, idleClip, sitClip = null, nominalHeig
// R17 bench-sit: a third action, PLAYED ONLY when setSitting(true) — so walkers (and ?classic, where
// sitClip is null) are byte-identical: sitA never plays, the plant never moves, setSitting is a no-op.
const sitA = sitClip ? _action(mixer, inner, sitClip) : null;
// [R29 Spike 1] the glance — same opt-in shape as sitA: PLAYED ONLY when setLooking(true), so walkers,
// placeholders and ?classic (lookClip null) are byte-identical — lookA never advances the mixer.
const lookA = lookClip ? _action(mixer, inner, lookClip) : null;
let seatBone = null;
if (sitA) inner.traverse(o => { if (!seatBone && o.isBone && /Spine1$/i.test(o.name)) seatBone = o; });
let sitting = false;
// [R29] foot bones, cached once for the per-frame plant (see plantFeet). Only built when a posed clip
// exists, so walkers/?classic pay nothing.
let footBones = null;
if (lookA) { const fb = []; inner.traverse(o => { if (o.isBone && /(Toe|Foot)/i.test(o.name)) fb.push(o); }); footBones = fb.length ? fb : null; }
let sitting = false, looking = false;
let moving = null; // tri-state so the first setMoving always applies
// both actions play; exactly one holds weight 1 at rest, setMoving transfers between them
if (walkA) { walkA.play(); walkA.setEffectiveWeight(1); }
@ -279,13 +293,53 @@ export function makeActor(rig, { walkClip, idleClip, sitClip = null, nominalHeig
fig.userData.procitySeated = false;
}
}
// [R29 Spike 1] the standing glance during a window-shop loiter. Deliberately SIMPLER than setSitting:
// it's a standing clip, so there is no posed foot-replant and no scale trap — the feet never leave the
// bind plant. Unlike sit (a snap, which had to avoid a crossfade fighting the discrete replant), this
// crossfades: idle → idle-variant reads naturally, and the sim only calls it while the ped is stopped.
// No-op when lookClip is absent (?classic / ?noassets) ⇒ byte-identical by construction.
function setLooking(l, fade = 0.3) {
if (!lookA || l === looking) return;
looking = l;
moving = null; // force the next setMoving to re-apply on the way out
if (l) {
lookA.reset().play();
if (idleA) idleA.crossFadeTo(lookA, fade, false);
else lookA.setEffectiveWeight(1);
if (walkA) walkA.setEffectiveWeight(0);
} else {
if (idleA) { idleA.reset().play(); lookA.crossFadeTo(idleA, fade, false); }
else lookA.setEffectiveWeight(0);
inner.position.y = bindPlantY; // drop the plant correction — back to the standing bind plant
}
}
// [R29] MEASURED, and it corrected my own assumption: I expected a standing clip to need no re-plant
// ("no hip descent to recover"). It does. `_rotOnly` drops the Hips POSITION track to stay rig-height
// independent, so look.glb's authored weight-shift dip cannot move the hips — it comes out as the FEET
// rising instead: lowest bone ranges 0.009…+0.205 m over the loop vs idle's 0.04…0.00. A 20 cm hover.
// Sit's one-time re-plant can't fix it because the float VARIES per frame, so this is the per-frame twin:
// reset to the bind plant, measure the posed feet, drop `inner` by the float / scale. Absolute (reset →
// measure → correct), never incremental, so it cannot drift. The result reproduces the authored motion
// exactly — the hips dip relative to the ground instead of the feet leaving it. Scale-aware like sit's.
// Cost is paid ONLY by glancing peds (a handful), and only on frames their mixer actually ran.
function plantFeet() {
if (!footBones || !looking) return;
inner.position.y = bindPlantY; // reference pose
inner.updateWorldMatrix(true, true);
let w = Infinity;
for (const b of footBones) { b.getWorldPosition(_wp); if (_wp.y < w) w = _wp.y; }
if (!isFinite(w)) return;
const S = fig.scale.y || 1;
inner.position.y -= (w - fig.position.y) / S; // posed soles → the footpath
}
// seed the clip phase so the crowd isn't in lockstep
function setPhase(p) {
if (walkA) walkA.time = p * (walkA.getClip().duration || 1);
if (idleA) idleA.time = p * (idleA.getClip().duration || 1);
if (lookA) lookA.time = p * (lookA.getClip().duration || 1); // R29: glances desync too
}
function dispose() { mixer.stopAllAction(); mixer.uncacheRoot(inner); _disposeInner(inner); }
return { fig, inner, mixer, head, seatBone, nominalHeight: nom, setMoving, setSitting, setPhase, dispose,
return { fig, inner, mixer, head, seatBone, nominalHeight: nom, setMoving, setSitting, setLooking, plantFeet, setPhase, dispose,
hasClips: !!(walkA && idleA) };
}

View File

@ -35,6 +35,7 @@ const PATRON_STRIDE = 10; // m walked between patronage check
const GIG_RANGE = 34; // m — a gig pulls peds from further than a normal shopfront
const GIG_SURGE = 0.55; // patron chance at the venue while the gig is on (vs ~0.16 day)
const BENCH_SIT_FRAC = 0.35; // R17: fraction of window-shop loiters that become a bench-sit (seeded)
const GLANCE_FRAC = 0.40; // R29: fraction of the REMAINING (standing) loiters that glance around
// time-of-day density curve: t01 in [0,1) over a day → crowd multiplier (CITY_SPEC: lunch rush,
// near-empty at night). Sampled at 8 control points, linearly interpolated.
@ -93,6 +94,7 @@ class RigPool {
release(actor) {
if (!actor) return;
if (actor.setSitting) actor.setSitting(false); // R17: never pool a rig mid-sit — restore the standing plant
if (actor.setLooking) actor.setLooking(false, 0); // R29: nor mid-glance — snap back to idle before pooling
let arr = this.free.get(actor.pedIndex);
if (!arr) this.free.set(actor.pedIndex, arr = []);
arr.push(actor);
@ -258,6 +260,7 @@ export class CitizenSim {
turn: rng(this.citySeed, 'turn', id), loit: rng(this.citySeed, 'loiter', id),
patron: null, patronTarget: null, patronTimer: 0, patronRng: rng(this.citySeed, 'patron', id),
sit: false, sitRng: rng(this.citySeed, 'benchsit', id), // R17: dedicated stream — no shift to turn/loit/patron
glance: false, glanceRng: rng(this.citySeed, 'glance', id), // R29: ditto — independently keyed, signature untouched
};
if (this.mode === 'rig' && this.fleet.ready) { const pk = pickRig(this.fleet, c.pedRoll); if (pk) { c.pedIndex = pk.index; c.subject = pk.index; } }
this._placeOnLane(c);
@ -307,6 +310,7 @@ export class CitizenSim {
turn: rng(this.citySeed, 'turn', id),
loit: rng(this.citySeed, 'loiter', id),
sit: false, sitRng: rng(this.citySeed, 'benchsit', id), // R17: dedicated stream — no shift to turn/loit/patron
glance: false, glanceRng: rng(this.citySeed, 'glance', id), // R29: ditto — independently keyed, signature untouched
};
// assign a real ped type if the fleet is already up (roster can grow after upgrade)
if (this.mode === 'rig' && this.fleet.ready) {
@ -522,7 +526,7 @@ export class CitizenSim {
this._setImpostorLayer(atlas);
this.mode = 'rig';
this.rigPool = new RigPool(this.fleet, { walkClip: this.fleet.walkClip, idleClip: this.fleet.idleClip, sitClip: this.fleet.sitClip }); // sitClip null under ?classic → bench-sit inert
this.rigPool = new RigPool(this.fleet, { walkClip: this.fleet.walkClip, idleClip: this.fleet.idleClip, sitClip: this.fleet.sitClip, lookClip: this.fleet.lookClip }); // sit/lookClip null under ?classic → bench-sit + glance inert
// assign real ped types to every citizen; drop placeholder near-actors so they re-acquire as rigs
const assign = c => {
const pk = pickRig(this.fleet, c.pedRoll);
@ -589,6 +593,12 @@ export class CitizenSim {
// so ?classic (no sit.glb) is inert at the source, not just at the render.
const wantsSit = c.sitRng ? (c.sitRng() < BENCH_SIT_FRAC) : false;
c.sit = wantsSit && !!(this.fleet && this.fleet.sitClip);
// [R29 Spike 1] a standing glance — the other way to spend a window-shop stop. Same discipline as
// the sit roll: draw ALWAYS from a dedicated stream (deterministic; never shifts turn/loit/patron),
// but only flip the flag when a clip exists ⇒ ?classic inert at the source. Mutually exclusive with
// the sit: you can't bench-sit and stand glancing, so the sit wins and the glance takes the rest.
const wantsGlance = c.glanceRng ? (c.glanceRng() < GLANCE_FRAC) : false;
c.glance = !c.sit && wantsGlance && !!(this.fleet && this.fleet.lookClip);
}
}
this._placeOnLane(c);
@ -732,7 +742,11 @@ export class CitizenSim {
// so every non-sitting ped is byte-identical.
const wantSit = !!(c.sit && c.loiter > 0 && a.setSitting);
if (a.setSitting) a.setSitting(wantSit);
if (!wantSit) a.setMoving?.(c.loiter <= 0);
// [R29 Spike 1] the glance rides the same seam: a stopped ped either sits, glances, or plain idles.
// No-op for walkers, placeholders and ?classic (no lookClip) ⇒ every non-glancing ped byte-identical.
const wantLook = !wantSit && !!(c.glance && c.loiter > 0 && a.setLooking);
if (a.setLooking) a.setLooking(wantLook);
if (!wantSit && !wantLook) a.setMoving?.(c.loiter <= 0);
} else if (want === 'mid') {
mid.push(c);
}
@ -747,7 +761,9 @@ export class CitizenSim {
const c = near[i], a = c.actor;
c._acc += dt;
if (a.mixer) {
if (i < MIXER_ALWAYS) { a.mixer.update(c._acc); c._acc = 0; } else extra.push(c);
// [R29] plantFeet must follow the mixer that posed the feet — same coupling rule the drummer's
// post-mix lean taught us. Only glancing peds pay it; it self-guards on `looking`.
if (i < MIXER_ALWAYS) { a.mixer.update(c._acc); c._acc = 0; if (c.glance) a.plantFeet?.(); } else extra.push(c);
} else {
a.tick?.(c._acc, c.loiter <= 0); c._acc = 0; // placeholder
}
@ -758,6 +774,8 @@ export class CitizenSim {
for (let k = 0; k < MIXER_EXTRA && k < extra.length; k++) {
const c = extra[(this._mixerCursor + k) % extra.length];
c.actor.mixer.update(c._acc); c._acc = 0;
if (c.glance) c.actor.plantFeet?.(); // R29: the round-robin tier plants on the frames its mixer ran
}
this._mixerCursor = (this._mixerCursor + MIXER_EXTRA) % extra.length;
}