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>
This commit is contained in:
parent
09dff6b788
commit
10025d4082
@ -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)
|
## 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:
|
*PROCITY-D, 2026-07-17. The oldest tri item on the books (v3.1-era). The brief attached the wind-sway rule:
|
||||||
|
|||||||
@ -186,7 +186,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 }); // [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
|
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 §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1.
|
// [Lane F §F2] ?dig=1 enables Lane C's crate-riffle inside record interiors (v2). Off ⇒ byte-identical to v1.
|
||||||
|
|||||||
@ -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
|
// 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 } = {}) {
|
export function loadPedFleet(base = 'models/peds/', { sit = false, look = 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,
|
walkClip: null, idleClip: null, sitClip: null, lookClip: null,
|
||||||
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
|
||||||
@ -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
|
if (sit) jobs.push(loadRig(`${base}sit.glb`).then(r => { // R16, gig/default boot only
|
||||||
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.sitClip = _rotOnly(c); } }));
|
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.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);
|
||||||
@ -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
|
// ---- 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). ----
|
// 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;
|
if (!rig || !rig.scene) return null;
|
||||||
const { fig, inner, head, nominalHeight: nom } = buildFigure(rig, nominalHeight);
|
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
|
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
|
// 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.
|
// 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;
|
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;
|
let seatBone = null;
|
||||||
if (sitA) inner.traverse(o => { if (!seatBone && o.isBone && /Spine1$/i.test(o.name)) seatBone = o; });
|
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
|
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
|
// both actions play; exactly one holds weight 1 at rest, setMoving transfers between them
|
||||||
if (walkA) { walkA.play(); walkA.setEffectiveWeight(1); }
|
if (walkA) { walkA.play(); walkA.setEffectiveWeight(1); }
|
||||||
@ -279,13 +293,53 @@ export function makeActor(rig, { walkClip, idleClip, sitClip = null, nominalHeig
|
|||||||
fig.userData.procitySeated = false;
|
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
|
// seed the clip phase so the crowd isn't in lockstep
|
||||||
function setPhase(p) {
|
function setPhase(p) {
|
||||||
if (walkA) walkA.time = p * (walkA.getClip().duration || 1);
|
if (walkA) walkA.time = p * (walkA.getClip().duration || 1);
|
||||||
if (idleA) idleA.time = p * (idleA.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); }
|
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) };
|
hasClips: !!(walkA && idleA) };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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_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 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 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,
|
// 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.
|
// near-empty at night). Sampled at 8 control points, linearly interpolated.
|
||||||
@ -93,6 +94,7 @@ class RigPool {
|
|||||||
release(actor) {
|
release(actor) {
|
||||||
if (!actor) return;
|
if (!actor) return;
|
||||||
if (actor.setSitting) actor.setSitting(false); // R17: never pool a rig mid-sit — restore the standing plant
|
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);
|
let arr = this.free.get(actor.pedIndex);
|
||||||
if (!arr) this.free.set(actor.pedIndex, arr = []);
|
if (!arr) this.free.set(actor.pedIndex, arr = []);
|
||||||
arr.push(actor);
|
arr.push(actor);
|
||||||
@ -258,6 +260,7 @@ export class CitizenSim {
|
|||||||
turn: rng(this.citySeed, 'turn', id), loit: rng(this.citySeed, 'loiter', id),
|
turn: rng(this.citySeed, 'turn', id), loit: rng(this.citySeed, 'loiter', id),
|
||||||
patron: null, patronTarget: null, patronTimer: 0, patronRng: rng(this.citySeed, 'patron', 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
|
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; } }
|
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);
|
this._placeOnLane(c);
|
||||||
@ -307,6 +310,7 @@ export class CitizenSim {
|
|||||||
turn: rng(this.citySeed, 'turn', id),
|
turn: rng(this.citySeed, 'turn', id),
|
||||||
loit: rng(this.citySeed, 'loiter', id),
|
loit: rng(this.citySeed, 'loiter', id),
|
||||||
sit: false, sitRng: rng(this.citySeed, 'benchsit', id), // R17: dedicated stream — no shift to turn/loit/patron
|
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)
|
// assign a real ped type if the fleet is already up (roster can grow after upgrade)
|
||||||
if (this.mode === 'rig' && this.fleet.ready) {
|
if (this.mode === 'rig' && this.fleet.ready) {
|
||||||
@ -522,7 +526,7 @@ export class CitizenSim {
|
|||||||
this._setImpostorLayer(atlas);
|
this._setImpostorLayer(atlas);
|
||||||
|
|
||||||
this.mode = 'rig';
|
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
|
// assign real ped types to every citizen; drop placeholder near-actors so they re-acquire as rigs
|
||||||
const assign = c => {
|
const assign = c => {
|
||||||
const pk = pickRig(this.fleet, c.pedRoll);
|
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.
|
// 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;
|
const wantsSit = c.sitRng ? (c.sitRng() < BENCH_SIT_FRAC) : false;
|
||||||
c.sit = wantsSit && !!(this.fleet && this.fleet.sitClip);
|
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);
|
this._placeOnLane(c);
|
||||||
@ -732,7 +742,11 @@ export class CitizenSim {
|
|||||||
// so every non-sitting ped is byte-identical.
|
// so every non-sitting ped is byte-identical.
|
||||||
const wantSit = !!(c.sit && c.loiter > 0 && a.setSitting);
|
const wantSit = !!(c.sit && c.loiter > 0 && a.setSitting);
|
||||||
if (a.setSitting) a.setSitting(wantSit);
|
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') {
|
} else if (want === 'mid') {
|
||||||
mid.push(c);
|
mid.push(c);
|
||||||
}
|
}
|
||||||
@ -747,7 +761,9 @@ export class CitizenSim {
|
|||||||
const c = near[i], a = c.actor;
|
const c = near[i], a = c.actor;
|
||||||
c._acc += dt;
|
c._acc += dt;
|
||||||
if (a.mixer) {
|
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 {
|
} else {
|
||||||
a.tick?.(c._acc, c.loiter <= 0); c._acc = 0; // placeholder
|
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++) {
|
for (let k = 0; k < MIXER_EXTRA && k < extra.length; k++) {
|
||||||
const c = extra[(this._mixerCursor + k) % extra.length];
|
const c = extra[(this._mixerCursor + k) % extra.length];
|
||||||
c.actor.mixer.update(c._acc); c._acc = 0;
|
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;
|
this._mixerCursor = (this._mixerCursor + MIXER_EXTRA) % extra.length;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user