@adatechnology/game-crowd 0.2.6 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,6 +90,47 @@ Civilians are a literal port of the game's survivors on the block (`jogo.js` G 6
90
90
  without the rig; the vultures of G 66700-66708 and the live map's ring of birds of the Bancada de Hordas (G 53609-53615). Two goldens replay them
91
91
  with the horde: `game-crowd-civilians.json` (3 scenes) and `game-crowd-herd.json`.
92
92
 
93
+ ## Physics: a dense horde and the chain of a blast
94
+
95
+ `CrowdOptions.physics` is off by default and then the horde is the game's, bit for bit (the team goldens run with it off). `CROWD_PHYSICS_DEFAULTS` turns everything on:
96
+
97
+ ```ts
98
+ const crowd = createCrowd({ pack, seed, physics: { ...CROWD_PHYSICS_DEFAULTS, playerPressure: { enabled: true, knockThreshold: 1500, crushDamagePerSecond: 6 } } })
99
+ // fixed steps only: the same seed gives the same result at 30, 60 and 144 fps
100
+ crowd.update(1 / 60, { target, bandCentre: [x, z] })
101
+ crowd.blast({ point: [x, y, z], energy: 200000, radius: 15, lethalRadius: 5 }) // resolved at the start of the next step, in one pass over the agents
102
+ crowd.stats.physics // falls by cause, fallsBeyondBlast, climbers, moundMax, thrownPeak, blastChecks...
103
+ ```
104
+
105
+ - **Contact:** inelastic impulses between bodies (symmetric Gauss-Seidel, so a low index has no advantage) and a position correction by the mean of what each agent was asked to move; it never adds energy.
106
+ - **Pressure:** a field apart from the contact that moves one hop per fixed step, so a file of N agents pushing on a wall gives its front `N` times the drive (`design-chain-horde.md` section 4.5).
107
+ - **Balance:** standing, staggering, fallen, getting up and thrown, by pressure over a critical value of the body (`CrowdType.balance` and the personality), shoves, trips, landings and collapses.
108
+ - **Mound:** lying bodies are a height per cell (integer millimetres) that others climb, trample and bring down when the base goes.
109
+ - **Thrown bodies:** a pool of 32 simulated here (`crowd.thrown`) and only drawn by the renderer; `thrown.priority` is `'mixed'` (half the slots to the fastest, half to the nearest to the band centre), `'fastest'` or `'nearest'`.
110
+ - **Personality:** speed, reaction, stability, trip, drive, cadence, aggression, gait and idle from the identity of each agent, never from the shared random stream.
111
+ - **Far band (R1):** no pairs. Every 12th step a coarse grid of 2 m counts the standing walkers of band R1 per cell; above 4 bodies the wanted speed is `4 / bodies` and the field is `(bodies − 4) · 150` N, which the same balance rule reads (`physics.density`, needs `balance`). A body that falls there lies without entering the mound, and joins it when it comes into R0.
112
+ - **Player pressure:** `playerPressure` reports the pressure of the horde on the target through `knock` and `hurt` (off in the package; a game turns it on).
113
+
114
+ ### The contact and the pressure in a worker
115
+
116
+ `physics.worker` is `'off'` by default (the step runs where it always did). `'auto'` moves the contact and the pressure to a worker when one can be made and stays in the step when it cannot; `'on'` throws instead of staying.
117
+ The worker is **one step behind**: it is handed the state at the end of step *k*, and step *k + 1* (`workerLatency: 1`, default) or *k + 2* (`workerLatency: 2`) adds what it computed as deltas of position, push and velocity and replaces the pressure field. With the same inputs per step the answer is the same whichever thread computed it: `inline` (the same kernel on the calling thread) is the reference, and the tests compare it with a worker byte for byte. Balance, mound, thrown bodies and blasts stay on the main thread.
118
+
119
+ ```ts
120
+ const crowd = createCrowd({ pack, seed, physics: { ...CROWD_PHYSICS_DEFAULTS, worker: 'auto', workerLatency: 2 } })
121
+ // ...
122
+ crowd.dispose() // stops the worker
123
+ ```
124
+
125
+ - `workerTransport: 'shared'` (default where `SharedArrayBuffer` is usable): frame in a `SharedArrayBuffer`, control word with `Atomics`. The main thread waits for the answer, so the run is reproducible. A browser needs the page **cross-origin isolated**: serve it with `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` (or `credentialless`), and every cross-origin sub-resource with `Cross-Origin-Resource-Policy` or CORS. `crossOriginIsolated` is then `true`. Node has `SharedArrayBuffer` always.
126
+ - `workerTransport: 'transfer'`: without isolation; the frame goes by `postMessage` with its buffer transferred. The main thread cannot wait for an event, so it applies the answer when it has arrived and runs without it otherwise (the contact then lags more): it is not reproducible. `'auto'` picks it when the page is not isolated.
127
+ - `workerFactory`: the default makes a module worker from `physics-worker.js`, which sits next to `index.js` in `dist`, with `new Worker(new URL('./physics-worker.js', import.meta.url), { type: 'module' })` (Vite, webpack 5 and esbuild resolve it); the `index.global.js` build has no `import.meta`, so there give a `workerFactory`. A host that bundles its own worker (or runs node `worker_threads`) gives the factory and calls `attachPhysicsWorker(port)` inside the worker; `tests/fixtures/node-physics-worker.mjs` is a node example.
128
+ - The frame copies the grid occupancy every step (1 byte per cell, 2.4 MB for the default 768 m grid): a `NavGrid` no bigger than the playable area keeps that copy small.
129
+
130
+ Measured with `bench/dense-update.mjs` (node, 2000 agents pressed against a wall, all in band R0, `WORKER=shared LATENCY=2`): `crowd.update` on the main thread goes from 3.0 to 3.8 ms to 1.6 to 2.1 ms mean (the machine was loaded); with one step of latency the main thread waits for the worker for a third of its step, so it gains little. See `specs/005-camadas-e-quadro/evidence.md`, row T4b.perf.
131
+
132
+ `bench/dense-update.mjs` measures `crowd.update` with 1000 and 2000 agents pressed against a wall, with and without the physics.
133
+
93
134
  ## What is not here yet
