Compare commits
2 Commits
09dff6b788
...
a80ad55f47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a80ad55f47 | ||
|
|
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)
|
||||
|
||||
*PROCITY-D, 2026-07-17. The oldest tri item on the books (v3.1-era). The brief attached the wind-sway rule:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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) };
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user