// 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()); }