94
135
 
95
136
  The drawing and the test area are in `@adatechnology/game-crowd-three`. The game's multi-floor houses (`ANDARES`) and the roof (`ROOF`) are not ported: the crowd walks one
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CorpseLedger } from '@adatechnology/game-characters';
2
+ import { BlastShoveOptions } from '@adatechnology/game-contracts';
2
3
 
3
4
  var numbers = {
4
5
  capacity: 6000,
@@ -178,6 +179,202 @@ declare function createDoors(grid: NavGrid, options: {
178
179
  numbers?: Partial<DoorNumbers>;
179
180
  }): Doors;
180
181
 
182
+ /**
183
+ * `shared`: one `SharedArrayBuffer` frame and a control word, the worker computes while the main thread runs the rest of the step; needs `crossOriginIsolated` in a browser.
184
+ * `transfer`: the frame goes by `postMessage` with its buffer transferred; no isolation needed, but the answer arrives whenever the event loop delivers it.
185
+ * `inline`: the same protocol and the same kernel on the calling thread, one step of latency as well; the reference the other two must match.
186
+ */
187
+ type OffloadTransport = 'shared' | 'transfer' | 'inline';
188
+ /** A worker as the runner sees it: browsers and node differ in how a message arrives, the factory hides that. */
189
+ type PhysicsWorkerHandle = {
190
+ postMessage(message: unknown, transfer?: ArrayBuffer[]): void;
191
+ onMessage(listener: (data: unknown) => void): void;
192
+ terminate(): void;
193
+ };
194
+ type PhysicsWorkerFactory = () => PhysicsWorkerHandle;
195
+
196
+ type CrowdContactOptions = {
197
+ /** Body radius over build (default 0.25 → 0.5 m between two average bodies; the separation keeps them at 0.78 m when it can). */
198
+ readonly radius?: number;
199
+ /** Iterations per fixed step in band R0 (default 2; 3 is the ceiling). */
200
+ readonly iterations?: 1 | 2 | 3;
201
+ /** Push velocity damping per second: push ← push·exp(−friction·h) (default 6 → λ = 0.905 at 60 Hz). */
202
+ readonly friction?: number;
203
+ /** Ceiling of the push velocity, m/s (default 3). */
204
+ readonly maxPush?: number;
205
+ /** Ceiling of the position correction of one agent in one iteration, m (default 0.1; the design started at 0.06). */
206
+ readonly maxCorrection?: number;
207
+ /** Jacobi relaxation of the position correction (default 1; the design started at 0.5, which left 0.25 m overlaps in a crush of 800). */
208
+ readonly relaxation?: number;
209
+ /** kg of a body of height 1 and build 1 (default 70). */
210
+ readonly baseMass?: number;
211
+ /** Ceiling of the separation term `|Σ sep · separationPush|`, m/s (default 1.5). */
212
+ readonly separationMax?: number;
213
+ };
214
+ type CrowdPressureOptions = {
215
+ /** Share kept per hop, τ (default 0.97). */
216
+ readonly transmission?: number;
217
+ /** Ceiling of the field per agent, N (default 6000). */
218
+ readonly cap?: number;
219
+ /** Cosine of the half angle that counts as "in front" (default 0.5 → 60°). */
220
+ readonly frontCone?: number;
221
+ /** Share of the will speed of a staggering agent (default 0.35); the field keeps the full drive. */
222
+ readonly staggerSpeed?: number;
223
+ };
224
+ type CrowdBalanceOptions = {
225
+ /** `P_crit = criticalFactor · m · g · stability` (default 2.0 → 1372 N for a walker). */
226
+ readonly criticalFactor?: number;
227
+ /** P/P_crit to start staggering (default 0.7). */
228
+ readonly staggerEnter?: number;
229
+ /** P/P_crit to stop, after `staggerMin` (default 0.4). */
230
+ readonly staggerLeave?: number;
231
+ /** Seconds (default 0.5). */
232
+ readonly staggerMin?: number;
233
+ /** 1/s, rate = fallRate·(P/P_crit − 1)² above 1 (default 1.5). */
234
+ readonly fallRate?: number;
235
+ /** |Δpush| in one step that staggers, m/s (default 1.2). */
236
+ readonly shoveStagger?: number;
237
+ /** |Δpush| that knocks down, m/s, times stability (default 2.5). */
238
+ readonly shoveFall?: number;
239
+ /** Seconds on the ground before trying to get up (default 1.5, plus reaction). */
240
+ readonly fallenMin?: number;
241
+ /** P/P_crit under which a fallen one may get up (default 0.3). */
242
+ readonly releaseFactor?: number;
243
+ /** Default 1.2. */
244
+ readonly getUpSeconds?: number;
245
+ /** Seconds of the exponential mean (default 0.3). */
246
+ readonly pressureTau?: number;
247
+ /** Ceiling of new falls per fixed step (default 8). */
248
+ readonly maxFallsPerStep?: number;
249
+ /** Ceiling of agents on the ground (default 300); above it a fall becomes a stagger. */
250
+ readonly maxFallen?: number;
251
+ /** Damage per second to a fallen body per 70 kg standing on it (default 6). */
252
+ readonly trampleDps?: number;
253
+ };
254
+ type CrowdMoundOptions = {
255
+ /** m per cell of a lying body, times build (default 0.28). */
256
+ readonly bodyThickness?: number;
257
+ /** Default 0.25. */
258
+ readonly corpseThickness?: number;
259
+ /** Ceiling of the mound per cell, m (default 1.6, about 5 or 6 bodies). */
260
+ readonly maxHeight?: number;
261
+ /** Corpses within this distance of the band centre count (default 35, band R0). */
262
+ readonly corpseRadius?: number;
263
+ };
264
+ type CrowdThrownOptions = {
265
+ /** Slots of the pool (default 32, at most 127). */
266
+ readonly pool?: number;
267
+ /** Only within this distance of the band centre (default 30 m). */
268
+ readonly radius?: number;
269
+ /** m around the landing point that is knocked (default 1.2: a body at 10 m/s knocks the ones within about two body widths; 0.7 left the chain beyond a grenade at 0 in 4 of 12 scenes). */
270
+ readonly landingRadius?: number;
271
+ /** Damage per m/s of landing speed above 6 m/s (default 8). */
272
+ readonly fallDamage?: number;
273
+ /** Who gets a slot when there are more candidates than slots: half the slots to the fastest and half to the nearest to the band centre (`'mixed'`, default), or only one criterion. */
274
+ readonly priority?: 'mixed' | 'fastest' | 'nearest';
275
+ /** Share of the slots that goes to the fastest in `'mixed'` (default 0.5). */
276
+ readonly mixedRatio?: number;
277
+ };
278
+ type CrowdBlastOptions = {
279
+ /** Damage at the centre per kJ before falloff (default 0.6 → m67 120 points). */
280
+ readonly pointsPerKilojoule?: number;
281
+ /** Shove speed from which a body flies, m/s (default 4). */
282
+ readonly throwSpeed?: number;
283
+ /** Blasts resolved in one fixed step; more wait for the next one, in order (default 8). */
284
+ readonly maxBlastsPerStep?: number;
285
+ /** `limb` effects per blast; the rest is summed into one blood effect (default 24). */
286
+ readonly limbEffectsPerBlast?: number;
287
+ /** `blood` effects per blast (default 32). */
288
+ readonly bloodEffectsPerBlast?: number;
289
+ /** The shove rule of `game-contracts`, the same numbers the host lends to the items and the furniture. */
290
+ readonly shove?: BlastShoveOptions;
291
+ };
292
+ type CrowdPersonalityOptions = {
293
+ /** Chase and wander factor (default [0.9, 1.1]). */
294
+ readonly speed?: readonly [number, number];
295
+ /** Seconds added to getting up and to the alert hop (default [0.05, 0.35]). */
296
+ readonly reaction?: readonly [number, number];
297
+ readonly stability?: readonly [number, number];
298
+ readonly trip?: readonly [number, number];
299
+ /** Pushing will (default [0.7, 1.3]). */
300
+ readonly drive?: readonly [number, number];
301
+ /** Walk cadence factor (default [0.85, 1.15]). */
302
+ readonly cadence?: readonly [number, number];
303
+ /** Attack will (default [0.8, 1.2]): divides `attackEvery`, multiplies the bite chance and decides who gets the last attack slots. */
304
+ readonly aggression?: readonly [number, number];
305
+ /** Weights of sway, shuffle, head twitch, still (default [3, 2, 2, 1]). */
306
+ readonly idle?: readonly number[];
307
+ };
308
+ /** Band R1, summarised (D10, `design-chain-horde.md` §9): no pairs, a coarse grid of bodies per cell. */
309
+ type CrowdDensityOptions = {
310
+ /** Side of a coarse cell, m (default 2). */
311
+ readonly cell?: number;
312
+ /** Bodies per cell that walk freely (default 4, one per square metre); above it the speed is `capacity / bodies`. */
313
+ readonly capacity?: number;
314
+ /** N of pressure per body above `capacity` (default 150). */
315
+ readonly pressurePerBody?: number;
316
+ };
317
+ /** Pressure of the horde on the target (D13.2): above `knockThreshold` N it knocks the target down; the excess crushes at `crushDamagePerSecond`. */
318
+ type CrowdPlayerPressureOptions = {
319
+ readonly enabled?: boolean;
320
+ readonly knockThreshold?: number;
321
+ readonly crushDamagePerSecond?: number;
322
+ };
323
+ type CrowdPhysicsOptions = {
324
+ readonly contact?: CrowdContactOptions | boolean;
325
+ /** Needs `contact`. */
326
+ readonly pressure?: CrowdPressureOptions | boolean;
327
+ /** Needs `contact`. */
328
+ readonly balance?: CrowdBalanceOptions | boolean;
329
+ /** Needs `balance`. */
330
+ readonly mound?: CrowdMoundOptions | boolean;
331
+ /** Needs `balance`. */
332
+ readonly thrown?: CrowdThrownOptions | boolean;
333
+ /** Needs `balance`. The far band without pairs: speed and pressure from the bodies per coarse cell. */
334
+ readonly density?: CrowdDensityOptions | boolean;
335
+ readonly blast?: CrowdBlastOptions;
336
+ readonly personality?: CrowdPersonalityOptions | boolean;
337
+ readonly door?: {
338
+ readonly pressureDamage?: number;
339
+ };
340
+ /** Needs `pressure`. Off in the package; a game turns it on. */
341
+ readonly playerPressure?: CrowdPlayerPressureOptions;
342
+ /** Flow budget in cells per step, `attackPose` in the simulation, positions kept for interpolation. */
343
+ readonly deterministic?: boolean;
344
+ /**
345
+ * Where the contact and the pressure run. `'off'` (default): in the step, exactly as before. `'on'`: in a worker, one step behind (the main thread applies what the worker
346
+ * computed from the previous step), and an error when no worker can be made. `'auto'`: the same when a worker can be made, `'off'` when it cannot. Needs `contact`.
347
+ */
348
+ readonly worker?: 'off' | 'auto' | 'on';
349
+ /** Makes the worker (default: a module worker that loads `physics-worker.js` next to the bundle). A host that bundles its own worker gives the factory and calls `attachPhysicsWorker` in it. */
350
+ readonly workerFactory?: PhysicsWorkerFactory;
351
+ /** `'shared'` (a `SharedArrayBuffer`, deterministic, needs `crossOriginIsolated`), `'transfer'` (`postMessage`, any host, the answer is as late as the event loop makes it), `'inline'` (no worker: the same protocol on the main thread, the reference). Default: `'shared'` when it is available, else `'transfer'`. */
352
+ readonly workerTransport?: OffloadTransport;
353
+ /** Steps between the state the worker is handed and the step that applies its answer (default 1). 2 gives the worker a whole step to finish in, so the main thread never waits for it, and costs one more step of lag in the contact. */
354
+ readonly workerLatency?: 1 | 2;
355
+ };
356
+ /** Everything on, deterministic, the numbers of the design (`design-chain-horde.md`); the player pressure stays off, a game turns it on. */
357
+ declare const CROWD_PHYSICS_DEFAULTS: CrowdPhysicsOptions;
358
+ type ResolvedPhysics = {
359
+ readonly contact: Required<CrowdContactOptions> | null;
360
+ readonly pressure: Required<CrowdPressureOptions> | null;
361
+ readonly balance: Required<CrowdBalanceOptions> | null;
362
+ readonly mound: Required<CrowdMoundOptions> | null;
363
+ readonly thrown: Required<CrowdThrownOptions> | null;
364
+ readonly density: Required<CrowdDensityOptions> | null;
365
+ readonly blast: Required<CrowdBlastOptions>;
366
+ readonly personality: Required<CrowdPersonalityOptions> | null;
367
+ readonly doorPressureDamage: number;
368
+ readonly playerPressure: Required<CrowdPlayerPressureOptions>;
369
+ readonly deterministic: boolean;
370
+ readonly worker: 'off' | 'auto' | 'on';
371
+ readonly workerFactory: PhysicsWorkerFactory | undefined;
372
+ readonly workerTransport: OffloadTransport | undefined;
373
+ readonly workerLatency: 1 | 2;
374
+ };
375
+ /** Reads `CrowdOptions.physics`: the defaults filled in, and a thrown error that lists every part whose dependency is missing. */
376
+ declare function resolvePhysics(physics?: CrowdPhysicsOptions): ResolvedPhysics;
377
+
181
378
  /**
182
379
  * The agents as a struct of arrays (G 66717 `Z`, ZMAX 6000): one typed array per field, the same types as the game so the floats round the same way.
183
380
  * The names are the game's in English; `armHp` and `legHp` hold two values per agent (left, right).
@@ -240,10 +437,40 @@ declare const createStore: (capacity: number) => {
240
437
  variant: Uint8Array<ArrayBuffer>;
241
438
  attackPose: Uint8Array<ArrayBuffer>;
242
439
  arms: Float32Array<ArrayBuffer>;
440
+ stepDir: Int32Array<ArrayBuffer>;
441
+ pushX: Float32Array<ArrayBuffer>;
442
+ pushZ: Float32Array<ArrayBuffer>;
443
+ mass: Float32Array<ArrayBuffer>;
444
+ pressure: Float32Array<ArrayBuffer>;
445
+ field: Float32Array<ArrayBuffer>;
446
+ fieldPrev: Float32Array<ArrayBuffer>;
447
+ aimX: Float32Array<ArrayBuffer>;
448
+ aimZ: Float32Array<ArrayBuffer>;
449
+ directShove: Float32Array<ArrayBuffer>;
450
+ blastStep: Int32Array<ArrayBuffer>;
451
+ balance: Uint8Array<ArrayBuffer>;
452
+ balanceTimer: Float32Array<ArrayBuffer>;
453
+ fallCause: Uint8Array<ArrayBuffer>;
454
+ identity: Uint32Array<ArrayBuffer>;
455
+ stability: Float32Array<ArrayBuffer>;
456
+ tripFactor: Float32Array<ArrayBuffer>;
457
+ driveFactor: Float32Array<ArrayBuffer>;
458
+ reaction: Float32Array<ArrayBuffer>;
459
+ cadenceFactor: Float32Array<ArrayBuffer>;
460
+ aggression: Float32Array<ArrayBuffer>;
461
+ idle: Uint8Array<ArrayBuffer>;
462
+ gaitStyle: Uint8Array<ArrayBuffer>;
463
+ wounds: Uint16Array<ArrayBuffer>;
464
+ leanX: Float32Array<ArrayBuffer>;
465
+ leanZ: Float32Array<ArrayBuffer>;
466
+ thrown: Int8Array<ArrayBuffer>;
467
+ prevX: Float32Array<ArrayBuffer>;
468
+ prevZ: Float32Array<ArrayBuffer>;
469
+ prevY: Float32Array<ArrayBuffer>;
243
470
  };
244
471
  type CrowdStore = ReturnType<typeof createStore>;
245
472
  /** The one-per-agent fields (G 66733 `FIELDS`): what a removal copies from the last agent. */
246
- declare const AGENT_FIELDS: readonly ["x", "z", "vx", "vz", "yaw", "phase", "type", "age", "outfit", "height", "build", "chaseSpeed", "wanderSpeed", "hp", "hpMax", "headHp", "missing", "helmet", "state", "stateTimer", "lost", "attackTimer", "band", "next", "wx", "wz", "crawl", "limp", "color", "skin", "alertIn", "cooldown", "y", "floor", "floorLocked", "vy", "abilityTimer", "eaten", "stuck", "stepCell", "burn", "hunts", "named", "dvx", "dvz", "steering", "renderYaw", "renderSpeed", "pause", "lod", "variant", "attackPose", "arms"];
473
+ declare const AGENT_FIELDS: readonly ["x", "z", "vx", "vz", "yaw", "phase", "type", "age", "outfit", "height", "build", "chaseSpeed", "wanderSpeed", "hp", "hpMax", "headHp", "missing", "helmet", "state", "stateTimer", "lost", "attackTimer", "band", "next", "wx", "wz", "crawl", "limp", "color", "skin", "alertIn", "cooldown", "y", "floor", "floorLocked", "vy", "abilityTimer", "eaten", "stuck", "stepCell", "burn", "hunts", "named", "dvx", "dvz", "steering", "renderYaw", "renderSpeed", "pause", "lod", "variant", "attackPose", "arms", "stepDir", "pushX", "pushZ", "mass", "pressure", "field", "fieldPrev", "aimX", "aimZ", "directShove", "blastStep", "balance", "balanceTimer", "fallCause", "identity", "stability", "tripFactor", "driveFactor", "reaction", "cadenceFactor", "aggression", "idle", "gaitStyle", "wounds", "leanX", "leanZ", "thrown", "prevX", "prevZ", "prevY"];
247
474
 
248
475
  /** What an agent is doing (G 66724 and 67180-67183): the store keeps the number. */
249
476
  declare const STATE: {
@@ -260,6 +487,24 @@ declare const STATE: {
260
487
  readonly PILE_STEP: 10;
261
488
  readonly SEEKING_SOUND: 12;
262
489
  };
490
+ /** Balance of an agent (physics): a state machine of its own, apart from `STATE`, so the game's behaviour machine stays as it is. */
491
+ declare const BALANCE: {
492
+ readonly STANDING: 0;
493
+ readonly STAGGERING: 1;
494
+ readonly FALLEN: 2;
495
+ readonly GETTING_UP: 3;
496
+ readonly THROWN: 4;
497
+ };
498
+ /** Why an agent went down (index `cause - 1` in `stats.physics.falls`). */
499
+ declare const FALL_CAUSE: {
500
+ readonly NONE: 0;
501
+ readonly PRESSURE: 1;
502
+ readonly SHOVE: 2;
503
+ readonly TRIP: 3;
504
+ readonly LANDING: 4;
505
+ readonly COLLAPSE: 5;
506
+ readonly BLAST: 6;
507
+ };
263
508
  /** Body regions of a hit (G 67012): 0 head, 1 torso, 2 left arm, 3 right arm, 4 left leg, 5 right leg. */
264
509
  type Region = 0 | 1 | 2 | 3 | 4 | 5;
265
510
  /**
@@ -436,6 +681,31 @@ type CrowdType = {
436
681
  readonly variants?: number;
437
682
  readonly eye?: number;
438
683
  readonly traits?: CrowdTraits;
684
+ /** How the body takes a push (physics only; a type without it uses the defaults). */
685
+ readonly balance?: CrowdBalanceTraits;
686
+ };
687
+ type CrowdBalanceTraits = {
688
+ /** Multiplies the critical pressure (default 1). */
689
+ readonly stability?: number;
690
+ /** Chance of tripping when entering a cell with a body (default 0.04). */
691
+ readonly trip?: number;
692
+ /** Force of its will against what is in front, in N (default 250). */
693
+ readonly driveForce?: number;
694
+ /** False: already on the ground (a crawler). */
695
+ readonly canFall?: boolean;
696
+ };
697
+ /** A blast as the crowd reads it. */
698
+ type CrowdBlast = {
699
+ readonly point: readonly [number, number, number];
700
+ /** J. */
701
+ readonly energy: number;
702
+ /** m, how far it hurts. */
703
+ readonly radius: number;
704
+ /** m, everything inside dies. */
705
+ readonly lethalRadius?: number;
706
+ readonly source?: string;
707
+ /** Id of the chain this blast belongs to (game-destruction `chain`), for the counters. */
708
+ readonly chain?: string;
439
709
  };
440
710
  /** The age of a body (G IDADES): its share, and what it does to speed, health, limbs and senses. */
441
711
  type CrowdAge = {
@@ -570,20 +840,24 @@ type CrowdEventMap = {
570
840
  x: number;
571
841
  z: number;
572
842
  };
843
+ fall: {
844
+ agent: number;
845
+ cause: number;
846
+ x: number;
847
+ z: number;
848
+ identity: number;
849
+ };
850
+ blasted: {
851
+ point: [number, number, number];
852
+ radius: number;
853
+ reached: number;
854
+ killed: number;
855
+ thrown: number;
856
+ fallen: number;
857
+ chain?: string;
858
+ };
573
859
  };
574
860
  type CrowdEventName = keyof CrowdEventMap;
575
-
576
- type CrowdNumbers = typeof defaults.numbers;
577
- type CrowdBand = {
578
- readonly max: number;
579
- readonly dt: number;
580
- };
581
- /** The game's numbers; `bands[3].max` is `Infinity` (JSON keeps it as null). */
582
- declare const CROWD_DEFAULTS: {
583
- readonly sources: Readonly<Record<string, string>>;
584
- readonly bands: readonly CrowdBand[];
585
- readonly numbers: CrowdNumbers;
586
- };
587
861
  /** A body on the ground (G 67062 `CP`). */
588
862
  type Corpse = {
589
863
  readonly x: number;
@@ -599,6 +873,116 @@ type Corpse = {
599
873
  readonly seed: number;
600
874
  readonly named: number;
601
875
  };
876
+
877
+ /** Counters of the physics, all zero with it off (`crowd.stats.physics`). */
878
+ type PhysicsStats = {
879
+ contacts: number;
880
+ maxOverlap: number;
881
+ meanOverlap: number;
882
+ /** Falls by cause: pressure, shove, trip, landing, collapse, blast. */
883
+ falls: [pressure: number, shove: number, trip: number, landing: number, collapse: number, blast: number];
884
+ /** Chain falls: shove, landing, collapse or trip within 240 steps of a blast, of an agent whose own direct shove from it was below `shoveStagger`. */
885
+ fallsBeyondBlast: number;
886
+ fallen: number;
887
+ staggering: number;
888
+ climbers: number;
889
+ moundMax: number;
890
+ thrownActive: number;
891
+ thrownPeak: number;
892
+ blastChecks: number;
893
+ chainLinks: number;
894
+ /** The highest field of the step, N: the front of a column, or the funnel of a door. */
895
+ fieldMax: number;
896
+ /** The pressure of the horde on the target, N (exponential mean). */
897
+ playerPressure: number;
898
+ };
899
+
900
+ type ContactParams = Required<CrowdContactOptions> & {
901
+ readonly h: number;
902
+ };
903
+ type ContactResult = {
904
+ contacts: number;
905
+ maxOverlap: number;
906
+ overlapSum: number;
907
+ };
908
+ type ContactWorkspace = {
909
+ readonly head: Int32Array;
910
+ readonly next: Int32Array;
911
+ readonly touched: Int32Array;
912
+ touchedCount: number;
913
+ readonly pairI: Int32Array;
914
+ readonly pairJ: Int32Array;
915
+ pairCount: number;
916
+ readonly deltaX: Float32Array;
917
+ readonly deltaZ: Float32Array;
918
+ readonly count: Uint8Array;
919
+ readonly pushBeforeX: Float32Array;
920
+ readonly pushBeforeZ: Float32Array;
921
+ /** |Δpush| the contact gave to each agent in the last step. */
922
+ readonly shove: Float32Array;
923
+ /** 1 for the agents that take part in the contact this step (`takesPartInContact`), read once. */
924
+ readonly takesPart: Uint8Array;
925
+ /** The radius and the effective mass of each agent this step, in doubles: the balance does not change inside the contact step. */
926
+ readonly radiusOf: Float64Array;
927
+ readonly massOf: Float64Array;
928
+ /** Per pair, valid for the positions of the current iteration: the reach, the distance, and the normal (only meaningful when the distance is under the reach) from `j` to `i`. */
929
+ readonly pairReach: Float64Array;
930
+ readonly pairDistance: Float64Array;
931
+ readonly pairNormalX: Float64Array;
932
+ readonly pairNormalZ: Float64Array;
933
+ };
934
+ /** The arrays the contact and the pressure read and write: the step needs no more of the store, so it can run over a copy of them (a worker). */
935
+ type PhysicsStore = Pick<CrowdStore, 'x' | 'z' | 'y' | 'vx' | 'vz' | 'pushX' | 'pushZ' | 'mass' | 'build' | 'balance' | 'state' | 'band' | 'identity' | 'aimX' | 'aimZ' | 'field' | 'fieldPrev' | 'type' | 'driveFactor'>;
936
+ type PhysicsGrid = Pick<NavGrid, 'size' | 'half' | 'occ' | 'cellOf'>;
937
+ type ContactView = {
938
+ n: number;
939
+ readonly store: PhysicsStore;
940
+ readonly grid: PhysicsGrid;
941
+ readonly workspace: ContactWorkspace;
942
+ };
943
+ declare const createContactView: (store: PhysicsStore, grid: PhysicsGrid) => ContactView;
944
+ /**
945
+ * One fixed step of contact in band R0 (`design-chain-horde.md` §4.3): `iterations` rounds of a symmetric Gauss-Seidel impulse (the pairs forward, then backward, so
946
+ * a low index has no advantage) and a Jacobi position correction (the mean of what each agent was asked to move), then the damping of the push.
947
+ * It resolves overlap and shock only: pressure is the field of `pressure.ts`. An inelastic impulse never adds energy, and the correction never touches the velocity.
948
+ */
949
+ declare function contactStep(view: ContactView, params: ContactParams, out: ContactResult): void;
950
+
951
+ /** The thrown bodies, simulated here and only read by the renderer (`x`, `y`, `z` are the centre of the body, `prev*` the position of the step before). */
952
+ type ThrownPool = {
953
+ readonly size: number;
954
+ /** An agent index, `CARRIED_BODY` for a dead body carried until it lands, `FREE_SLOT` for an empty slot. */
955
+ readonly agent: Int32Array;
956
+ readonly identity: Uint32Array;
957
+ readonly corpse: (Corpse | null)[];
958
+ readonly x: Float32Array;
959
+ readonly y: Float32Array;
960
+ readonly z: Float32Array;
961
+ readonly prevX: Float32Array;
962
+ readonly prevY: Float32Array;
963
+ readonly prevZ: Float32Array;
964
+ readonly vx: Float32Array;
965
+ readonly vy: Float32Array;
966
+ readonly vz: Float32Array;
967
+ readonly pitch: Float32Array;
968
+ readonly roll: Float32Array;
969
+ readonly spinPitch: Float32Array;
970
+ readonly spinRoll: Float32Array;
971
+ active(): number;
972
+ };
973
+
974
+ type CrowdNumbers = typeof defaults.numbers;
975
+ type CrowdBand = {
976
+ readonly max: number;
977
+ readonly dt: number;
978
+ };
979
+ /** The game's numbers; `bands[3].max` is `Infinity` (JSON keeps it as null). */
980
+ declare const CROWD_DEFAULTS: {
981
+ readonly sources: Readonly<Record<string, string>>;
982
+ readonly bands: readonly CrowdBand[];
983
+ readonly numbers: CrowdNumbers;
984
+ };
985
+
602
986
  /** A custom move: runs on every decision tick after the timed states and before the moves on the target; `true` means it took care of the movement. */
603
987
  type CrowdPlugin = {
604
988
  readonly id: string;
@@ -635,10 +1019,13 @@ type CrowdOptions = {
635
1019
  /** Flow field: half window in cells (192), and the clock its time budget reads (default `performance.now`; pass `() => 0` to finish every field in one frame). */
636
1020
  readonly flow?: {
637
1021
  readonly radius?: number;
638
- readonly clock?: () => number;
1022
+ readonly clock?: () => number; /** Flow budget in cells per step instead of the clock: the same field at any frame rate (`physics.deterministic` sets 20000). */
1023
+ readonly cellsPerStep?: number;
639
1024
  };
640
1025
  readonly plugins?: readonly CrowdPlugin[];
641
1026
  readonly target?: CrowdTarget | null;
1027
+ /** Contact, pressure, balance, mounds, thrown bodies and personality: all off by default, and then the horde is the game's, bit for bit. */
1028
+ readonly physics?: CrowdPhysicsOptions;
642
1029
  };
643
1030
  type CrowdView = {
644
1031
  readonly x: number;
@@ -654,6 +1041,8 @@ type CrowdUpdateContext = {
654
1041
  readonly flowTarget?: readonly [number, number];
655
1042
  /** Where the viewer looks (yaw: forward is `(-sin, -cos)`); the stalker wakes when the target turns its back. Default: the target's view. */
656
1043
  readonly viewYaw?: number;
1044
+ /** Centre of the relevance bands, for scenes without a player near the agents (tests, benches, cutscenes). Default: the target, or `flowTarget` without one. */
1045
+ readonly bandCentre?: readonly [number, number];
657
1046
  };
658
1047
  type Crowd = {
659
1048
  readonly store: CrowdStore;
@@ -667,9 +1056,16 @@ type Crowd = {
667
1056
  slots: number;
668
1057
  kills: number;
669
1058
  heads: number;
1059
+ physics: PhysicsStats;
670
1060
  };
671
1061
  /** Height of the bodies piled per grid cell. */
672
1062
  readonly pile: Float32Array;
1063
+ /** Millimetres of lying bodies per navigation cell, recomputed every fixed step (empty with `physics.mound` off). */
1064
+ readonly mound: Uint16Array;
1065
+ /** The thrown bodies of a blast, simulated here and only drawn by the renderer (empty with `physics.thrown` off). */
1066
+ readonly thrown: ThrownPool;
1067
+ /** A blast: queued, and resolved at the start of the next step in one pass over the agents. Without `physics` it only hurts, kills and stuns. */
1068
+ blast(blast: CrowdBlast): void;
673
1069
  /** How many agents are steps of a pile now. */
674
1070
  pileCount(): number;
675
1071
  readonly trail: {
@@ -690,6 +1086,7 @@ type Crowd = {
690
1086
  spawn(x: number, z: number, type?: number): number;
691
1087
  remove(i: number): void;
692
1088
  update(dt: number, context?: CrowdUpdateContext): void;
1089
+ dispose(): void;
693
1090
  emitNoise(x: number, z: number, radius: number): void;
694
1091
  damage(i: number, hit: CrowdHit): CrowdDamageResult;
695
1092
  kill(i: number, headshot?: boolean): void;
@@ -1125,4 +1522,25 @@ type InfectionBinding = {
1125
1522
  */
1126
1523
  declare function bindInfection(crowd: Crowd, model: InfectionModelLike, options: InfectionBindingOptions): InfectionBinding;
1127
1524
 
1128
- export { AGENT_FIELDS, CELL, CIVILIAN_DEFAULTS, CROWD_DEFAULTS, type Civilian, type CivilianBuilding, type CivilianEventMap, type CivilianEventName, type CivilianNumbers, type CivilianOptions, type CivilianSpec, type CivilianState, type Civilians, type Corpse, type Crowd, type CrowdAge, type CrowdBand, type CrowdCaliber, type CrowdDamageResult, type CrowdEffectId, type CrowdEventMap, type CrowdEventName, type CrowdHit, type CrowdMessageId, type CrowdNumbers, type CrowdOptions, type CrowdOutfit, type CrowdPack, type CrowdPlugin, type CrowdSoundId, type CrowdStore, type CrowdTarget, type CrowdTraits, type CrowdType, type CrowdUpdateContext, type CrowdView, DOOR_DEFAULTS, type Door, type DoorEventMap, type DoorEventName, type DoorNumbers, type Doors, type Flock, type FlockBird, type FlockMode, type FlockOptions, HERD_DEFAULTS, HERD_ROLES, type Herd, type HerdAnimal, type HerdAnimalSpec, type HerdEventMap, type HerdEventName, type HerdNumbers, type HerdOptions, type HerdRole, type InfectionBinding, type InfectionBindingOptions, type InfectionModelLike, MISSING, type MigrationOptions, type NavGrid, type NavGridOptions, type PlayerCrowdLike, type Region, STATE, bindInfection, carrionCenter, createCivilians, createCrowd, createDoors, createFlock, createHerd, createNavGrid, createRng, createStore };
1525
+ /** murmur3 fmix32: a full avalanche of the 32 bits. */
1526
+ declare const mix32: (value: number) => number;
1527
+ /**
1528
+ * 0..1 from an identity, a step and a salt: the counter-based draw every physics feature uses (never the shared `random` stream, whose order is the game's).
1529
+ * The salt goes in before the step so two different `(step, salt)` pairs never share an input.
1530
+ */
1531
+ declare const unit: (identity: number, step: number, salt: number) => number;
1532
+ /** The identity of the `serial`-th agent born into a crowd seeded with `seed`. */
1533
+ declare const identityOf: (seed: number, serial: number) => number;
1534
+
1535
+ /** What a worker thread has to offer: browsers and node give it different shapes, so the host adapts them. */
1536
+ type WorkerPort = {
1537
+ postMessage(message: unknown, transfer?: ArrayBuffer[]): void;
1538
+ onMessage(listener: (data: unknown) => void): void;
1539
+ };
1540
+ /**
1541
+ * The worker side: builds the kernel on `init`, then either loops on the shared control word (shared transport) or answers each transferred frame.
1542
+ * A host that loads the worker its own way (a bundler, node `worker_threads`) calls this with its port.
1543
+ */
1544
+ declare function attachPhysicsWorker(port: WorkerPort): void;
1545
+
1546
+ export { AGENT_FIELDS, BALANCE, CELL, CIVILIAN_DEFAULTS, CROWD_DEFAULTS, CROWD_PHYSICS_DEFAULTS, type Civilian, type CivilianBuilding, type CivilianEventMap, type CivilianEventName, type CivilianNumbers, type CivilianOptions, type CivilianSpec, type CivilianState, type Civilians, type ContactParams, type ContactResult, type ContactView, type Corpse, type Crowd, type CrowdAge, type CrowdBalanceOptions, type CrowdBalanceTraits, type CrowdBand, type CrowdBlast, type CrowdBlastOptions, type CrowdCaliber, type CrowdContactOptions, type CrowdDamageResult, type CrowdDensityOptions, type CrowdEffectId, type CrowdEventMap, type CrowdEventName, type CrowdHit, type CrowdMessageId, type CrowdMoundOptions, type CrowdNumbers, type CrowdOptions, type CrowdOutfit, type CrowdPack, type CrowdPersonalityOptions, type CrowdPhysicsOptions, type CrowdPlayerPressureOptions, type CrowdPlugin, type CrowdPressureOptions, type CrowdSoundId, type CrowdStore, type CrowdTarget, type CrowdThrownOptions, type CrowdTraits, type CrowdType, type CrowdUpdateContext, type CrowdView, DOOR_DEFAULTS, type Door, type DoorEventMap, type DoorEventName, type DoorNumbers, type Doors, FALL_CAUSE, type Flock, type FlockBird, type FlockMode, type FlockOptions, HERD_DEFAULTS, HERD_ROLES, type Herd, type HerdAnimal, type HerdAnimalSpec, type HerdEventMap, type HerdEventName, type HerdNumbers, type HerdOptions, type HerdRole, type InfectionBinding, type InfectionBindingOptions, type InfectionModelLike, MISSING, type MigrationOptions, type NavGrid, type NavGridOptions, type OffloadTransport, type PhysicsStats, type PhysicsWorkerFactory, type PhysicsWorkerHandle, type PlayerCrowdLike, type Region, type ResolvedPhysics, STATE, type ThrownPool, type WorkerPort, attachPhysicsWorker, bindInfection, carrionCenter, contactStep, createCivilians, createContactView, createCrowd, createDoors, createFlock, createHerd, createNavGrid, createRng, createStore, identityOf, mix32, resolvePhysics, unit };