THE ROUND IN ONE MEASUREMENT (12 samples x 146 active, ?clips=0 vs default — a new flag that turns off the library and nothing else): walking 99.3% -> 78.1% · bench-sit 0 -> 9.5% · lean 0 -> 7.1% · stopped in own idle 0.4% -> 5.1% · DISTINCT CLIPS ACROSS THE CROWD 4 -> 20. The town was 99.3% people walking because standing still had nowhere to happen. Wiring: new postures.js + clipbank.js. idles.glb (10/10) drives a per-citizen deterministic idle on every near-tier actor plus the seeded shopkeeper. locomotion gives 33.7% of walkers a shopping bag. sitlean (8/8, lazy) puts 4 sits on Lane B's ACTUAL benches and 4 leans on shopfront walls. browse (5/8, lazy) is a real BROWSE state at C's browse points, seeded per (shopId, slot). venue (5/6, lazy) widens the gig crowd, plus a publican pouring and a record keeper in headphones. social (0/8) is never fetched — two-person conversation needs a paired state machine, filed to R42. Cost: boot = 4 requests, 1.24 MB / 16 clips resident; the rest lazy on first need; heap delta +3.34 MB; mixer median 0.1 ms both arms. ?clips=0 / ?classic=1 / ?noassets=1 fetch ZERO clips — not even clipbank.js (dynamic import). No shell edit needed. DRAWS: +0 on every bookmark (street_noon 193, crossroads 108, night_crowd 128, market_square 94, night_neon 111, interior 110 — identical both arms). Ruling 4 respected exactly. DETERMINISM: 150 citizens, two fresh contexts, byte-equal posture signature. Controls: seed+1 differs; EVERY clip GLB delayed 2 s -> identical signature (posture is a pure function of (citySeed, id), never of residency). 6 new streams collide with none of the 12 pre-R41 keys. TWO FINDINGS THAT CHANGED THE DESIGN: the idle pool was INVISIBLE — wired only to the R17/R29 node loiter, so only 0.8% of citizens were ever stopped; and the lean never fired at all (0 in a 9 s run). Both moved to the patronage stride check. Bench stations are GATED not trusted: 14/14 derived stations coincide with real instanced geometry within 2 cm, and the control (same stations offset 2 m) matches 0/14. Filed to B: one benchStops(plan) export retires the mirror, and furniture.js puts the bench's front ALONG the street rather than facing the road, contradicting its own comment. Leak: +0 geometries, +1 texture over 6 enter/exit cycles. Goldens 157,647/157,647, 0x5f76e76. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
112 lines
6.9 KiB
JavaScript
112 lines
6.9 KiB
JavaScript
// PROCITY Lane D — R41 §41.3: the POSTURE TABLE. Pure, seeded, THREE-free, load-state-free.
|
|
//
|
|
// The whole point of this file is that WHICH clip a citizen plays is a pure function of
|
|
// (citySeed, identity) — never of what happens to be resident in memory when the question is asked.
|
|
// That is what makes the determinism law survivable under lazy loading: the assignment is decided
|
|
// once, deterministically, and the ClipBank either has that clip yet or the actor falls back to the
|
|
// R2-era 8-clip asset. Same seed → same postures, byte-equal, whether the group GLB landed in 40 ms
|
|
// or 4 s or never.
|
|
//
|
|
// Consequences of that rule, deliberately:
|
|
// • the pools below are LITERAL clip-id lists, not manifest-derived. A manifest read is async; an
|
|
// async pool would make posture depend on load timing, which is the bug this file exists to
|
|
// forbid. `web/assets/motion_manifest.json` is still the contract — `tools/qa/r41_postures.mjs`
|
|
// asserts every id here resolves in it, with the right category and the right group file.
|
|
// • every stream is freshly keyed (`posture`, `browse-pose`, `keeper-pose`, `gig-pose`,
|
|
// `benchstop`, `leanstop`). None of them shifts `turn`/`loiter`/`patron`/`benchsit`/`glance`, so
|
|
// the R8/R17/R29 crowd behaviour and every existing identity signature are untouched.
|
|
// • `@`-prefixed ids are SENTINELS for the pre-R41 8-clip assets (`models/peds/walk.glb` etc.),
|
|
// not library clips. The gate skips them; the actor maps them to its base action.
|
|
//
|
|
// No imports but the PRNG ⇒ node can import this file directly (the gate does).
|
|
|
|
import { rng } from '../core/prng.js';
|
|
|
|
// ---- pools (curated from web/assets/motion_manifest.json, R41 §41.1) ----
|
|
// Chosen for a 1990s Australian high street, not for coverage. Excluded on purpose:
|
|
// · the whole `social.glb` group — a two-person clip needs a two-person state machine and a
|
|
// partner-pairing pass; 896 KB for a state D does not have yet. Filed R42.
|
|
// · `browse_hold_turn_l/r` (172°/147° loop seam — one-shot turns, no turn state to hang them on),
|
|
// `turn_*`/`walk_to_stand` (same: the sim turns instantly at a node), `venue_clap_seated`
|
|
// (no seated venue slot yet).
|
|
export const IDLE_POOL = [
|
|
'idle_breathing', 'idle_standing', 'idle_look_around', 'idle_weight_shift', 'idle_cocky_lean',
|
|
'idle_yawn', 'idle_happy_1', 'idle_happy_2', 'idle_old_man', 'idle_smoking',
|
|
];
|
|
// '@walk' = the fleet's own walk.glb (the base gait every citizen has had since R2). The shopping-bag
|
|
// walk is the only locomotion clip with a state to live in; the turn set is filed to R42 with the
|
|
// turn-state machine it needs.
|
|
export const WALK_POOL = ['@walk', '@walk', 'walk_shopping_bag']; // ⇒ 1/3 carry a bag
|
|
export const SIT_POOL = ['sit_bench_swing', 'sit_hands_thighs', 'sit_impatient', 'sit_fidget_feet'];
|
|
export const LEAN_POOL = ['lean_wall_postures', 'lean_wall_leg_up', 'lean_wall_shoulders', 'lean_wall_walkman'];
|
|
// browse: two continuous holds + the two cabinet one-shots + the pick-up. The pick-up (92° seam) and
|
|
// the cabinet pair read as crate-digging when the bank ping-pongs them (see clipbank.js LOOP_SEAM_DEG).
|
|
export const BROWSE_POOL = [
|
|
'browse_hold_idle', 'browse_carry_box', 'browse_open_cabinet', 'browse_close_cabinet', 'browse_pick_up',
|
|
];
|
|
// the gig. The existing v3 dance set (dance_party/medium/drink/sway, models/peds/*.glb) is NOT
|
|
// replaced — venue_northern_soul JOINS it, so the epoch's crowd keeps its vocabulary and gains one.
|
|
export const VENUE_DANCE_POOL = ['venue_northern_soul'];
|
|
export const VENUE_STAND_POOL = ['venue_clap_stand', 'venue_cheer', 'venue_headphones'];
|
|
// keeper flavour by shop type — the publican pours, the record-shop keeper wears the cans. Everything
|
|
// else takes a seeded idle. Both live in venue.glb, so those two shop types (and only those) pay its
|
|
// 526 KB, lazily, on first entry.
|
|
export const KEEPER_TYPE_CLIP = { pub: 'venue_bartending', band_room: 'venue_bartending', rsl: 'venue_bartending', record: 'venue_headphones' };
|
|
|
|
// clip-id → its group GLB. Static so a lazy load can be kicked before the manifest lands; the gate
|
|
// asserts it against `motion_manifest.json`'s own `clips[id].group` field, id for id.
|
|
export const GROUP_OF = (() => {
|
|
const g = {};
|
|
for (const id of IDLE_POOL) g[id] = 'idles.glb';
|
|
for (const id of WALK_POOL) if (id[0] !== '@') g[id] = 'locomotion.glb';
|
|
for (const id of SIT_POOL) g[id] = 'sitlean.glb';
|
|
for (const id of LEAN_POOL) g[id] = 'sitlean.glb';
|
|
for (const id of BROWSE_POOL) g[id] = 'browse.glb';
|
|
for (const id of VENUE_DANCE_POOL) g[id] = 'venue.glb';
|
|
for (const id of VENUE_STAND_POOL) g[id] = 'venue.glb';
|
|
for (const id of Object.values(KEEPER_TYPE_CLIP)) g[id] = 'venue.glb';
|
|
return g;
|
|
})();
|
|
// the groups fetched at BOOT (everything else is lazy — see LANE_D_NOTES §41 for the fetch ledger)
|
|
export const BOOT_GROUPS = ['idles.glb', 'locomotion.glb'];
|
|
|
|
const at = (arr, r01) => arr[(r01 * arr.length) | 0];
|
|
|
|
// ---- the citizen's four standing postures — one stream, four draws, forever ----
|
|
// id is the sim's citizen id (v1: an integer; stream mode: `${chunkKey}#${i}`) — both are stable
|
|
// per (seed, place), so this is stable per (seed, place) too.
|
|
export function posturesFor(citySeed, id) {
|
|
const r = rng(citySeed, 'posture', id);
|
|
return {
|
|
idle: at(IDLE_POOL, r()),
|
|
walk: at(WALK_POOL, r()),
|
|
sit: at(SIT_POOL, r()),
|
|
lean: at(LEAN_POOL, r()),
|
|
};
|
|
}
|
|
// stable one-line signature of a citizen's posture set (the determinism artefact the gate diffs)
|
|
export function postureSig(id, p) { return `${id}|${p.idle}|${p.walk}|${p.sit}|${p.lean}`; }
|
|
|
|
// ---- the other seeded picks, each on its own freshly-keyed stream ----
|
|
// a browse point reads the same way every visit: keyed by (shopId, slot), never by visit order.
|
|
export function browsePostureFor(citySeed, shopId, slot) {
|
|
return at(BROWSE_POOL, rng(citySeed, 'browse-pose', `${shopId}#${slot}`)());
|
|
}
|
|
// the shopkeeper: type flavour if this trade has one, else a seeded idle, stable per shop.
|
|
export function keeperPostureFor(citySeed, shopId, type) {
|
|
const flavour = KEEPER_TYPE_CLIP[type];
|
|
if (flavour) return flavour;
|
|
return at(IDLE_POOL, rng(citySeed, 'keeper-pose', shopId)());
|
|
}
|
|
// A gig crowd slot — WIDENING, not replacing. The v3 dance pick (band.js's `gigdance` stream over
|
|
// `fleet.danceClips`) is left exactly as it is; this is a SECOND, independently keyed roll that says
|
|
// "this slot takes a venue clip instead". Returns null ⇒ the slot keeps its R13/R14 behaviour
|
|
// verbatim, which is also what every venue-clip-absent boot gets ⇒ byte-identical by construction.
|
|
export const GIG_DANCE_SWAP = 1 / 3; // of dancers take the northern-soul step
|
|
export const GIG_STAND_SWAP = 2 / 3; // of standers clap / cheer / listen instead of plain idling
|
|
export function gigPostureFor(citySeed, key, dancing) {
|
|
const r = rng(citySeed, 'gig-pose', key);
|
|
if (r() >= (dancing ? GIG_DANCE_SWAP : GIG_STAND_SWAP)) return null;
|
|
return at(dancing ? VENUE_DANCE_POOL : VENUE_STAND_POOL, r());
|
|
}
|