@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/dist/index.js CHANGED
@@ -161,6 +161,11 @@ function createDoors(grid, options) {
161
161
 
162
162
  // src/grid.ts
163
163
  var CELL = { FREE: 0, SOLID: 1, LOW_WALL: 2, OPEN_DOOR: 3, GLASS: 4, CLOSED_DOOR: 5 };
164
+ var createCellOf = (half, cell, n) => (x, z) => {
165
+ const gx = Math.floor((x + half) / cell);
166
+ const gz = Math.floor((z + half) / cell);
167
+ return gx < 0 || gz < 0 || gx >= n || gz >= n ? -1 : gz * n + gx;
168
+ };
164
169
  var BORDER_WIDTH = 1;
165
170
  var BORDER_HEIGHT = 50;
166
171
  var LOS_STEP = 0.5;
@@ -172,11 +177,7 @@ function createNavGrid(options = {}) {
172
177
  const n = size / cell;
173
178
  const occ = new Uint8Array(n * n);
174
179
  const occH = new Float32Array(n * n);
175
- const cellOf = (x, z) => {
176
- const gx = Math.floor((x + half) / cell);
177
- const gz = Math.floor((z + half) / cell);
178
- return gx < 0 || gz < 0 || gx >= n || gz >= n ? -1 : gz * n + gx;
179
- };
180
+ const cellOf = createCellOf(half, cell, n);
180
181
  const solidAt = (x, z, y = 0.5) => {
181
182
  const c = cellOf(x, z);
182
183
  return c < 0 || occ[c] !== 0 && occ[c] !== 3 && occH[c] > y;
@@ -230,6 +231,1859 @@ function createNavGrid(options = {}) {
230
231
  };
231
232
  }
232
233
 
234
+ // src/identity.ts
235
+ var mix32 = (value) => {
236
+ let hash = value >>> 0;
237
+ hash ^= hash >>> 16;
238
+ hash = Math.imul(hash, 2246822507);
239
+ hash ^= hash >>> 13;
240
+ hash = Math.imul(hash, 3266489909);
241
+ hash ^= hash >>> 16;
242
+ return hash >>> 0;
243
+ };
244
+ var unit = (identity, step, salt) => (mix32(mix32(identity ^ salt) + Math.imul(step + 1, 2654435769) >>> 0) >>> 0) / 4294967296;
245
+ var identityOf = (seed, serial) => mix32(seed ^ mix32(serial));
246
+ var SALT = {
247
+ FALL: 62481,
248
+ TRIP: 29209,
249
+ LIMB: 110848,
250
+ REGION: 189968,
251
+ JAW: 239536,
252
+ CORPSE: 311518,
253
+ SPIN: 364922,
254
+ SPEED: 413981,
255
+ REACTION: 466972,
256
+ STABILITY: 569886,
257
+ TRIP_FACTOR: 605021,
258
+ DRIVE: 655571,
259
+ CADENCE: 728142,
260
+ AGGRESSION: 797335,
261
+ IDLE: 867848,
262
+ GAIT: 937753
263
+ };
264
+
265
+ // src/types.ts
266
+ var STATE = {
267
+ WANDER: 0,
268
+ CHASE: 1,
269
+ GRAB: 2,
270
+ STUNNED: 3,
271
+ CALLING: 4,
272
+ AIRBORNE: 5,
273
+ DORMANT: 6,
274
+ SWELLING: 7,
275
+ SWING: 8,
276
+ EATING: 9,
277
+ PILE_STEP: 10,
278
+ SEEKING_SOUND: 12
279
+ };
280
+ var BALANCE = { STANDING: 0, STAGGERING: 1, FALLEN: 2, GETTING_UP: 3, THROWN: 4 };
281
+ var FALL_CAUSE = { NONE: 0, PRESSURE: 1, SHOVE: 2, TRIP: 3, LANDING: 4, COLLAPSE: 5, BLAST: 6 };
282
+ var MISSING = { SKULL: 1, ARM_L: 2, ARM_R: 4, LEG_L: 8, LEG_R: 16, ARM_L_BROKEN: 32, ARM_R_BROKEN: 64, LEG_L_BROKEN: 128, LEG_R_BROKEN: 256, JAW: 512 };
283
+
284
+ // src/physics/balance.ts
285
+ var GRAVITY_FORCE = 9.8;
286
+ var GROUND_MASS_FACTOR = 3;
287
+ var TRIP_MIN_SPEED = 0.5;
288
+ var TRIP_REFERENCE_SPEED = 1.4;
289
+ var TRIP_MOUND = 150;
290
+ var TRIP_CLEARANCE = 0.1;
291
+ var COLLAPSE_STAGGER = 0.6;
292
+ var COLLAPSE_FALL = 1;
293
+ var CAUSE_ORDER = [FALL_CAUSE.PRESSURE, FALL_CAUSE.SHOVE, FALL_CAUSE.TRIP, FALL_CAUSE.LANDING, FALL_CAUSE.COLLAPSE, FALL_CAUSE.BLAST];
294
+ var CHAIN_WINDOW_STEPS = 240;
295
+ var CARRY_STEPS = 24;
296
+ var isCarried = (cause) => cause !== FALL_CAUSE.PRESSURE && cause !== FALL_CAUSE.TRIP;
297
+ function createBalance(ctx) {
298
+ const { store } = ctx;
299
+ const requests = [];
300
+ const balanceOptions = () => ctx.options.balance;
301
+ const stabilityOf = (i) => (ctx.typeOf(i).balance?.stability ?? 1) * store.stability[i];
302
+ const canFall = (i) => ctx.typeOf(i).balance?.canFall !== false;
303
+ const criticalOf = (i) => balanceOptions().criticalFactor * store.mass[i] * GRAVITY_FORCE * stabilityOf(i);
304
+ const requestFall = (i, cause, score) => {
305
+ if (!canFall(i)) return;
306
+ requests.push({ index: i, identity: store.identity[i], cause, score, born: ctx.step });
307
+ };
308
+ const stagger = (i) => {
309
+ if (store.balance[i] !== BALANCE.STANDING || !canFall(i)) return;
310
+ store.balance[i] = BALANCE.STAGGERING;
311
+ store.balanceTimer[i] = 0;
312
+ };
313
+ const shove = (i, speed, cause) => {
314
+ const options = balanceOptions();
315
+ const knockSpeed = options.shoveFall * stabilityOf(i);
316
+ if (store.balance[i] !== BALANCE.STANDING && store.balance[i] !== BALANCE.STAGGERING) return;
317
+ if (speed > knockSpeed) requestFall(i, cause, speed / knockSpeed);
318
+ else if (speed > options.shoveStagger) stagger(i);
319
+ };
320
+ const countsAsChainFall = (i, cause) => {
321
+ if (cause === FALL_CAUSE.PRESSURE || cause === FALL_CAUSE.BLAST) return false;
322
+ if (ctx.step - ctx.lastBlastStep > CHAIN_WINDOW_STEPS) return false;
323
+ const direct = store.blastStep[i] === ctx.lastBlastStep ? store.directShove[i] : 0;
324
+ return direct < balanceOptions().shoveStagger;
325
+ };
326
+ const goDown = (i, cause) => {
327
+ ctx.stats.falls[CAUSE_ORDER.indexOf(cause)]++;
328
+ if (countsAsChainFall(i, cause)) ctx.stats.fallsBeyondBlast++;
329
+ if (store.state[i] === STATE.PILE_STEP) ctx.releasePileStep(i);
330
+ store.balance[i] = BALANCE.FALLEN;
331
+ store.balanceTimer[i] = 0;
332
+ store.fallCause[i] = cause;
333
+ store.vx[i] = store.vz[i] = 0;
334
+ store.pushX[i] /= GROUND_MASS_FACTOR;
335
+ store.pushZ[i] /= GROUND_MASS_FACTOR;
336
+ store.aimX[i] = store.aimZ[i] = 0;
337
+ const target = ctx.target();
338
+ if (target && target.state.grabbedBy === i) target.release();
339
+ ctx.emit("fall", { agent: i, cause, x: store.x[i], z: store.z[i], identity: store.identity[i] });
340
+ };
341
+ const flush = () => {
342
+ if (!requests.length) return;
343
+ const options = balanceOptions();
344
+ requests.sort((a, b) => b.score - a.score || a.identity - b.identity);
345
+ let fallen = ctx.stats.fallen;
346
+ let fallsLeft = options.maxFallsPerStep;
347
+ const waiting = [];
348
+ for (const request of requests) {
349
+ const i = request.index;
350
+ if (i >= store.n || store.identity[i] !== request.identity) continue;
351
+ const balance = store.balance[i];
352
+ if (balance !== BALANCE.STANDING && balance !== BALANCE.STAGGERING) continue;
353
+ if (fallsLeft > 0 && fallen < options.maxFallen) {
354
+ goDown(i, request.cause);
355
+ fallsLeft--;
356
+ fallen++;
357
+ continue;
358
+ }
359
+ stagger(i);
360
+ if (isCarried(request.cause) && ctx.step - request.born < CARRY_STEPS) waiting.push(request);
361
+ }
362
+ requests.length = 0;
363
+ requests.push(...waiting);
364
+ };
365
+ const hasBodyOnTop = (i) => {
366
+ const cell = ctx.grid.cellOf(store.x[i], store.z[i]);
367
+ if (cell < 0) return false;
368
+ const options = ctx.options.mound;
369
+ if (!options) return false;
370
+ const top = store.y[i] + options.bodyThickness * store.build[i];
371
+ return ctx.moundAt(cell) / 1e3 > top + 0.05 || ctx.weightOn(i) > 0;
372
+ };
373
+ const stepStanding = (i, ratio, contactShove, options) => {
374
+ if (contactShove > options.shoveFall * stabilityOf(i)) return requestFall(i, FALL_CAUSE.SHOVE, contactShove / (options.shoveFall * stabilityOf(i)));
375
+ if (ratio > options.staggerEnter || contactShove > options.shoveStagger) stagger(i);
376
+ else tryTrip(i);
377
+ };
378
+ const tryTrip = (i) => {
379
+ const cell = ctx.grid.cellOf(store.x[i], store.z[i]);
380
+ if (cell < 0 || cell === ctx.grid.cellOf(store.prevX[i], store.prevZ[i])) return;
381
+ const mound = ctx.moundAt(cell);
382
+ if (mound < TRIP_MOUND || store.y[i] >= mound / 1e3 - TRIP_CLEARANCE) return;
383
+ const speed = Math.hypot(store.vx[i] + store.pushX[i], store.vz[i] + store.pushZ[i]);
384
+ if (speed <= TRIP_MIN_SPEED) return;
385
+ const chance = (ctx.typeOf(i).balance?.trip ?? 0.04) * store.tripFactor[i] * (speed / TRIP_REFERENCE_SPEED);
386
+ if (unit(store.identity[i], ctx.step, SALT.TRIP) < chance) requestFall(i, FALL_CAUSE.TRIP, 1);
387
+ };
388
+ const stepStaggering = (i, ratio, contactShove, options) => {
389
+ store.balanceTimer[i] += ctx.h;
390
+ if (contactShove > options.shoveFall * stabilityOf(i)) return requestFall(i, FALL_CAUSE.SHOVE, contactShove / (options.shoveFall * stabilityOf(i)));
391
+ if (ratio > 1 && unit(store.identity[i], ctx.step, SALT.FALL) < 1 - Math.exp(-options.fallRate * (ratio - 1) ** 2 * ctx.h)) return requestFall(i, FALL_CAUSE.PRESSURE, ratio);
392
+ if (ratio < options.staggerLeave && store.balanceTimer[i] >= options.staggerMin) {
393
+ store.balance[i] = BALANCE.STANDING;
394
+ store.balanceTimer[i] = 0;
395
+ }
396
+ };
397
+ const stepFallen = (i, ratio, options) => {
398
+ store.balanceTimer[i] += ctx.h;
399
+ if (store.balanceTimer[i] < options.fallenMin + store.reaction[i] || ratio >= options.releaseFactor || hasBodyOnTop(i)) return;
400
+ store.balance[i] = BALANCE.GETTING_UP;
401
+ store.balanceTimer[i] = 0;
402
+ };
403
+ const step = (options, contactShove) => {
404
+ const smoothing = 1 - Math.exp(-ctx.h / options.pressureTau);
405
+ let fallen = 0;
406
+ let staggering = 0;
407
+ for (let i = 0; i < store.n; i++) {
408
+ const state = store.balance[i];
409
+ if (state === BALANCE.THROWN) continue;
410
+ store.pressure[i] += (store.field[i] - store.pressure[i]) * smoothing;
411
+ const ratio = store.pressure[i] / criticalOf(i);
412
+ if (state === BALANCE.STANDING) stepStanding(i, ratio, contactShove[i], options);
413
+ else if (state === BALANCE.STAGGERING) stepStaggering(i, ratio, contactShove[i], options);
414
+ else if (state === BALANCE.FALLEN) stepFallen(i, ratio, options);
415
+ else if ((store.balanceTimer[i] += ctx.h) >= options.getUpSeconds) {
416
+ store.balance[i] = BALANCE.STANDING;
417
+ store.balanceTimer[i] = 0;
418
+ }
419
+ if (store.balance[i] === BALANCE.STAGGERING) staggering++;
420
+ else if (store.balance[i] === BALANCE.FALLEN || store.balance[i] === BALANCE.GETTING_UP) fallen++;
421
+ }
422
+ ctx.stats.fallen = fallen;
423
+ ctx.stats.staggering = staggering;
424
+ };
425
+ const landed = (i, height) => {
426
+ if (store.balance[i] !== BALANCE.STANDING && store.balance[i] !== BALANCE.STAGGERING) return;
427
+ if (height > COLLAPSE_FALL) requestFall(i, FALL_CAUSE.COLLAPSE, height / COLLAPSE_FALL);
428
+ else if (height > COLLAPSE_STAGGER) stagger(i);
429
+ };
430
+ return { requestFall, stagger, shove, step, flush, landed, pending: () => requests.length };
431
+ }
432
+
433
+ // src/physics/blast.ts
434
+ import { blastShove } from "@adatechnology/game-contracts";
435
+ var CENTRE_HEIGHT = 1;
436
+ var DISMEMBER_SHARE = 0.5;
437
+ var LIMB_CHANCE = 0.5;
438
+ var STUN_BASE = 0.45;
439
+ var BLOOD_PER_BODY = 60;
440
+ var LIMBS = [
441
+ { bit: MISSING.ARM_L, region: 2 },
442
+ { bit: MISSING.ARM_R, region: 3 },
443
+ { bit: MISSING.LEG_L, region: 4 },
444
+ { bit: MISSING.LEG_R, region: 5 }
445
+ ];
446
+ function createBlasts(ctx, hooks) {
447
+ const { store } = ctx;
448
+ const options = ctx.options.blast;
449
+ const queue = [];
450
+ const emitLimb = (tally, i, region) => {
451
+ if (tally.limbEffects++ >= options.limbEffectsPerBlast) return;
452
+ ctx.emit("effect", { effect: "limb", point: [store.x[i], store.height[i] * 0.9, store.z[i]], agent: i, region, severed: true });
453
+ };
454
+ const emitBlood = (tally, i) => {
455
+ if (tally.bloodEffects++ >= options.bloodEffectsPerBlast) {
456
+ tally.bloodSum += BLOOD_PER_BODY;
457
+ return;
458
+ }
459
+ ctx.emit("effect", { effect: "blood", point: [store.x[i], store.height[i] * 1.2, store.z[i]], agent: i, amount: BLOOD_PER_BODY, died: true });
460
+ };
461
+ const kill = (i, blast, distance, tally) => {
462
+ tally.killed++;
463
+ store.hp[i] = 0;
464
+ if (blast.lethalRadius !== void 0 && distance < DISMEMBER_SHARE * blast.lethalRadius) {
465
+ for (let k = 0; k < LIMBS.length; k++) {
466
+ if (unit(store.identity[i], ctx.step, SALT.LIMB + k) >= LIMB_CHANCE) continue;
467
+ store.missing[i] |= LIMBS[k].bit;
468
+ emitLimb(tally, i, LIMBS[k].region);
469
+ }
470
+ }
471
+ emitBlood(tally, i);
472
+ ctx.deferKill(i);
473
+ };
474
+ const hurt = (i, blast, distance, index) => {
475
+ const falloff = (1 - distance / blast.radius) ** 2;
476
+ const amount = blast.energy / 1e3 * options.pointsPerKilojoule * falloff;
477
+ const draw = (salt) => () => unit(store.identity[i], ctx.step, salt);
478
+ if (ctx.damage(i, { region: 1, amount, distance, cut: falloff }, draw(SALT.JAW + index)).died) return true;
479
+ const region = Math.floor(unit(store.identity[i], ctx.step, SALT.REGION + index) * 6);
480
+ return ctx.damage(i, { region, amount: amount / 2, distance, cut: falloff }, draw(SALT.JAW + 8 + index)).died;
481
+ };
482
+ const shoveOf = (i, blasts) => {
483
+ const position = [store.x[i], store.y[i] + CENTRE_HEIGHT * store.height[i], store.z[i]];
484
+ let vx = 0;
485
+ let vy = 0;
486
+ let vz = 0;
487
+ let falloff = 0;
488
+ for (const blast of blasts) {
489
+ const shove = blastShove({ point: blast.point, energy: blast.energy, radius: blast.radius }, { position, mass: store.mass[i] }, options.shove);
490
+ if (!shove) continue;
491
+ vx += shove.speed * shove.direction[0];
492
+ vy += shove.speed * shove.direction[1];
493
+ vz += shove.speed * shove.direction[2];
494
+ falloff = Math.max(falloff, shove.falloff);
495
+ }
496
+ return vx || vy || vz ? { vx, vy, vz, falloff } : void 0;
497
+ };
498
+ const distanceTo = (i, blast) => Math.hypot(store.x[i] - blast.point[0], store.y[i] + CENTRE_HEIGHT * store.height[i] - blast.point[1], store.z[i] - blast.point[2]);
499
+ const applyShove = (i, shove, candidates, tallies, blasts) => {
500
+ const horizontal = Math.hypot(shove.vx, shove.vz);
501
+ const speed = Math.hypot(horizontal, shove.vy);
502
+ store.directShove[i] = horizontal;
503
+ store.blastStep[i] = ctx.step;
504
+ const { balance, thrown } = hooks;
505
+ if (thrown && speed >= options.throwSpeed && (store.x[i] - ctx.centre.x) ** 2 + (store.z[i] - ctx.centre.z) ** 2 <= ctx.options.thrown.radius ** 2) {
506
+ candidates.push({ index: i, speed, vx: shove.vx, vy: shove.vy, vz: shove.vz, distance: Math.hypot(store.x[i] - ctx.centre.x, store.z[i] - ctx.centre.z) });
507
+ return;
508
+ }
509
+ if (store.hp[i] <= 0) return;
510
+ if (!balance) {
511
+ store.state[i] = STATE.STUNNED;
512
+ store.stateTimer[i] = STUN_BASE + shove.falloff;
513
+ store.vx[i] = shove.vx;
514
+ store.vz[i] = shove.vz;
515
+ return;
516
+ }
517
+ store.pushX[i] += shove.vx;
518
+ store.pushZ[i] += shove.vz;
519
+ balance.shove(i, horizontal, FALL_CAUSE.BLAST);
520
+ for (let b = 0; b < blasts.length; b++) if (distanceTo(i, blasts[b]) <= blasts[b].radius && horizontal > ctx.options.balance.shoveFall) tallies[b].fallen++;
521
+ };
522
+ const affect = (i, blasts, tallies, candidates) => {
523
+ let dead = store.hp[i] <= 0;
524
+ for (let b = 0; b < blasts.length && !dead; b++) {
525
+ const blast = blasts[b];
526
+ const distance = distanceTo(i, blast);
527
+ if (distance > blast.radius) continue;
528
+ tallies[b].reached++;
529
+ if (blast.lethalRadius !== void 0 && distance < blast.lethalRadius) kill(i, blast, distance, tallies[b]);
530
+ else if (hurt(i, blast, distance, b)) tallies[b].killed++;
531
+ dead = store.hp[i] <= 0 || dead;
532
+ }
533
+ const shove = shoveOf(i, blasts);
534
+ if (shove) applyShove(i, shove, candidates, tallies, blasts);
535
+ };
536
+ const resolve = () => {
537
+ if (!queue.length) return;
538
+ const blasts = queue.splice(0, options.maxBlastsPerStep);
539
+ const tallies = blasts.map(() => ({ reached: 0, killed: 0, fallen: 0, limbEffects: 0, bloodEffects: 0, bloodSum: 0 }));
540
+ const candidates = [];
541
+ ctx.lastBlastStep = ctx.step;
542
+ ctx.stats.blastChecks = store.n * blasts.length;
543
+ for (let i = 0; i < store.n; i++) if (store.balance[i] !== BALANCE.THROWN) affect(i, blasts, tallies, candidates);
544
+ const thrown = hooks.thrown?.assign(candidates) ?? 0;
545
+ blasts.forEach((blast, b) => {
546
+ const tally = tallies[b];
547
+ if (tally.bloodSum > 0) ctx.emit("effect", { effect: "blood", point: [blast.point[0], blast.point[1], blast.point[2]], amount: tally.bloodSum, died: true });
548
+ hooks.pileBlast(blast.point[0], blast.point[2], blast.radius);
549
+ ctx.emit("blasted", {
550
+ point: [blast.point[0], blast.point[1], blast.point[2]],
551
+ radius: blast.radius,
552
+ reached: tally.reached,
553
+ killed: tally.killed,
554
+ thrown: b === 0 ? thrown : 0,
555
+ fallen: tally.fallen,
556
+ ...blast.chain === void 0 ? {} : { chain: blast.chain }
557
+ });
558
+ if (blast.chain !== void 0) ctx.stats.chainLinks++;
559
+ });
560
+ };
561
+ return { enqueue: (blast) => void queue.push(blast), resolve, pending: () => queue.length };
562
+ }
563
+
564
+ // src/physics/contact.ts
565
+ var GROUND_MASS_FACTOR2 = 3;
566
+ var PAIR_MARGIN = 0.15;
567
+ var MAX_LEVEL_GAP = 0.15;
568
+ var PAIRS_PER_AGENT = 12;
569
+ var MIN_DISTANCE = 1e-6;
570
+ var createContactWorkspace = (capacity, grid) => ({
571
+ head: new Int32Array(Math.floor(grid.size) ** 2).fill(-1),
572
+ next: new Int32Array(capacity),
573
+ touched: new Int32Array(capacity),
574
+ touchedCount: 0,
575
+ pairI: new Int32Array(capacity * PAIRS_PER_AGENT),
576
+ pairJ: new Int32Array(capacity * PAIRS_PER_AGENT),
577
+ pairCount: 0,
578
+ deltaX: new Float32Array(capacity),
579
+ deltaZ: new Float32Array(capacity),
580
+ count: new Uint8Array(capacity),
581
+ pushBeforeX: new Float32Array(capacity),
582
+ pushBeforeZ: new Float32Array(capacity),
583
+ shove: new Float32Array(capacity),
584
+ takesPart: new Uint8Array(capacity),
585
+ radiusOf: new Float64Array(capacity),
586
+ massOf: new Float64Array(capacity),
587
+ pairReach: new Float64Array(capacity * PAIRS_PER_AGENT),
588
+ pairDistance: new Float64Array(capacity * PAIRS_PER_AGENT),
589
+ pairNormalX: new Float64Array(capacity * PAIRS_PER_AGENT),
590
+ pairNormalZ: new Float64Array(capacity * PAIRS_PER_AGENT)
591
+ });
592
+ var createContactView = (store, grid) => ({ n: 0, store, grid, workspace: createContactWorkspace(store.x.length, grid) });
593
+ var takesPartInContact = (store, i) => store.band[i] === 0 && store.balance[i] !== BALANCE.THROWN && store.state[i] !== STATE.PILE_STEP && store.state[i] !== STATE.DORMANT;
594
+ var isOnGround = (store, i) => store.balance[i] === BALANCE.FALLEN || store.balance[i] === BALANCE.GETTING_UP;
595
+ var effectiveMass = (store, i) => store.mass[i] * (isOnGround(store, i) ? GROUND_MASS_FACTOR2 : 1);
596
+ var bodyRadius = (store, i, radius) => radius * store.build[i];
597
+ function readBodies(view, radius) {
598
+ const { store, workspace } = view;
599
+ const { takesPart, radiusOf, massOf } = workspace;
600
+ for (let i = 0; i < view.n; i++) {
601
+ const takes = takesPartInContact(store, i);
602
+ takesPart[i] = takes ? 1 : 0;
603
+ radiusOf[i] = bodyRadius(store, i, radius);
604
+ massOf[i] = effectiveMass(store, i);
605
+ }
606
+ }
607
+ function rebuildHash(view) {
608
+ const { store, grid, workspace } = view;
609
+ const side = Math.floor(grid.size);
610
+ for (let k = 0; k < workspace.touchedCount; k++) workspace.head[workspace.touched[k]] = -1;
611
+ workspace.touchedCount = 0;
612
+ for (let i = 0; i < view.n; i++) {
613
+ if (!workspace.takesPart[i]) continue;
614
+ const hx = Math.floor(store.x[i] + grid.half);
615
+ const hz = Math.floor(store.z[i] + grid.half);
616
+ if (hx < 0 || hz < 0 || hx >= side || hz >= side) continue;
617
+ const cell = hz * side + hx;
618
+ if (workspace.head[cell] === -1) workspace.touched[workspace.touchedCount++] = cell;
619
+ workspace.next[i] = workspace.head[cell];
620
+ workspace.head[cell] = i;
621
+ }
622
+ }
623
+ var FORWARD_CELLS = [1, 0, -1, 1, 0, 1, 1, 1];
624
+ function addPairsOfCell(view, i, cell, sameCell) {
625
+ const { store, workspace } = view;
626
+ const { head, next, radiusOf, pairI, pairJ } = workspace;
627
+ const { x, z, y } = store;
628
+ const xi = x[i];
629
+ const zi = z[i];
630
+ const yi = y[i];
631
+ const reachBase = radiusOf[i] + PAIR_MARGIN;
632
+ for (let j = head[cell]; j !== -1; j = next[j]) {
633
+ if (sameCell && j <= i || Math.abs(yi - y[j]) > MAX_LEVEL_GAP) continue;
634
+ const reach = reachBase + radiusOf[j];
635
+ const ex = x[j] - xi;
636
+ const ez = z[j] - zi;
637
+ if (ex * ex + ez * ez >= reach * reach || workspace.pairCount >= pairI.length) continue;
638
+ pairI[workspace.pairCount] = i;
639
+ pairJ[workspace.pairCount++] = j;
640
+ }
641
+ }
642
+ function collectPairs(view) {
643
+ const { store, grid, workspace } = view;
644
+ const side = Math.floor(grid.size);
645
+ workspace.pairCount = 0;
646
+ for (let i = 0; i < view.n; i++) {
647
+ if (!workspace.takesPart[i]) continue;
648
+ const hx = Math.floor(store.x[i] + grid.half);
649
+ const hz = Math.floor(store.z[i] + grid.half);
650
+ if (hx < 0 || hz < 0 || hx >= side || hz >= side) continue;
651
+ addPairsOfCell(view, i, hz * side + hx, true);
652
+ for (let k = 0; k < FORWARD_CELLS.length; k += 2) {
653
+ const qx = hx + FORWARD_CELLS[k];
654
+ const qz = hz + FORWARD_CELLS[k + 1];
655
+ if (qx >= 0 && qz >= 0 && qx < side && qz < side) addPairsOfCell(view, i, qz * side + qx, false);
656
+ }
657
+ }
658
+ }
659
+ function measurePairs(view) {
660
+ const { store, workspace } = view;
661
+ const { pairI, pairJ, radiusOf, pairReach, pairDistance, pairNormalX, pairNormalZ } = workspace;
662
+ const { x, z, identity } = store;
663
+ for (let p = 0; p < workspace.pairCount; p++) {
664
+ const i = pairI[p];
665
+ const j = pairJ[p];
666
+ const reach = radiusOf[i] + radiusOf[j];
667
+ const ex = x[i] - x[j];
668
+ const ez = z[i] - z[j];
669
+ const distance = Math.sqrt(ex * ex + ez * ez);
670
+ pairReach[p] = reach;
671
+ pairDistance[p] = distance;
672
+ if (distance >= reach) continue;
673
+ if (distance > MIN_DISTANCE) {
674
+ pairNormalX[p] = ex / distance;
675
+ pairNormalZ[p] = ez / distance;
676
+ continue;
677
+ }
678
+ const angle = 2 * Math.PI * unit(identity[i] ^ identity[j], 0, SALT.SPIN);
679
+ pairNormalX[p] = Math.cos(angle);
680
+ pairNormalZ[p] = Math.sin(angle);
681
+ }
682
+ }
683
+ function solveImpulse(store, workspace, p) {
684
+ if (workspace.pairDistance[p] >= workspace.pairReach[p]) return;
685
+ const i = workspace.pairI[p];
686
+ const j = workspace.pairJ[p];
687
+ const nx = workspace.pairNormalX[p];
688
+ const nz = workspace.pairNormalZ[p];
689
+ const { vx, vz, pushX, pushZ } = store;
690
+ const closing = (vx[i] + pushX[i] - vx[j] - pushX[j]) * nx + (vz[i] + pushZ[i] - vz[j] - pushZ[j]) * nz;
691
+ if (closing >= 0) return;
692
+ const massI = workspace.massOf[i];
693
+ const massJ = workspace.massOf[j];
694
+ const impulse = -closing * massI * massJ / (massI + massJ);
695
+ pushX[i] += impulse * nx / massI;
696
+ pushZ[i] += impulse * nz / massI;
697
+ pushX[j] -= impulse * nx / massJ;
698
+ pushZ[j] -= impulse * nz / massJ;
699
+ }
700
+ function accumulateCorrection(workspace, p, relaxation) {
701
+ const distance = workspace.pairDistance[p];
702
+ const reach = workspace.pairReach[p];
703
+ if (distance >= reach) return;
704
+ const i = workspace.pairI[p];
705
+ const j = workspace.pairJ[p];
706
+ const massI = workspace.massOf[i];
707
+ const massJ = workspace.massOf[j];
708
+ const move = relaxation * (reach - distance);
709
+ const nx = workspace.pairNormalX[p];
710
+ const nz = workspace.pairNormalZ[p];
711
+ workspace.deltaX[i] += move * nx * (massJ / (massI + massJ));
712
+ workspace.deltaZ[i] += move * nz * (massJ / (massI + massJ));
713
+ workspace.deltaX[j] -= move * nx * (massI / (massI + massJ));
714
+ workspace.deltaZ[j] -= move * nz * (massI / (massI + massJ));
715
+ workspace.count[i]++;
716
+ workspace.count[j]++;
717
+ }
718
+ var isOpen = (grid, x, z) => {
719
+ const cell = grid.cellOf(x, z);
720
+ return cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3);
721
+ };
722
+ function applyCorrection(view, agent, params) {
723
+ const { store, grid, workspace } = view;
724
+ const count = workspace.count[agent];
725
+ if (!count) return;
726
+ workspace.count[agent] = 0;
727
+ const cap = params.maxCorrection;
728
+ const dx = Math.max(-cap, Math.min(cap, workspace.deltaX[agent] / count));
729
+ const dz = Math.max(-cap, Math.min(cap, workspace.deltaZ[agent] / count));
730
+ if (isOpen(grid, store.x[agent] + dx, store.z[agent])) store.x[agent] += dx;
731
+ if (isOpen(grid, store.x[agent], store.z[agent] + dz)) store.z[agent] += dz;
732
+ }
733
+ function resetCorrection(workspace, agent) {
734
+ workspace.deltaX[agent] = 0;
735
+ workspace.deltaZ[agent] = 0;
736
+ workspace.count[agent] = 0;
737
+ }
738
+ function correctPositions(view, params) {
739
+ const { workspace } = view;
740
+ for (let p = 0; p < workspace.pairCount; p++) {
741
+ resetCorrection(workspace, workspace.pairI[p]);
742
+ resetCorrection(workspace, workspace.pairJ[p]);
743
+ }
744
+ for (let p = 0; p < workspace.pairCount; p++) accumulateCorrection(workspace, p, params.relaxation);
745
+ for (let p = 0; p < workspace.pairCount; p++) {
746
+ applyCorrection(view, workspace.pairI[p], params);
747
+ applyCorrection(view, workspace.pairJ[p], params);
748
+ }
749
+ }
750
+ function measureOverlaps(view, out) {
751
+ const { store, workspace } = view;
752
+ for (let p = 0; p < workspace.pairCount; p++) {
753
+ const i = workspace.pairI[p];
754
+ const j = workspace.pairJ[p];
755
+ if (isOnGround(store, i) && isOnGround(store, j)) continue;
756
+ const reach = workspace.radiusOf[i] + workspace.radiusOf[j];
757
+ const overlap = reach - Math.hypot(store.x[i] - store.x[j], store.z[i] - store.z[j]);
758
+ if (overlap <= 0) continue;
759
+ out.contacts++;
760
+ out.overlapSum += overlap;
761
+ if (overlap > out.maxOverlap) out.maxOverlap = overlap;
762
+ }
763
+ }
764
+ function dampPush(view, params) {
765
+ const { store, workspace } = view;
766
+ const lambda = Math.exp(-params.friction * params.h);
767
+ for (let i = 0; i < view.n; i++) {
768
+ workspace.shove[i] = Math.hypot(store.pushX[i] - workspace.pushBeforeX[i], store.pushZ[i] - workspace.pushBeforeZ[i]);
769
+ store.pushX[i] *= lambda;
770
+ store.pushZ[i] *= lambda;
771
+ const speed = Math.hypot(store.pushX[i], store.pushZ[i]);
772
+ if (speed <= params.maxPush) continue;
773
+ store.pushX[i] *= params.maxPush / speed;
774
+ store.pushZ[i] *= params.maxPush / speed;
775
+ }
776
+ }
777
+ function prepareHash(view, radius) {
778
+ readBodies(view, radius);
779
+ rebuildHash(view);
780
+ }
781
+ function contactStep(view, params, out) {
782
+ const { store, workspace } = view;
783
+ out.contacts = 0;
784
+ out.maxOverlap = 0;
785
+ out.overlapSum = 0;
786
+ for (let i = 0; i < view.n; i++) {
787
+ workspace.pushBeforeX[i] = store.pushX[i];
788
+ workspace.pushBeforeZ[i] = store.pushZ[i];
789
+ }
790
+ readBodies(view, params.radius);
791
+ rebuildHash(view);
792
+ collectPairs(view);
793
+ for (let iteration = 0; iteration < params.iterations; iteration++) {
794
+ measurePairs(view);
795
+ for (let p = 0; p < workspace.pairCount; p++) solveImpulse(store, workspace, p);
796
+ for (let p = workspace.pairCount - 1; p >= 0; p--) solveImpulse(store, workspace, p);
797
+ correctPositions(view, params);
798
+ }
799
+ measureOverlaps(view, out);
800
+ dampPush(view, params);
801
+ }
802
+
803
+ // src/physics/context.ts
804
+ var createPhysicsStats = () => ({
805
+ contacts: 0,
806
+ maxOverlap: 0,
807
+ meanOverlap: 0,
808
+ falls: [0, 0, 0, 0, 0, 0],
809
+ fallsBeyondBlast: 0,
810
+ fallen: 0,
811
+ staggering: 0,
812
+ climbers: 0,
813
+ moundMax: 0,
814
+ thrownActive: 0,
815
+ thrownPeak: 0,
816
+ blastChecks: 0,
817
+ chainLinks: 0,
818
+ fieldMax: 0,
819
+ playerPressure: 0
820
+ });
821
+
822
+ // src/physics/density.ts
823
+ var DECISION_STEPS = 12;
824
+ var FAR_BAND = 1;
825
+ function createDensity(ctx, options) {
826
+ const { store, grid } = ctx;
827
+ const side = Math.ceil(grid.size / options.cell);
828
+ const bodies = new Uint16Array(side * side);
829
+ const touched = [];
830
+ const half = grid.half;
831
+ const cellOf = (i) => {
832
+ const gx = Math.floor((store.x[i] + half) / options.cell);
833
+ const gz = Math.floor((store.z[i] + half) / options.cell);
834
+ return gx < 0 || gz < 0 || gx >= side || gz >= side ? -1 : gz * side + gx;
835
+ };
836
+ const isWalking = (i) => store.band[i] === FAR_BAND && (store.balance[i] === BALANCE.STANDING || store.balance[i] === BALANCE.STAGGERING) && store.state[i] !== STATE.PILE_STEP && store.state[i] !== STATE.DORMANT;
837
+ const count = () => {
838
+ for (let k = 0; k < touched.length; k++) bodies[touched[k]] = 0;
839
+ touched.length = 0;
840
+ for (let i = 0; i < store.n; i++) {
841
+ if (!isWalking(i)) continue;
842
+ const cell = cellOf(i);
843
+ if (cell < 0) continue;
844
+ if (bodies[cell] === 0) touched.push(cell);
845
+ bodies[cell]++;
846
+ }
847
+ };
848
+ const step = () => {
849
+ if (ctx.step % DECISION_STEPS === 0) count();
850
+ if (!touched.length) return;
851
+ for (let i = 0; i < store.n; i++) {
852
+ if (!isWalking(i)) continue;
853
+ const cell = cellOf(i);
854
+ const around = cell < 0 ? 0 : bodies[cell];
855
+ store.field[i] = around > options.capacity ? (around - options.capacity) * options.pressurePerBody : 0;
856
+ }
857
+ };
858
+ const bodiesAround = (i) => {
859
+ const cell = cellOf(i);
860
+ return cell < 0 ? 0 : bodies[cell];
861
+ };
862
+ const slowdown = (i, band) => {
863
+ if (band !== FAR_BAND) return 1;
864
+ const around = bodiesAround(i);
865
+ return around > options.capacity ? options.capacity / around : 1;
866
+ };
867
+ return { step, slowdown, bodiesAround };
868
+ }
869
+
870
+ // src/physics/mound.ts
871
+ var CELLS_PER_BODY = 3;
872
+ var BODY_PART_SPACING = 0.5;
873
+ var CLIMB_HEIGHT = 0.25;
874
+ var ELEVATED = 0.05;
875
+ var NEUTRAL_MASS = 70;
876
+ var NEIGHBOURS = [-1, 1, -1, 0, -1, -1, 0, -1, 0, 1, 1, -1, 1, 0, 1, 1];
877
+ var BODY_IDENTITY_SCALE = 1e6;
878
+ function createMound(ctx, extraBodies, height) {
879
+ const { store, grid } = ctx;
880
+ const options = ctx.options.mound;
881
+ const balance = ctx.options.balance;
882
+ const cellCount = grid.n * grid.n;
883
+ const touched = new Int32Array((store.capacity + extraBodies) * CELLS_PER_BODY * 2);
884
+ let touchedCount = 0;
885
+ const entryCapacity = (store.capacity + extraBodies) * CELLS_PER_BODY;
886
+ const entries = { count: 0, cell: new Int32Array(entryCapacity), thickness: new Uint16Array(entryCapacity), identity: new Uint32Array(entryCapacity), agent: new Int32Array(entryCapacity) };
887
+ const standMass = new Float32Array(cellCount);
888
+ const standTouched = new Int32Array(store.capacity);
889
+ let standTouchedCount = 0;
890
+ const maxMillimetres = Math.round(options.maxHeight * 1e3);
891
+ const cells = [0, 0, 0];
892
+ const cellsOfBody = (x, z, yaw) => {
893
+ const sin = Math.sin(yaw);
894
+ const cos = Math.cos(yaw);
895
+ for (let part2 = 0; part2 < CELLS_PER_BODY; part2++) {
896
+ const offset = (part2 - 1) * BODY_PART_SPACING;
897
+ cells[part2] = grid.cellOf(x + sin * offset, z + cos * offset);
898
+ }
899
+ };
900
+ const addBody = (x, z, yaw, millimetres, identity, agent) => {
901
+ cellsOfBody(x, z, yaw);
902
+ for (const cell of cells) {
903
+ if (cell < 0) continue;
904
+ if (height[cell] === 0) touched[touchedCount++] = cell;
905
+ height[cell] += millimetres;
906
+ const index = entries.count++;
907
+ entries.cell[index] = cell;
908
+ entries.thickness[index] = millimetres;
909
+ entries.identity[index] = identity;
910
+ entries.agent[index] = agent;
911
+ }
912
+ };
913
+ const sumBodies = () => {
914
+ for (let k = 0; k < touchedCount; k++) height[touched[k]] = 0;
915
+ touchedCount = 0;
916
+ entries.count = 0;
917
+ for (let i = 0; i < store.n; i++) {
918
+ const state = store.balance[i];
919
+ if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
920
+ addBody(store.x[i], store.z[i], store.renderYaw[i], Math.round(options.bodyThickness * store.build[i] * 1e3), store.identity[i], i);
921
+ }
922
+ const reach = options.corpseRadius * options.corpseRadius;
923
+ for (const { data } of ctx.corpseList()) {
924
+ if ((data.x - ctx.centre.x) ** 2 + (data.z - ctx.centre.z) ** 2 > reach) continue;
925
+ addBody(data.x, data.z, data.yaw, Math.round(options.corpseThickness * data.build * 1e3), mix32(Math.floor(data.seed * BODY_IDENTITY_SCALE)), -1);
926
+ }
927
+ };
928
+ const isOpenCell = (cell) => grid.occ[cell] === 0 || grid.occ[cell] === 3;
929
+ const lowestNeighbour = (cell, millimetres) => {
930
+ const cx = cell % grid.n;
931
+ const cz = Math.floor(cell / grid.n);
932
+ let best = -1;
933
+ for (let k = 0; k < NEIGHBOURS.length; k += 2) {
934
+ const nx = cx + NEIGHBOURS[k];
935
+ const nz = cz + NEIGHBOURS[k + 1];
936
+ if (nx < 0 || nz < 0 || nx >= grid.n || nz >= grid.n) continue;
937
+ const neighbour = nz * grid.n + nx;
938
+ if (!isOpenCell(neighbour) || height[neighbour] + millimetres > maxMillimetres) continue;
939
+ if (best < 0 || height[neighbour] < height[best] || height[neighbour] === height[best] && neighbour < best) best = neighbour;
940
+ }
941
+ return best;
942
+ };
943
+ const slideEntry = (index) => {
944
+ const cell = entries.cell[index];
945
+ const target = lowestNeighbour(cell, entries.thickness[index]);
946
+ if (target < 0) return;
947
+ if (height[target] === 0) touched[touchedCount++] = target;
948
+ height[target] += entries.thickness[index];
949
+ height[cell] -= entries.thickness[index];
950
+ entries.cell[index] = target;
951
+ const agent = entries.agent[index];
952
+ if (agent < 0) return;
953
+ store.x[agent] += (target % grid.n - cell % grid.n) * grid.cell;
954
+ store.z[agent] += (Math.floor(target / grid.n) - Math.floor(cell / grid.n)) * grid.cell;
955
+ };
956
+ const capHeights = () => {
957
+ let over = false;
958
+ for (let k = 0; k < touchedCount && !over; k++) over = height[touched[k]] > maxMillimetres;
959
+ if (!over) return;
960
+ const order = Array.from({ length: entries.count }, (_, index) => index).sort((a, b) => entries.cell[a] - entries.cell[b] || entries.identity[a] - entries.identity[b] || a - b);
961
+ let current = -1;
962
+ let running = 0;
963
+ for (const index of order) {
964
+ const cell = entries.cell[index];
965
+ if (cell !== current) {
966
+ current = cell;
967
+ running = 0;
968
+ }
969
+ if (height[cell] <= maxMillimetres) continue;
970
+ if (running + entries.thickness[index] <= maxMillimetres) running += entries.thickness[index];
971
+ else slideEntry(index);
972
+ }
973
+ for (let k = 0; k < touchedCount; k++) if (height[touched[k]] > maxMillimetres) height[touched[k]] = maxMillimetres;
974
+ };
975
+ const seatFallen = () => {
976
+ let top = 0;
977
+ for (let k = 0; k < touchedCount; k++) top = Math.max(top, height[touched[k]]);
978
+ ctx.stats.moundMax = top / 1e3;
979
+ for (let i = 0; i < store.n; i++) {
980
+ const state = store.balance[i];
981
+ if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
982
+ const cell = grid.cellOf(store.x[i], store.z[i]);
983
+ if (cell >= 0) store.y[i] = Math.max(0, height[cell] / 1e3 - options.bodyThickness * store.build[i]);
984
+ }
985
+ };
986
+ const weighStanding = () => {
987
+ for (let k = 0; k < standTouchedCount; k++) standMass[standTouched[k]] = 0;
988
+ standTouchedCount = 0;
989
+ let climbers = 0;
990
+ for (let i = 0; i < store.n; i++) {
991
+ const state = store.balance[i];
992
+ if (store.band[i] !== 0 || store.state[i] === STATE.PILE_STEP || state !== BALANCE.STANDING && state !== BALANCE.STAGGERING) continue;
993
+ if (store.y[i] >= CLIMB_HEIGHT) climbers++;
994
+ if (store.y[i] <= ELEVATED) continue;
995
+ const cell = grid.cellOf(store.x[i], store.z[i]);
996
+ if (cell < 0) continue;
997
+ if (standMass[cell] === 0) standTouched[standTouchedCount++] = cell;
998
+ standMass[cell] += store.mass[i];
999
+ }
1000
+ ctx.stats.climbers = climbers;
1001
+ };
1002
+ const trample = () => {
1003
+ if (!standTouchedCount) return;
1004
+ for (let i = 0; i < store.n; i++) {
1005
+ const state = store.balance[i];
1006
+ if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
1007
+ const mass = weightOn(i);
1008
+ if (!mass) continue;
1009
+ store.hp[i] -= balance.trampleDps * (mass / NEUTRAL_MASS) * ctx.h;
1010
+ if (store.hp[i] <= 0) ctx.deferKill(i);
1011
+ }
1012
+ };
1013
+ const step = () => {
1014
+ sumBodies();
1015
+ capHeights();
1016
+ seatFallen();
1017
+ weighStanding();
1018
+ trample();
1019
+ };
1020
+ const weightOn = (i) => {
1021
+ cellsOfBody(store.x[i], store.z[i], store.renderYaw[i]);
1022
+ let mass = 0;
1023
+ for (const cell of cells) if (cell >= 0) mass += standMass[cell];
1024
+ return mass;
1025
+ };
1026
+ return { height, step, weightOn };
1027
+ }
1028
+
1029
+ // src/physics/offload/default-worker.ts
1030
+ function createDefaultWorkerFactory() {
1031
+ if (typeof Worker === "undefined") return null;
1032
+ let url;
1033
+ try {
1034
+ url = new URL("./physics-worker.js", import.meta.url);
1035
+ } catch {
1036
+ return null;
1037
+ }
1038
+ return () => {
1039
+ const worker = new Worker(url, { type: "module" });
1040
+ return {
1041
+ postMessage: (message, transfer) => worker.postMessage(message, transfer ?? []),
1042
+ onMessage: (listener) => worker.addEventListener("message", (event) => listener(event.data)),
1043
+ terminate: () => worker.terminate()
1044
+ };
1045
+ };
1046
+ }
1047
+
1048
+ // src/physics/offload/frame.ts
1049
+ var F32_INPUTS = ["x", "z", "y", "vx", "vz", "pushX", "pushZ", "mass", "build", "aimX", "aimZ", "driveFactor", "fieldPrev"];
1050
+ var U8_INPUTS = ["band", "balance", "state", "type"];
1051
+ var F32_OUTPUTS = ["deltaX", "deltaZ", "deltaPushX", "deltaPushZ", "deltaVx", "deltaVz", "field", "shove", "doorLoad"];
1052
+ var HEADER = { n: 0, h: 1, playerOn: 2, playerX: 3, playerZ: 4, contacts: 5, maxOverlap: 6, overlapSum: 7, playerLoad: 8, drivers: 9, fieldMax: 10, size: 16 };
1053
+ var align8 = (bytes) => Math.ceil(bytes / 8) * 8;
1054
+ var frameBytes = ({ capacity, cells }) => HEADER.size * 8 + (F32_INPUTS.length + F32_OUTPUTS.length + 2) * align8(capacity * 4) + U8_INPUTS.length * align8(capacity) + align8(cells);
1055
+ function createFrame(buffer, shape) {
1056
+ const { capacity, cells } = shape;
1057
+ let offset = 0;
1058
+ const take = (make, bytesPerElement, length) => {
1059
+ const view = make(buffer, offset, length);
1060
+ offset += align8(length * bytesPerElement);
1061
+ return view;
1062
+ };
1063
+ const f64 = (b, o, l) => new Float64Array(b, o, l);
1064
+ const f32 = (b, o, l) => new Float32Array(b, o, l);
1065
+ const u8 = (b, o, l) => new Uint8Array(b, o, l);
1066
+ const header = take(f64, 8, HEADER.size);
1067
+ const floats = {};
1068
+ for (const key of [...F32_INPUTS, ...F32_OUTPUTS]) floats[key] = take(f32, 4, capacity);
1069
+ const identity = take((b, o, l) => new Uint32Array(b, o, l), 4, capacity);
1070
+ const doorCell = take((b, o, l) => new Int32Array(b, o, l), 4, capacity);
1071
+ const bytes = {};
1072
+ for (const key of U8_INPUTS) bytes[key] = take(u8, 1, capacity);
1073
+ const occ = take(u8, 1, cells);
1074
+ return { header, f32: floats, u8: bytes, identity, doorCell, occ };
1075
+ }
1076
+ var INPUT_KEYS = { f32: F32_INPUTS, u8: U8_INPUTS };
1077
+
1078
+ // src/physics/offload/offload.ts
1079
+ var isOpen2 = (grid, x, z) => {
1080
+ const cell = grid.cellOf(x, z);
1081
+ return cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3);
1082
+ };
1083
+ function writeSnapshot(frame, source, h, player) {
1084
+ const { store, grid } = source;
1085
+ const n = store.n;
1086
+ for (const key of INPUT_KEYS.f32) frame.f32[key].set(store[key].subarray(0, n));
1087
+ for (const key of INPUT_KEYS.u8) frame.u8[key].set(store[key].subarray(0, n));
1088
+ frame.identity.set(store.identity.subarray(0, n));
1089
+ frame.occ.set(grid.occ);
1090
+ frame.header[HEADER.n] = n;
1091
+ frame.header[HEADER.h] = h;
1092
+ frame.header[HEADER.playerOn] = player ? 1 : 0;
1093
+ frame.header[HEADER.playerX] = player?.x ?? 0;
1094
+ frame.header[HEADER.playerZ] = player?.z ?? 0;
1095
+ }
1096
+ function createOffload(runner, host, latency = 1) {
1097
+ const { store, grid, stats, contact } = host;
1098
+ const queue = [];
1099
+ const spare = Array.from({ length: latency }, () => ({ count: 0, origin: null, originBuffer: new Int32Array(store.capacity) }));
1100
+ const result = { playerLoad: 0 };
1101
+ const apply = (frame, pending) => {
1102
+ const { f32, header } = frame;
1103
+ const { origin, count } = pending;
1104
+ for (let agent = 0; agent < store.n; agent++) {
1105
+ const from = origin ? origin[agent] : agent < count ? agent : -1;
1106
+ if (from < 0) {
1107
+ contact.workspace.shove[agent] = 0;
1108
+ continue;
1109
+ }
1110
+ const dx = f32.deltaX[from];
1111
+ const dz = f32.deltaZ[from];
1112
+ if (dx !== 0 && isOpen2(grid, store.x[agent] + dx, store.z[agent])) store.x[agent] += dx;
1113
+ if (dz !== 0 && isOpen2(grid, store.x[agent], store.z[agent] + dz)) store.z[agent] += dz;
1114
+ store.pushX[agent] += f32.deltaPushX[from];
1115
+ store.pushZ[agent] += f32.deltaPushZ[from];
1116
+ store.vx[agent] += f32.deltaVx[from];
1117
+ store.vz[agent] += f32.deltaVz[from];
1118
+ store.field[agent] = f32.field[from];
1119
+ store.fieldPrev[agent] = f32.field[from];
1120
+ contact.workspace.shove[agent] = f32.shove[from];
1121
+ const cell = frame.doorCell[from];
1122
+ if (cell >= 0) host.addDoorLoad(cell, f32.doorLoad[from]);
1123
+ }
1124
+ stats.contacts = header[HEADER.contacts];
1125
+ stats.maxOverlap = header[HEADER.maxOverlap];
1126
+ stats.meanOverlap = header[HEADER.contacts] ? header[HEADER.overlapSum] / header[HEADER.contacts] : 0;
1127
+ stats.fieldMax = header[HEADER.fieldMax];
1128
+ result.playerLoad = header[HEADER.playerLoad];
1129
+ };
1130
+ const step = (h, player) => {
1131
+ result.playerLoad = 0;
1132
+ if (queue.length >= latency) {
1133
+ const frame2 = runner.settle();
1134
+ if (frame2) {
1135
+ const pending = queue.shift();
1136
+ apply(frame2, pending);
1137
+ pending.origin = null;
1138
+ spare.push(pending);
1139
+ }
1140
+ }
1141
+ const frame = runner.frame();
1142
+ if (frame && spare.length) {
1143
+ writeSnapshot(frame, { store, grid }, h, player);
1144
+ runner.submit();
1145
+ const pending = spare.shift();
1146
+ pending.count = store.n;
1147
+ queue.push(pending);
1148
+ }
1149
+ return result;
1150
+ };
1151
+ const remap = (removed, moved) => {
1152
+ queue.forEach((pending) => {
1153
+ if (!pending.origin) {
1154
+ pending.origin = pending.originBuffer.fill(-1);
1155
+ for (let agent = 0; agent < pending.count; agent++) pending.origin[agent] = agent;
1156
+ }
1157
+ pending.origin[removed] = removed === moved ? -1 : pending.origin[moved];
1158
+ if (removed !== moved) pending.origin[moved] = -1;
1159
+ });
1160
+ };
1161
+ return { step, remap, dispose: () => runner.dispose() };
1162
+ }
1163
+
1164
+ // src/physics/pressure.ts
1165
+ var TOUCH_SLACK = 0.08;
1166
+ var WALL_PROBE = 0.12;
1167
+ var PLAYER_REACH = 1.3;
1168
+ var PLAYER_RADIUS_SQUARED = PLAYER_REACH * PLAYER_REACH;
1169
+ var createPressureWorkspace = (capacity, pairCapacity) => ({
1170
+ weightSum: new Float32Array(capacity),
1171
+ pressing: new Uint8Array(capacity),
1172
+ wallKind: new Uint8Array(capacity),
1173
+ wallDoor: new Int32Array(capacity),
1174
+ playerWeight: new Float32Array(capacity),
1175
+ cutExcess: new Float32Array(capacity),
1176
+ cutX: new Float32Array(capacity),
1177
+ cutZ: new Float32Array(capacity),
1178
+ pairWeightForward: new Float32Array(pairCapacity),
1179
+ pairWeightBackward: new Float32Array(pairCapacity),
1180
+ canPress: new Uint8Array(capacity)
1181
+ });
1182
+ var isPressing = (store, i) => takesPartInContact(store, i) && (store.balance[i] === BALANCE.STANDING || store.balance[i] === BALANCE.STAGGERING);
1183
+ function readPressing(view, workspace) {
1184
+ for (let i = 0; i < view.n; i++) workspace.canPress[i] = isPressing(view.store, i) ? 1 : 0;
1185
+ }
1186
+ var frontWeight = (alignment, cone) => alignment > cone ? (alignment - cone) / (1 - cone) : 0;
1187
+ var hasAim = (store, i) => store.aimX[i] !== 0 || store.aimZ[i] !== 0;
1188
+ function noteCut(store, workspace, i, j, nx, nz) {
1189
+ const excess = (store.vx[i] - store.vx[j]) * nx + (store.vz[i] - store.vz[j]) * nz;
1190
+ if (excess <= workspace.cutExcess[i]) return;
1191
+ workspace.cutExcess[i] = excess;
1192
+ workspace.cutX[i] = nx;
1193
+ workspace.cutZ[i] = nz;
1194
+ }
1195
+ function weighPairs(view, workspace, params) {
1196
+ const { store } = view;
1197
+ for (let p = 0; p < view.workspace.pairCount; p++) {
1198
+ workspace.pairWeightForward[p] = 0;
1199
+ workspace.pairWeightBackward[p] = 0;
1200
+ const i = view.workspace.pairI[p];
1201
+ const j = view.workspace.pairJ[p];
1202
+ if (!workspace.canPress[i] || !workspace.canPress[j]) continue;
1203
+ const ex = store.x[j] - store.x[i];
1204
+ const ez = store.z[j] - store.z[i];
1205
+ const distance = Math.sqrt(ex * ex + ez * ez);
1206
+ if (distance < 1e-6 || distance > (store.build[i] + store.build[j]) * params.radius + TOUCH_SLACK) continue;
1207
+ const nx = ex / distance;
1208
+ const nz = ez / distance;
1209
+ const forward = frontWeight(store.aimX[i] * nx + store.aimZ[i] * nz, params.frontCone);
1210
+ const backward = frontWeight(-(store.aimX[j] * nx + store.aimZ[j] * nz), params.frontCone);
1211
+ if (forward > 0) {
1212
+ workspace.pressing[i] = 1;
1213
+ noteCut(store, workspace, i, j, nx, nz);
1214
+ }
1215
+ if (backward > 0) {
1216
+ workspace.pressing[j] = 1;
1217
+ noteCut(store, workspace, j, i, -nx, -nz);
1218
+ }
1219
+ if (forward > 0 && backward > 0) continue;
1220
+ workspace.pairWeightForward[p] = forward;
1221
+ workspace.pairWeightBackward[p] = backward;
1222
+ workspace.weightSum[i] += forward;
1223
+ workspace.weightSum[j] += backward;
1224
+ }
1225
+ }
1226
+ function weighWalls(view, workspace, params, doorAt) {
1227
+ const { store, grid } = view;
1228
+ for (let i = 0; i < view.n; i++) {
1229
+ if (!workspace.canPress[i] || !hasAim(store, i)) continue;
1230
+ const reach = store.build[i] * params.radius + WALL_PROBE;
1231
+ const cell = grid.cellOf(store.x[i] + store.aimX[i] * reach, store.z[i] + store.aimZ[i] * reach);
1232
+ if (cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3)) continue;
1233
+ workspace.wallKind[i] = 1;
1234
+ workspace.wallDoor[i] = cell >= 0 && grid.occ[cell] === 5 ? doorAt(cell) : -1;
1235
+ workspace.pressing[i] = 1;
1236
+ workspace.weightSum[i] += 1;
1237
+ }
1238
+ }
1239
+ function weighPlayer(view, workspace, params, player) {
1240
+ const { store } = view;
1241
+ for (let i = 0; i < view.n; i++) {
1242
+ if (!workspace.canPress[i]) continue;
1243
+ const ex = player.x - store.x[i];
1244
+ const ez = player.z - store.z[i];
1245
+ const squared = ex * ex + ez * ez;
1246
+ if (squared > PLAYER_RADIUS_SQUARED || squared < 1e-6) continue;
1247
+ const distance = Math.sqrt(squared);
1248
+ const weight = hasAim(store, i) ? frontWeight((store.aimX[i] * ex + store.aimZ[i] * ez) / distance, params.frontCone) : 1;
1249
+ workspace.playerWeight[i] = weight;
1250
+ if (weight > 0) workspace.pressing[i] = 1;
1251
+ workspace.weightSum[i] += weight;
1252
+ }
1253
+ }
1254
+ function clearWorkspace(view, workspace) {
1255
+ const { store } = view;
1256
+ for (let i = 0; i < view.n; i++) {
1257
+ workspace.weightSum[i] = 0;
1258
+ workspace.pressing[i] = 0;
1259
+ workspace.wallKind[i] = 0;
1260
+ workspace.playerWeight[i] = 0;
1261
+ workspace.cutExcess[i] = 0;
1262
+ store.field[i] = 0;
1263
+ }
1264
+ }
1265
+ function ownDrive(store, context, workspace, i) {
1266
+ if (!hasAim(store, i) || !workspace.pressing[i]) return 0;
1267
+ return context.driveForceOfType[store.type[i]] * store.driveFactor[i];
1268
+ }
1269
+ function spreadField(view, workspace, params, context, out) {
1270
+ const { store } = view;
1271
+ for (let i = 0; i < view.n; i++) {
1272
+ if (!workspace.canPress[i]) continue;
1273
+ store.field[i] = ownDrive(store, context, workspace, i);
1274
+ if (store.field[i] > 0) out.drivers++;
1275
+ }
1276
+ for (let p = 0; p < view.workspace.pairCount; p++) {
1277
+ const i = view.workspace.pairI[p];
1278
+ const j = view.workspace.pairJ[p];
1279
+ const forward = workspace.pairWeightForward[p];
1280
+ const backward = workspace.pairWeightBackward[p];
1281
+ if (forward > 0) store.field[j] += params.transmission * store.fieldPrev[i] * forward * forward / workspace.weightSum[i];
1282
+ if (backward > 0) store.field[i] += params.transmission * store.fieldPrev[j] * backward * backward / workspace.weightSum[j];
1283
+ }
1284
+ for (let i = 0; i < view.n; i++) {
1285
+ if (!workspace.canPress[i]) continue;
1286
+ const passed = params.transmission * store.fieldPrev[i] / (workspace.weightSum[i] || 1);
1287
+ if (workspace.wallKind[i] && workspace.wallDoor[i] >= 0) {
1288
+ if (context.doorOut) {
1289
+ context.doorOut.cell[i] = workspace.wallDoor[i];
1290
+ context.doorOut.load[i] = passed * params.h;
1291
+ } else context.doorLoad[workspace.wallDoor[i]] += passed * params.h;
1292
+ }
1293
+ if (workspace.playerWeight[i] > 0) out.playerLoad += passed * workspace.playerWeight[i] * workspace.playerWeight[i];
1294
+ if (store.field[i] > params.cap) store.field[i] = params.cap;
1295
+ }
1296
+ }
1297
+ function applyCuts(view, workspace) {
1298
+ const { store } = view;
1299
+ for (let i = 0; i < view.n; i++) {
1300
+ const excess = workspace.cutExcess[i];
1301
+ if (excess <= 0) continue;
1302
+ store.vx[i] -= excess * workspace.cutX[i];
1303
+ store.vz[i] -= excess * workspace.cutZ[i];
1304
+ }
1305
+ }
1306
+ function pressureStep(view, workspace, params, context, out) {
1307
+ out.playerLoad = 0;
1308
+ out.drivers = 0;
1309
+ clearWorkspace(view, workspace);
1310
+ readPressing(view, workspace);
1311
+ weighPairs(view, workspace, params);
1312
+ weighWalls(view, workspace, params, context.doorAt);
1313
+ if (context.player) weighPlayer(view, workspace, params, context.player);
1314
+ spreadField(view, workspace, params, context, out);
1315
+ applyCuts(view, workspace);
1316
+ for (let i = 0; i < view.n; i++) view.store.fieldPrev[i] = view.store.field[i];
1317
+ }
1318
+
1319
+ // src/physics/offload/kernel.ts
1320
+ var PAIRS_PER_AGENT2 = 12;
1321
+ var STORE_KEYS = ["x", "z", "y", "vx", "vz", "pushX", "pushZ", "mass", "build", "aimX", "aimZ", "driveFactor", "fieldPrev"];
1322
+ function createKernel(setup) {
1323
+ const { capacity } = setup;
1324
+ const side = Math.floor(setup.gridSize / setup.gridCell);
1325
+ const half = setup.gridSize / 2;
1326
+ const before = { x: new Float32Array(capacity), z: new Float32Array(capacity), pushX: new Float32Array(capacity), pushZ: new Float32Array(capacity), vx: new Float32Array(capacity), vz: new Float32Array(capacity) };
1327
+ const driveForceOfType = Float32Array.from(setup.driveForceOfType);
1328
+ const contactResult = { contacts: 0, maxOverlap: 0, overlapSum: 0 };
1329
+ const pressureResult = { playerLoad: 0, drivers: 0 };
1330
+ const workspace = setup.pressure ? createPressureWorkspace(capacity, capacity * PAIRS_PER_AGENT2) : null;
1331
+ const cellOf = createCellOf(half, setup.gridCell, side);
1332
+ const doorLoadUnused = new Float32Array(0);
1333
+ const store = {};
1334
+ const grid = { size: setup.gridSize, half, occ: new Uint8Array(0), cellOf };
1335
+ const view = { n: 0, store, grid, workspace: createContactWorkspace(capacity, grid) };
1336
+ const bind = (frame) => {
1337
+ Object.assign(store, frame.u8, { identity: frame.identity, field: frame.f32.field });
1338
+ for (const key of STORE_KEYS) store[key] = frame.f32[key];
1339
+ grid.occ = frame.occ;
1340
+ return view;
1341
+ };
1342
+ const run = (frame) => {
1343
+ const { header } = frame;
1344
+ const n = header[HEADER.n];
1345
+ const contact = bind(frame);
1346
+ contact.n = n;
1347
+ for (let i = 0; i < n; i++) {
1348
+ before.x[i] = frame.f32.x[i];
1349
+ before.z[i] = frame.f32.z[i];
1350
+ before.pushX[i] = frame.f32.pushX[i];
1351
+ before.pushZ[i] = frame.f32.pushZ[i];
1352
+ before.vx[i] = frame.f32.vx[i];
1353
+ before.vz[i] = frame.f32.vz[i];
1354
+ }
1355
+ const h = header[HEADER.h];
1356
+ const params = { ...setup.contact, h };
1357
+ contactStep(contact, params, contactResult);
1358
+ header[HEADER.contacts] = contactResult.contacts;
1359
+ header[HEADER.maxOverlap] = contactResult.maxOverlap;
1360
+ header[HEADER.overlapSum] = contactResult.overlapSum;
1361
+ frame.doorCell.fill(-1, 0, n);
1362
+ frame.f32.doorLoad.fill(0, 0, n);
1363
+ if (workspace && setup.pressure) {
1364
+ const player = header[HEADER.playerOn] ? { x: header[HEADER.playerX], z: header[HEADER.playerZ] } : void 0;
1365
+ const doorOut = { cell: frame.doorCell, load: frame.f32.doorLoad };
1366
+ pressureStep(contact, workspace, { ...setup.pressure, h, radius: setup.contact.radius }, { driveForceOfType, doorAt: (cell) => cell, doorLoad: doorLoadUnused, player, doorOut }, pressureResult);
1367
+ let top = 0;
1368
+ for (let i = 0; i < n; i++) top = Math.max(top, frame.f32.field[i]);
1369
+ header[HEADER.fieldMax] = top;
1370
+ } else header[HEADER.fieldMax] = 0;
1371
+ header[HEADER.playerLoad] = pressureResult.playerLoad;
1372
+ header[HEADER.drivers] = pressureResult.drivers;
1373
+ const { f32 } = frame;
1374
+ for (let i = 0; i < n; i++) {
1375
+ f32.deltaX[i] = f32.x[i] - before.x[i];
1376
+ f32.deltaZ[i] = f32.z[i] - before.z[i];
1377
+ f32.deltaPushX[i] = f32.pushX[i] - before.pushX[i];
1378
+ f32.deltaPushZ[i] = f32.pushZ[i] - before.pushZ[i];
1379
+ f32.deltaVx[i] = f32.vx[i] - before.vx[i];
1380
+ f32.deltaVz[i] = f32.vz[i] - before.vz[i];
1381
+ f32.shove[i] = contact.workspace.shove[i];
1382
+ }
1383
+ };
1384
+ return { run };
1385
+ }
1386
+
1387
+ // src/physics/offload/worker-host.ts
1388
+ var CONTROL = { IDLE: 0, SUBMITTED: 1, DONE: 2, STOP: 3 };
1389
+ function attachPhysicsWorker(port) {
1390
+ let kernel = null;
1391
+ let shape = null;
1392
+ port.onMessage((data) => {
1393
+ const message = data;
1394
+ if (message.type === "init") {
1395
+ kernel = createKernel(message.setup);
1396
+ shape = message.shape;
1397
+ if (message.frames && message.control) runShared(kernel, message.frames.map((buffer) => createFrame(buffer, message.shape)), new Int32Array(message.control));
1398
+ return;
1399
+ }
1400
+ if (!kernel || !shape) return;
1401
+ const frame = createFrame(message.buffer, shape);
1402
+ kernel.run(frame);
1403
+ port.postMessage({ type: "done", buffer: message.buffer }, [message.buffer]);
1404
+ });
1405
+ }
1406
+ function runShared(kernel, frames, control) {
1407
+ for (let turn = 0; ; turn++) {
1408
+ const slot = turn % frames.length;
1409
+ for (; ; ) {
1410
+ const state = Atomics.load(control, slot);
1411
+ if (state === CONTROL.STOP) return;
1412
+ if (state === CONTROL.SUBMITTED) break;
1413
+ Atomics.wait(control, slot, state);
1414
+ }
1415
+ kernel.run(frames[slot]);
1416
+ Atomics.store(control, slot, CONTROL.DONE);
1417
+ Atomics.notify(control, slot);
1418
+ }
1419
+ }
1420
+
1421
+ // src/physics/offload/runner.ts
1422
+ var WAIT_LIMIT_MS = 5e3;
1423
+ var canBlock = (() => {
1424
+ try {
1425
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 1, 0);
1426
+ return true;
1427
+ } catch {
1428
+ return false;
1429
+ }
1430
+ })();
1431
+ var isSharedAvailable = () => typeof SharedArrayBuffer !== "undefined" && globalThis.crossOriginIsolated !== false;
1432
+ function createInlineRunner(setup, shape, slots = 1) {
1433
+ const kernel = createKernel(setup);
1434
+ const frames = Array.from({ length: slots }, () => createFrame(new ArrayBuffer(frameBytes(shape)), shape));
1435
+ let submitted = 0;
1436
+ let settled = 0;
1437
+ const runner = {
1438
+ transport: "inline",
1439
+ get inFlight() {
1440
+ return submitted - settled;
1441
+ },
1442
+ frame: () => submitted - settled < slots ? frames[submitted % slots] : null,
1443
+ submit: () => kernel.run(frames[submitted++ % slots]),
1444
+ settle: () => submitted > settled ? frames[settled++ % slots] : null,
1445
+ dispose: () => void 0
1446
+ };
1447
+ return runner;
1448
+ }
1449
+ function createSharedRunner(setup, shape, factory, slots = 1) {
1450
+ const buffers = Array.from({ length: slots }, () => new SharedArrayBuffer(frameBytes(shape)));
1451
+ const control = new Int32Array(new SharedArrayBuffer(4 * slots));
1452
+ const frames = buffers.map((buffer) => createFrame(buffer, shape));
1453
+ const worker = factory();
1454
+ worker.postMessage({ type: "init", setup, shape, frames: buffers, control: control.buffer });
1455
+ let submitted = 0;
1456
+ let settled = 0;
1457
+ return {
1458
+ transport: "shared",
1459
+ get inFlight() {
1460
+ return submitted - settled;
1461
+ },
1462
+ frame: () => submitted - settled < slots ? frames[submitted % slots] : null,
1463
+ submit: () => {
1464
+ const slot = submitted++ % slots;
1465
+ Atomics.store(control, slot, CONTROL.SUBMITTED);
1466
+ Atomics.notify(control, slot);
1467
+ },
1468
+ settle: () => {
1469
+ if (submitted === settled) return null;
1470
+ const slot = settled++ % slots;
1471
+ const started = Date.now();
1472
+ while (Atomics.load(control, slot) !== CONTROL.DONE) {
1473
+ if (Date.now() - started > WAIT_LIMIT_MS) throw new Error("crowd physics worker did not answer");
1474
+ if (canBlock) Atomics.wait(control, slot, CONTROL.SUBMITTED, 50);
1475
+ }
1476
+ Atomics.store(control, slot, CONTROL.IDLE);
1477
+ return frames[slot];
1478
+ },
1479
+ dispose: () => {
1480
+ for (let slot = 0; slot < slots; slot++) {
1481
+ Atomics.store(control, slot, CONTROL.STOP);
1482
+ Atomics.notify(control, slot);
1483
+ }
1484
+ worker.terminate();
1485
+ }
1486
+ };
1487
+ }
1488
+ function createTransferRunner(setup, shape, factory, slots = 1) {
1489
+ const worker = factory();
1490
+ const free = Array.from({ length: slots }, () => new ArrayBuffer(frameBytes(shape)));
1491
+ const done = [];
1492
+ const views = /* @__PURE__ */ new WeakMap();
1493
+ const viewOf = (buffer) => {
1494
+ let view = views.get(buffer);
1495
+ if (!view) views.set(buffer, view = createFrame(buffer, shape));
1496
+ return view;
1497
+ };
1498
+ let inFlight = 0;
1499
+ worker.postMessage({ type: "init", setup, shape });
1500
+ worker.onMessage((data) => {
1501
+ const message = data;
1502
+ if (message.type === "done") done.push(message.buffer);
1503
+ });
1504
+ return {
1505
+ transport: "transfer",
1506
+ get inFlight() {
1507
+ return inFlight;
1508
+ },
1509
+ frame: () => free.length ? viewOf(free[0]) : null,
1510
+ submit: () => {
1511
+ const buffer = free.shift();
1512
+ if (!buffer) return;
1513
+ worker.postMessage({ type: "frame", buffer }, [buffer]);
1514
+ inFlight++;
1515
+ },
1516
+ settle: () => {
1517
+ const buffer = done.shift();
1518
+ if (!buffer) return null;
1519
+ inFlight--;
1520
+ free.push(buffer);
1521
+ return viewOf(buffer);
1522
+ },
1523
+ dispose: () => worker.terminate()
1524
+ };
1525
+ }
1526
+
1527
+ // src/physics/player-pressure.ts
1528
+ var KNOCK_SECONDS = 1.5;
1529
+ var KNOCK_COOLDOWN = 2;
1530
+ var CRUSH_BLOW_EVERY = 0.25;
1531
+ function createPlayerPressure(ctx) {
1532
+ const options = ctx.options.playerPressure;
1533
+ const tau = ctx.options.balance?.pressureTau ?? 0.3;
1534
+ let knockCooldown = 0;
1535
+ let crushed = 0;
1536
+ let crushClock = 0;
1537
+ const crush = (excess) => {
1538
+ crushed += options.crushDamagePerSecond * excess * ctx.h;
1539
+ crushClock += ctx.h;
1540
+ if (crushClock < CRUSH_BLOW_EVERY) return;
1541
+ crushClock = 0;
1542
+ if (crushed <= 0) return;
1543
+ ctx.target()?.hurt(crushed);
1544
+ crushed = 0;
1545
+ };
1546
+ const step = (load) => {
1547
+ ctx.stats.playerPressure += (load - ctx.stats.playerPressure) * (1 - Math.exp(-ctx.h / tau));
1548
+ if (!options.enabled) return;
1549
+ const target = ctx.target();
1550
+ knockCooldown = Math.max(0, knockCooldown - ctx.h);
1551
+ if (!target || !target.state.alive || ctx.stats.playerPressure <= options.knockThreshold) {
1552
+ crushed = 0;
1553
+ return;
1554
+ }
1555
+ if (knockCooldown === 0) {
1556
+ target.knock(KNOCK_SECONDS);
1557
+ knockCooldown = KNOCK_COOLDOWN;
1558
+ }
1559
+ crush(ctx.stats.playerPressure / options.knockThreshold - 1);
1560
+ };
1561
+ return { step };
1562
+ }
1563
+
1564
+ // src/physics/thrown.ts
1565
+ var FREE_SLOT = -2;
1566
+ var CARRIED_BODY = -1;
1567
+ var MAX_SPIN = 8;
1568
+ var SWEEP_DISTANCE = 0.25;
1569
+ var LANDING_DAMAGE_SPEED = 6;
1570
+ var CENTRE_HEIGHT2 = 1;
1571
+ var MAX_POOL = 127;
1572
+ var createThrownPool = (size) => {
1573
+ const f32 = () => new Float32Array(size);
1574
+ const pool = {
1575
+ size,
1576
+ agent: new Int32Array(size).fill(FREE_SLOT),
1577
+ identity: new Uint32Array(size),
1578
+ corpse: Array.from({ length: size }, () => null),
1579
+ x: f32(),
1580
+ y: f32(),
1581
+ z: f32(),
1582
+ prevX: f32(),
1583
+ prevY: f32(),
1584
+ prevZ: f32(),
1585
+ vx: f32(),
1586
+ vy: f32(),
1587
+ vz: f32(),
1588
+ pitch: f32(),
1589
+ roll: f32(),
1590
+ spinPitch: f32(),
1591
+ spinRoll: f32(),
1592
+ active: () => pool.agent.reduce((count, agent) => count + (agent === FREE_SLOT ? 0 : 1), 0)
1593
+ };
1594
+ return pool;
1595
+ };
1596
+ var byFastest = (a, b, identityOf2) => b.speed - a.speed || identityOf2(a.index) - identityOf2(b.index);
1597
+ var byNearest = (a, b, identityOf2) => a.distance - b.distance || identityOf2(a.index) - identityOf2(b.index);
1598
+ function chooseCandidates(candidates, free, options, identityOf2) {
1599
+ if (candidates.length <= free) return candidates;
1600
+ if (options.priority === "fastest") return candidates.sort((a, b) => byFastest(a, b, identityOf2)).slice(0, free);
1601
+ if (options.priority === "nearest") return candidates.sort((a, b) => byNearest(a, b, identityOf2)).slice(0, free);
1602
+ const fast = Math.round(free * options.mixedRatio);
1603
+ const sorted = candidates.sort((a, b) => byFastest(a, b, identityOf2));
1604
+ const rest = sorted.slice(fast).sort((a, b) => byNearest(a, b, identityOf2));
1605
+ return [...sorted.slice(0, fast), ...rest.slice(0, free - fast)];
1606
+ }
1607
+ function createThrown(ctx) {
1608
+ const { store, grid } = ctx;
1609
+ const options = ctx.options.thrown;
1610
+ const size = Math.min(MAX_POOL, options.pool);
1611
+ const pool = createThrownPool(size);
1612
+ const identityOf2 = (i) => store.identity[i];
1613
+ const assign = (candidates) => {
1614
+ let free = 0;
1615
+ for (let slot2 = 0; slot2 < size; slot2++) if (pool.agent[slot2] === FREE_SLOT) free++;
1616
+ const chosen = chooseCandidates(candidates, free, options, identityOf2);
1617
+ let slot = 0;
1618
+ for (const candidate of chosen) {
1619
+ while (pool.agent[slot] !== FREE_SLOT) slot++;
1620
+ const i = candidate.index;
1621
+ pool.agent[slot] = i;
1622
+ pool.identity[slot] = store.identity[i];
1623
+ pool.corpse[slot] = null;
1624
+ pool.x[slot] = pool.prevX[slot] = store.x[i];
1625
+ pool.y[slot] = pool.prevY[slot] = store.y[i] + CENTRE_HEIGHT2 * store.height[i];
1626
+ pool.z[slot] = pool.prevZ[slot] = store.z[i];
1627
+ pool.vx[slot] = candidate.vx;
1628
+ pool.vy[slot] = candidate.vy;
1629
+ pool.vz[slot] = candidate.vz;
1630
+ pool.pitch[slot] = pool.roll[slot] = 0;
1631
+ pool.spinPitch[slot] = (unit(store.identity[i], ctx.step, SALT.SPIN) * 2 - 1) * MAX_SPIN;
1632
+ pool.spinRoll[slot] = (unit(store.identity[i], ctx.step, SALT.SPIN + 1) * 2 - 1) * MAX_SPIN;
1633
+ if (store.state[i] === STATE.PILE_STEP) ctx.releasePileStep(i);
1634
+ store.balance[i] = BALANCE.THROWN;
1635
+ store.thrown[i] = slot;
1636
+ store.vx[i] = store.vz[i] = store.pushX[i] = store.pushZ[i] = 0;
1637
+ store.aimX[i] = store.aimZ[i] = 0;
1638
+ }
1639
+ return chosen.length;
1640
+ };
1641
+ const remap = (removed, moved) => {
1642
+ var _a;
1643
+ for (let slot = 0; slot < size; slot++) {
1644
+ const agent = pool.agent[slot];
1645
+ if (agent === removed) {
1646
+ (_a = pool.corpse)[slot] ?? (_a[slot] = ctx.corpseOf(removed, unit(pool.identity[slot], ctx.step, SALT.CORPSE) * 10));
1647
+ pool.agent[slot] = CARRIED_BODY;
1648
+ } else if (agent === moved && moved !== removed) pool.agent[slot] = removed;
1649
+ }
1650
+ };
1651
+ const sweepWall = (x0, z0, x1, z1, y) => {
1652
+ const cell = grid.cell;
1653
+ let gx = Math.floor((x0 + grid.half) / cell);
1654
+ let gz = Math.floor((z0 + grid.half) / cell);
1655
+ const dx = x1 - x0;
1656
+ const dz = z1 - z0;
1657
+ const stepX = dx > 0 ? 1 : -1;
1658
+ const stepZ = dz > 0 ? 1 : -1;
1659
+ const deltaX = dx === 0 ? Infinity : Math.abs(cell / dx);
1660
+ const deltaZ = dz === 0 ? Infinity : Math.abs(cell / dz);
1661
+ let nextX = dx === 0 ? Infinity : ((dx > 0 ? gx + 1 : gx) * cell - grid.half - x0) / dx;
1662
+ let nextZ = dz === 0 ? Infinity : ((dz > 0 ? gz + 1 : gz) * cell - grid.half - z0) / dz;
1663
+ let t = 0;
1664
+ while (Math.min(nextX, nextZ) <= 1) {
1665
+ if (nextX < nextZ) {
1666
+ t = nextX;
1667
+ nextX += deltaX;
1668
+ gx += stepX;
1669
+ } else {
1670
+ t = nextZ;
1671
+ nextZ += deltaZ;
1672
+ gz += stepZ;
1673
+ }
1674
+ const hitX = x0 + dx * t;
1675
+ const hitZ = z0 + dz * t;
1676
+ if (grid.solidAt(hitX + Math.sign(dx) * 1e-4, hitZ + Math.sign(dz) * 1e-4, y)) return { x: x0 + dx * Math.max(0, t - 1e-3), z: z0 + dz * Math.max(0, t - 1e-3) };
1677
+ }
1678
+ return void 0;
1679
+ };
1680
+ const fly = (slot) => {
1681
+ pool.prevX[slot] = pool.x[slot];
1682
+ pool.prevY[slot] = pool.y[slot];
1683
+ pool.prevZ[slot] = pool.z[slot];
1684
+ pool.vy[slot] -= ctx.gravity * ctx.h;
1685
+ pool.pitch[slot] += pool.spinPitch[slot] * ctx.h;
1686
+ pool.roll[slot] += pool.spinRoll[slot] * ctx.h;
1687
+ const nx = pool.x[slot] + pool.vx[slot] * ctx.h;
1688
+ const nz = pool.z[slot] + pool.vz[slot] * ctx.h;
1689
+ const ny = pool.y[slot] + pool.vy[slot] * ctx.h;
1690
+ const speed = Math.hypot(pool.vx[slot], pool.vy[slot], pool.vz[slot]);
1691
+ const wall = speed * ctx.h > SWEEP_DISTANCE ? sweepWall(pool.x[slot], pool.z[slot], nx, nz, ny) : grid.solidAt(nx, nz, ny) ? { x: pool.x[slot], z: pool.z[slot] } : void 0;
1692
+ pool.x[slot] = wall ? wall.x : nx;
1693
+ pool.z[slot] = wall ? wall.z : nz;
1694
+ pool.y[slot] = ny;
1695
+ if (wall) pool.vx[slot] = pool.vz[slot] = 0;
1696
+ };
1697
+ const massOf = (slot) => {
1698
+ const agent = pool.agent[slot];
1699
+ if (agent >= 0) return store.mass[agent];
1700
+ const body = pool.corpse[slot];
1701
+ return ctx.options.contact.baseMass * body.height * body.build * body.build;
1702
+ };
1703
+ const knockNeighbours = (contact, balance, slot) => {
1704
+ const side = Math.floor(grid.size);
1705
+ const x = pool.x[slot];
1706
+ const z = pool.z[slot];
1707
+ const hx = Math.floor(x + grid.half);
1708
+ const hz = Math.floor(z + grid.half);
1709
+ const mass = massOf(slot);
1710
+ const speed = Math.hypot(pool.vx[slot], pool.vz[slot]);
1711
+ const reach = options.landingRadius * options.landingRadius;
1712
+ for (let oz = -1; oz <= 1; oz++)
1713
+ for (let ox = -1; ox <= 1; ox++) {
1714
+ if (hx + ox < 0 || hz + oz < 0 || hx + ox >= side || hz + oz >= side) continue;
1715
+ for (let j = contact.workspace.head[(hz + oz) * side + hx + ox]; j !== -1; j = contact.workspace.next[j]) {
1716
+ const ex = store.x[j] - x;
1717
+ const ez = store.z[j] - z;
1718
+ if (j === pool.agent[slot] || ex * ex + ez * ez > reach || !takesPartInContact(store, j)) continue;
1719
+ const away = Math.hypot(ex, ez);
1720
+ const along = away > 1e-6 ? [ex / away, ez / away] : [pool.vx[slot] / (speed || 1), pool.vz[slot] / (speed || 1)];
1721
+ const delta = mass / (mass + store.mass[j]) * speed;
1722
+ store.pushX[j] += delta * along[0];
1723
+ store.pushZ[j] += delta * along[1];
1724
+ balance.shove(j, delta, FALL_CAUSE.LANDING);
1725
+ }
1726
+ }
1727
+ };
1728
+ const land = (contact, balance, slot, ground) => {
1729
+ knockNeighbours(contact, balance, slot);
1730
+ const x = pool.x[slot];
1731
+ const z = pool.z[slot];
1732
+ ctx.emit("sound", { sound: "impact", volume: 1.2, position: [x, ground, z], distance: Math.hypot(x - ctx.centre.x, z - ctx.centre.z) });
1733
+ ctx.emit("effect", { effect: "dust", point: [x, ground, z], color: 9075302 });
1734
+ const agent = pool.agent[slot];
1735
+ if (agent >= 0) {
1736
+ store.balance[agent] = BALANCE.FALLEN;
1737
+ store.balanceTimer[agent] = 0;
1738
+ store.fallCause[agent] = FALL_CAUSE.BLAST;
1739
+ store.x[agent] = x;
1740
+ store.z[agent] = z;
1741
+ store.y[agent] = ground;
1742
+ store.vy[agent] = 0;
1743
+ store.thrown[agent] = -1;
1744
+ const amount = Math.max(0, -pool.vy[slot] - LANDING_DAMAGE_SPEED) * options.fallDamage;
1745
+ if (amount > 0) ctx.damage(agent, { region: 1, amount, distance: 0 }, () => unit(store.identity[agent], ctx.step, SALT.JAW));
1746
+ } else {
1747
+ ctx.emit("effect", { effect: "blood", point: [x, ground + 0.1, z], amount: 30, died: true });
1748
+ ctx.addCorpse({ ...pool.corpse[slot], x, z });
1749
+ }
1750
+ pool.agent[slot] = FREE_SLOT;
1751
+ pool.corpse[slot] = null;
1752
+ };
1753
+ const step = (contact, balance) => {
1754
+ let active = 0;
1755
+ for (let slot = 0; slot < size; slot++) {
1756
+ if (pool.agent[slot] === FREE_SLOT) continue;
1757
+ fly(slot);
1758
+ const cell = grid.cellOf(pool.x[slot], pool.z[slot]);
1759
+ const ground = cell < 0 ? 0 : ctx.groundAt(cell, pool.y[slot]);
1760
+ const agent = pool.agent[slot];
1761
+ if (pool.y[slot] <= ground) {
1762
+ land(contact, balance, slot, ground);
1763
+ continue;
1764
+ }
1765
+ active++;
1766
+ if (agent < 0) continue;
1767
+ store.x[agent] = pool.x[slot];
1768
+ store.z[agent] = pool.z[slot];
1769
+ store.y[agent] = Math.max(ground, pool.y[slot] - CENTRE_HEIGHT2 * store.height[agent]);
1770
+ }
1771
+ ctx.stats.thrownActive = active;
1772
+ ctx.stats.thrownPeak = Math.max(ctx.stats.thrownPeak, active);
1773
+ };
1774
+ return { pool, assign, step, remap };
1775
+ }
1776
+
1777
+ // src/physics/engine.ts
1778
+ var DEFAULT_DRIVE_FORCE = 250;
1779
+ var GRAVITY_FORCE2 = 9.8;
1780
+ var EXTRA_BODIES = 500;
1781
+ var PAIRS_PER_AGENT3 = 12;
1782
+ function createPhysics(base, options, stats = createPhysicsStats()) {
1783
+ const moundOn = options.mound !== null;
1784
+ const ctx = { ...base, options, stats, step: -1, h: 0, centre: { x: 0, z: 0 }, lastBlastStep: -1e3, moundAt: (cell) => moundOn ? base.moundHeight[cell] : 0, weightOn: () => 0 };
1785
+ const { store, grid } = base;
1786
+ const balance = options.balance ? createBalance(ctx) : null;
1787
+ const thrown = options.thrown ? createThrown(ctx) : null;
1788
+ const contact = options.contact ? createContactView(store, grid) : null;
1789
+ const pressure = options.pressure && contact ? createPressureWorkspace(store.capacity, store.capacity * PAIRS_PER_AGENT3) : null;
1790
+ const mound = options.mound ? createMound(ctx, Math.max(EXTRA_BODIES, base.corpsesMax), base.moundHeight) : null;
1791
+ if (mound) ctx.weightOn = mound.weightOn;
1792
+ const density = options.density && balance ? createDensity(ctx, options.density) : null;
1793
+ const playerPressure = pressure ? createPlayerPressure(ctx) : null;
1794
+ const blasts = createBlasts(ctx, { balance, thrown, pileBlast: base.pileBlast });
1795
+ const contactResult = { contacts: 0, maxOverlap: 0, overlapSum: 0 };
1796
+ const pressureResult = { playerLoad: 0, drivers: 0 };
1797
+ const driveForceOfType = Float32Array.from(base.types, (type) => type.balance?.driveForce ?? DEFAULT_DRIVE_FORCE);
1798
+ const offload = options.worker !== "off" && contact ? createOffloadFor() : null;
1799
+ let doorLoad = new Float32Array(0);
1800
+ let doorSince = new Float32Array(0);
1801
+ const keepPositions = options.contact !== null || options.deterministic;
1802
+ function createRunner() {
1803
+ const gridCells = grid.occ.length;
1804
+ const shape = { capacity: store.capacity, cells: gridCells };
1805
+ const setup = { capacity: store.capacity, gridSize: grid.size, gridCell: grid.cell, contact: options.contact, pressure: options.pressure, driveForceOfType: Array.from(driveForceOfType) };
1806
+ const transport = options.workerTransport ?? (isSharedAvailable() ? "shared" : "transfer");
1807
+ const slots = options.workerLatency;
1808
+ if (transport === "inline") return createInlineRunner(setup, shape, slots);
1809
+ const factory = options.workerFactory ?? createDefaultWorkerFactory();
1810
+ if (!factory) {
1811
+ if (options.worker === "on") throw new Error('crowd physics: worker "on" but no Worker can be made here (give physics.workerFactory)');
1812
+ return null;
1813
+ }
1814
+ return transport === "shared" ? createSharedRunner(setup, shape, factory, slots) : createTransferRunner(setup, shape, factory, slots);
1815
+ }
1816
+ function createOffloadFor() {
1817
+ const runner = createRunner();
1818
+ if (!runner) return null;
1819
+ const host = {
1820
+ store,
1821
+ grid,
1822
+ stats,
1823
+ contact,
1824
+ addDoorLoad: (cell, load) => {
1825
+ const door = ctx.doors.at(cell);
1826
+ if (door < 0) return;
1827
+ growDoorArrays();
1828
+ doorLoad[door] += load;
1829
+ }
1830
+ };
1831
+ return createOffload(runner, host, options.workerLatency);
1832
+ }
1833
+ const beginStep = (h, centre) => {
1834
+ ctx.step++;
1835
+ ctx.h = h;
1836
+ ctx.centre = { x: centre.x, z: centre.z };
1837
+ if (keepPositions) {
1838
+ for (let i = 0; i < store.n; i++) {
1839
+ store.prevX[i] = store.x[i];
1840
+ store.prevZ[i] = store.z[i];
1841
+ store.prevY[i] = store.y[i];
1842
+ }
1843
+ }
1844
+ blasts.resolve();
1845
+ };
1846
+ const hitDoors = () => {
1847
+ for (let door = 0; door < ctx.doors.list.length; door++) {
1848
+ if (doorLoad[door] <= 0) continue;
1849
+ doorSince[door] += ctx.h;
1850
+ if (doorSince[door] < ctx.attackEvery) continue;
1851
+ ctx.doors.hit(door, doorLoad[door] * options.doorPressureDamage);
1852
+ doorLoad[door] = 0;
1853
+ doorSince[door] = 0;
1854
+ }
1855
+ };
1856
+ const growDoorArrays = () => {
1857
+ if (doorLoad.length >= ctx.doors.list.length) return;
1858
+ const length = Math.max(64, ctx.doors.list.length * 2);
1859
+ doorLoad = Float32Array.from({ length }, (_, door) => doorLoad[door] ?? 0);
1860
+ doorSince = Float32Array.from({ length }, (_, door) => doorSince[door] ?? 0);
1861
+ };
1862
+ const runPressure = (view, workspace) => {
1863
+ growDoorArrays();
1864
+ const target = ctx.target();
1865
+ const player = options.playerPressure.enabled && target && target.state.alive ? { x: target.state.x, z: target.state.z } : void 0;
1866
+ pressureStep(view, workspace, { ...options.pressure, h: ctx.h, radius: options.contact.radius }, { driveForceOfType, doorAt: ctx.doors.at, doorLoad, player }, pressureResult);
1867
+ let top = 0;
1868
+ for (let i = 0; i < store.n; i++) top = Math.max(top, store.field[i]);
1869
+ stats.fieldMax = top;
1870
+ hitDoors();
1871
+ };
1872
+ const stepOffloaded = (running, view) => {
1873
+ growDoorArrays();
1874
+ const target = ctx.target();
1875
+ const player = options.playerPressure.enabled && target && target.state.alive ? { x: target.state.x, z: target.state.z } : void 0;
1876
+ const result = running.step(ctx.h, player);
1877
+ pressureResult.playerLoad = result.playerLoad;
1878
+ prepareHash(view, options.contact.radius);
1879
+ if (pressure) hitDoors();
1880
+ };
1881
+ const measureLean = () => {
1882
+ const maxPush = options.contact.maxPush;
1883
+ for (let i = 0; i < store.n; i++) {
1884
+ store.leanX[i] = store.pushX[i] / maxPush;
1885
+ store.leanZ[i] = store.pushZ[i] / maxPush;
1886
+ }
1887
+ };
1888
+ const endStep = () => {
1889
+ if (!contact) return;
1890
+ contact.n = store.n;
1891
+ if (offload) stepOffloaded(offload, contact);
1892
+ else {
1893
+ contactStep(contact, { ...options.contact, h: ctx.h }, contactResult);
1894
+ stats.contacts = contactResult.contacts;
1895
+ stats.maxOverlap = contactResult.maxOverlap;
1896
+ stats.meanOverlap = contactResult.contacts ? contactResult.overlapSum / contactResult.contacts : 0;
1897
+ if (pressure) runPressure(contact, pressure);
1898
+ }
1899
+ density?.step();
1900
+ measureLean();
1901
+ balance?.step(options.balance, contact.workspace.shove);
1902
+ mound?.step();
1903
+ thrown?.step(contact, balance);
1904
+ balance?.flush();
1905
+ playerPressure?.step(pressureResult.playerLoad);
1906
+ };
1907
+ const criticalOf = (i) => options.balance.criticalFactor * store.mass[i] * GRAVITY_FORCE2 * (base.typeOf(i).balance?.stability ?? 1) * store.stability[i];
1908
+ return {
1909
+ ctx,
1910
+ active: contact !== null,
1911
+ stats,
1912
+ blasts,
1913
+ thrown: thrown?.pool ?? createThrownPool(0),
1914
+ contact,
1915
+ balance,
1916
+ beginStep,
1917
+ endStep,
1918
+ remap: (removed, moved) => {
1919
+ thrown?.remap(removed, moved);
1920
+ offload?.remap(removed, moved);
1921
+ },
1922
+ dispose: () => offload?.dispose(),
1923
+ densitySlowdown: (i, band) => density?.slowdown(i, band) ?? 1,
1924
+ pressureRatio: (i) => options.balance ? store.pressure[i] / criticalOf(i) : 0
1925
+ };
1926
+ }
1927
+
1928
+ // src/physics/pile-column.ts
1929
+ var COLLAPSE_MARK = -7;
1930
+ function collapsePileColumns(store, columns, cellCount, release) {
1931
+ const owners = /* @__PURE__ */ new Map();
1932
+ for (let i = 0; i < store.n; i++) if (store.state[i] === STATE.PILE_STEP && store.stepCell[i] >= 0) owners.set(store.stepCell[i], i);
1933
+ for (const column of columns.splice(0)) {
1934
+ if (column.dir === 0) continue;
1935
+ for (let k = 1, cell = column.cell - column.dir; cell >= 0 && cell < cellCount; k++, cell = column.cell - column.dir * k) {
1936
+ const j = owners.get(cell);
1937
+ if (j === void 0) break;
1938
+ owners.delete(cell);
1939
+ release(j);
1940
+ store.state[j] = STATE.AIRBORNE;
1941
+ store.vy[j] = 0;
1942
+ store.stateTimer[j] = COLLAPSE_MARK;
1943
+ }
1944
+ }
1945
+ }
1946
+
1947
+ // src/personality.ts
1948
+ var GAIT_WEIGHTS = [6, 1, 1, 1, 1, 2];
1949
+ var triangular = (identity, salt, range) => range[0] + (range[1] - range[0]) * ((unit(identity, 0, salt) + unit(identity, 1, salt)) / 2);
1950
+ var weighted = (identity, salt, weights) => {
1951
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
1952
+ let pick = unit(identity, 0, salt) * total;
1953
+ for (let index = 0; index < weights.length; index++) {
1954
+ pick -= weights[index];
1955
+ if (pick < 0) return index;
1956
+ }
1957
+ return weights.length - 1;
1958
+ };
1959
+ function applyNeutralPersonality(store, i) {
1960
+ store.stability[i] = store.tripFactor[i] = store.driveFactor[i] = store.cadenceFactor[i] = store.aggression[i] = 1;
1961
+ store.reaction[i] = 0;
1962
+ store.idle[i] = 0;
1963
+ store.gaitStyle[i] = 0;
1964
+ }
1965
+ function applyPersonality(store, i, options) {
1966
+ const identity = store.identity[i];
1967
+ const speed = triangular(identity, SALT.SPEED, options.speed);
1968
+ store.chaseSpeed[i] *= speed;
1969
+ store.wanderSpeed[i] *= speed;
1970
+ store.reaction[i] = triangular(identity, SALT.REACTION, options.reaction);
1971
+ store.stability[i] = triangular(identity, SALT.STABILITY, options.stability);
1972
+ store.tripFactor[i] = triangular(identity, SALT.TRIP_FACTOR, options.trip);
1973
+ store.driveFactor[i] = triangular(identity, SALT.DRIVE, options.drive);
1974
+ store.cadenceFactor[i] = triangular(identity, SALT.CADENCE, options.cadence);
1975
+ store.aggression[i] = triangular(identity, SALT.AGGRESSION, options.aggression);
1976
+ store.idle[i] = weighted(identity, SALT.IDLE, options.idle);
1977
+ store.gaitStyle[i] = weighted(identity, SALT.GAIT, GAIT_WEIGHTS);
1978
+ }
1979
+
1980
+ // src/physics/seat.ts
1981
+ function seatPhysics(store, i, born) {
1982
+ store.identity[i] = identityOf(born.seed, born.serial);
1983
+ store.mass[i] = born.baseMass * store.height[i] * store.build[i] ** 2;
1984
+ store.balance[i] = BALANCE.STANDING;
1985
+ store.balanceTimer[i] = store.pushX[i] = store.pushZ[i] = store.pressure[i] = store.field[i] = store.fieldPrev[i] = store.aimX[i] = store.aimZ[i] = store.directShove[i] = store.leanX[i] = store.leanZ[i] = 0;
1986
+ store.fallCause[i] = 0;
1987
+ store.blastStep[i] = -1;
1988
+ store.thrown[i] = -1;
1989
+ store.wounds[i] = 0;
1990
+ store.stepDir[i] = 0;
1991
+ store.prevX[i] = store.x[i];
1992
+ store.prevZ[i] = store.z[i];
1993
+ store.prevY[i] = 0;
1994
+ if (born.personality) applyPersonality(store, i, born.personality);
1995
+ else applyNeutralPersonality(store, i);
1996
+ }
1997
+
1998
+ // src/physics/options.ts
1999
+ var CONTACT_DEFAULTS = { radius: 0.25, iterations: 2, friction: 6, maxPush: 3, maxCorrection: 0.1, relaxation: 1, baseMass: 70, separationMax: 1.5 };
2000
+ var PRESSURE_DEFAULTS = { transmission: 0.97, cap: 6e3, frontCone: 0.5, staggerSpeed: 0.35 };
2001
+ var BALANCE_DEFAULTS = {
2002
+ criticalFactor: 2,
2003
+ staggerEnter: 0.7,
2004
+ staggerLeave: 0.4,
2005
+ staggerMin: 0.5,
2006
+ fallRate: 1.5,
2007
+ shoveStagger: 1.2,
2008
+ shoveFall: 2.5,
2009
+ fallenMin: 1.5,
2010
+ releaseFactor: 0.3,
2011
+ getUpSeconds: 1.2,
2012
+ pressureTau: 0.3,
2013
+ maxFallsPerStep: 8,
2014
+ maxFallen: 300,
2015
+ trampleDps: 6
2016
+ };
2017
+ var MOUND_DEFAULTS = { bodyThickness: 0.28, corpseThickness: 0.25, maxHeight: 1.6, corpseRadius: 35 };
2018
+ var THROWN_DEFAULTS = { pool: 32, radius: 30, landingRadius: 1.2, fallDamage: 8, priority: "mixed", mixedRatio: 0.5 };
2019
+ var DENSITY_DEFAULTS = { cell: 2, capacity: 4, pressurePerBody: 150 };
2020
+ var BLAST_DEFAULTS = { pointsPerKilojoule: 0.6, throwSpeed: 4, maxBlastsPerStep: 8, limbEffectsPerBlast: 24, bloodEffectsPerBlast: 32, shove: {} };
2021
+ var PERSONALITY_DEFAULTS = {
2022
+ speed: [0.9, 1.1],
2023
+ reaction: [0.05, 0.35],
2024
+ stability: [0.75, 1.25],
2025
+ trip: [0.5, 1.5],
2026
+ drive: [0.7, 1.3],
2027
+ cadence: [0.85, 1.15],
2028
+ aggression: [0.8, 1.2],
2029
+ idle: [3, 2, 2, 1]
2030
+ };
2031
+ var PLAYER_PRESSURE_DEFAULTS = { enabled: false, knockThreshold: 1500, crushDamagePerSecond: 6 };
2032
+ var DOOR_PRESSURE_DAMAGE = 4e-3;
2033
+ var DETERMINISTIC_FLOW_CELLS = 2e4;
2034
+ var CROWD_PHYSICS_DEFAULTS = {
2035
+ contact: CONTACT_DEFAULTS,
2036
+ pressure: PRESSURE_DEFAULTS,
2037
+ balance: BALANCE_DEFAULTS,
2038
+ mound: MOUND_DEFAULTS,
2039
+ thrown: THROWN_DEFAULTS,
2040
+ density: DENSITY_DEFAULTS,
2041
+ blast: BLAST_DEFAULTS,
2042
+ personality: PERSONALITY_DEFAULTS,
2043
+ door: { pressureDamage: DOOR_PRESSURE_DAMAGE },
2044
+ playerPressure: PLAYER_PRESSURE_DEFAULTS,
2045
+ deterministic: true
2046
+ };
2047
+ var part = (option, defaults) => {
2048
+ if (option === void 0 || option === false) return null;
2049
+ return option === true ? defaults : { ...defaults, ...option };
2050
+ };
2051
+ function resolvePhysics(physics = {}) {
2052
+ const contact = part(physics.contact, CONTACT_DEFAULTS);
2053
+ const pressure = part(physics.pressure, PRESSURE_DEFAULTS);
2054
+ const balance = part(physics.balance, BALANCE_DEFAULTS);
2055
+ const mound = part(physics.mound, MOUND_DEFAULTS);
2056
+ const thrown = part(physics.thrown, THROWN_DEFAULTS);
2057
+ const density = part(physics.density, DENSITY_DEFAULTS);
2058
+ const playerPressure = { ...PLAYER_PRESSURE_DEFAULTS, ...physics.playerPressure };
2059
+ const missing = [];
2060
+ if (!contact && pressure) missing.push("pressure needs contact");
2061
+ if (!contact && balance) missing.push("balance needs contact");
2062
+ if (!balance && mound) missing.push("mound needs balance");
2063
+ if (!balance && thrown) missing.push("thrown needs balance");
2064
+ if (!balance && density) missing.push("density needs balance");
2065
+ if (!pressure && playerPressure.enabled) missing.push("playerPressure needs pressure");
2066
+ if (!contact && physics.worker === "on") missing.push("worker needs contact");
2067
+ if (missing.length) throw new Error(`crowd physics: ${missing.join("; ")}`);
2068
+ return {
2069
+ contact,
2070
+ pressure,
2071
+ balance,
2072
+ mound,
2073
+ thrown,
2074
+ density,
2075
+ blast: { ...BLAST_DEFAULTS, ...physics.blast },
2076
+ personality: part(physics.personality, PERSONALITY_DEFAULTS),
2077
+ doorPressureDamage: physics.door?.pressureDamage ?? DOOR_PRESSURE_DAMAGE,
2078
+ playerPressure,
2079
+ deterministic: physics.deterministic ?? false,
2080
+ worker: physics.worker ?? "off",
2081
+ workerFactory: physics.workerFactory,
2082
+ workerTransport: physics.workerTransport,
2083
+ workerLatency: physics.workerLatency ?? 1
2084
+ };
2085
+ }
2086
+
233
2087
  // src/rng.ts
234
2088
  function createRng(seed) {
235
2089
  let state = seed >>> 0;
@@ -302,7 +2156,38 @@ var createStore = (capacity) => {
302
2156
  lod: u8(),
303
2157
  variant: u8(),
304
2158
  attackPose: u8(),
305
- arms: f32()
2159
+ arms: f32(),
2160
+ stepDir: new Int32Array(capacity),
2161
+ // physics (CrowdOptions.physics): they exist with it off, so the store keeps its shape; the game replays never read them
2162
+ pushX: f32(),
2163
+ pushZ: f32(),
2164
+ mass: f32(),
2165
+ pressure: f32(),
2166
+ field: f32(),
2167
+ fieldPrev: f32(),
2168
+ aimX: f32(),
2169
+ aimZ: f32(),
2170
+ directShove: f32(),
2171
+ blastStep: new Int32Array(capacity),
2172
+ balance: u8(),
2173
+ balanceTimer: f32(),
2174
+ fallCause: u8(),
2175
+ identity: new Uint32Array(capacity),
2176
+ stability: f32(),
2177
+ tripFactor: f32(),
2178
+ driveFactor: f32(),
2179
+ reaction: f32(),
2180
+ cadenceFactor: f32(),
2181
+ aggression: f32(),
2182
+ idle: u8(),
2183
+ gaitStyle: u8(),
2184
+ wounds: new Uint16Array(capacity),
2185
+ leanX: f32(),
2186
+ leanZ: f32(),
2187
+ thrown: new Int8Array(capacity),
2188
+ prevX: f32(),
2189
+ prevZ: f32(),
2190
+ prevY: f32()
306
2191
  };
307
2192
  };
308
2193
  var AGENT_FIELDS = [
@@ -358,26 +2243,39 @@ var AGENT_FIELDS = [
358
2243
  "lod",
359
2244
  "variant",
360
2245
  "attackPose",
361
- "arms"
2246
+ "arms",
2247
+ "stepDir",
2248
+ "pushX",
2249
+ "pushZ",
2250
+ "mass",
2251
+ "pressure",
2252
+ "field",
2253
+ "fieldPrev",
2254
+ "aimX",
2255
+ "aimZ",
2256
+ "directShove",
2257
+ "blastStep",
2258
+ "balance",
2259
+ "balanceTimer",
2260
+ "fallCause",
2261
+ "identity",
2262
+ "stability",
2263
+ "tripFactor",
2264
+ "driveFactor",
2265
+ "reaction",
2266
+ "cadenceFactor",
2267
+ "aggression",
2268
+ "idle",
2269
+ "gaitStyle",
2270
+ "wounds",
2271
+ "leanX",
2272
+ "leanZ",
2273
+ "thrown",
2274
+ "prevX",
2275
+ "prevZ",
2276
+ "prevY"
362
2277
  ];
363
2278
 
364
- // src/types.ts
365
- var STATE = {
366
- WANDER: 0,
367
- CHASE: 1,
368
- GRAB: 2,
369
- STUNNED: 3,
370
- CALLING: 4,
371
- AIRBORNE: 5,
372
- DORMANT: 6,
373
- SWELLING: 7,
374
- SWING: 8,
375
- EATING: 9,
376
- PILE_STEP: 10,
377
- SEEKING_SOUND: 12
378
- };
379
- var MISSING = { SKULL: 1, ARM_L: 2, ARM_R: 4, LEG_L: 8, LEG_R: 16, ARM_L_BROKEN: 32, ARM_R_BROKEN: 64, LEG_L_BROKEN: 128, LEG_R_BROKEN: 256, JAW: 512 };
380
-
381
2279
  // src/crowd.ts
382
2280
  var CROWD_DEFAULTS = {
383
2281
  sources: defaults_default.sources,
@@ -385,6 +2283,7 @@ var CROWD_DEFAULTS = {
385
2283
  numbers: defaults_default.numbers
386
2284
  };
387
2285
  var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
2286
+ var NO_TRAITS = {};
388
2287
  var TAU_APPROX = 6.2832;
389
2288
  var TURN = 6.28;
390
2289
  function createCrowd(options) {
@@ -406,10 +2305,25 @@ function createCrowd(options) {
406
2305
  const HALF = grid.half;
407
2306
  const GC = grid.cell;
408
2307
  const FW = grid.n;
2308
+ const physics = resolvePhysics(options.physics);
2309
+ const flowCells = options.flow?.cellsPerStep ?? (physics.deterministic ? DETERMINISTIC_FLOW_CELLS : void 0);
2310
+ const seed32 = (options.seed ?? 1) >>> 0;
2311
+ let serial = 0;
2312
+ const moundHeight = new Uint16Array(physics.mound ? FW * FW : 0);
2313
+ const moundOn = physics.mound !== null;
2314
+ const balanceOn = physics.balance !== null;
2315
+ const staggerSpeed = physics.pressure?.staggerSpeed ?? 1;
2316
+ const separationMax = physics.contact?.separationMax ?? Infinity;
2317
+ const RESERVED_SLOTS = 2;
2318
+ const WALK_DOWN = 0.6;
2319
+ const ATTACK_ENTER = 2.56;
2320
+ const ATTACK_LEAVE = 4.84;
409
2321
  const HC = N.hashCell;
410
2322
  const HN = Math.floor(grid.size / HC);
411
- const hHead = new Int32Array(HN * HN);
2323
+ const hHead = new Int32Array(HN * HN).fill(-1);
412
2324
  const hNext = new Int32Array(Z.capacity);
2325
+ const hashTouched = new Int32Array(Z.capacity);
2326
+ let hashTouchedN = 0;
413
2327
  const pile = new Float32Array(FW * FW);
414
2328
  let pileN = 0;
415
2329
  const flow = {
@@ -442,7 +2356,7 @@ function createCrowd(options) {
442
2356
  }
443
2357
  const typeIndexById = new Map(types.map((type, index) => [type.id, index]));
444
2358
  const noises = [];
445
- const stats = { bands: [0, 0, 0, 0], slots: 0, kills: 0, heads: 0 };
2359
+ const stats = { bands: [0, 0, 0, 0], slots: 0, kills: 0, heads: 0, physics: createPhysicsStats() };
446
2360
  const corpses = createCorpseLedger({ max: N.corpsesMax, seconds: N.corpseSeconds });
447
2361
  const trail = [];
448
2362
  let trailLast = { x: 1e9, z: 1e9 };
@@ -477,7 +2391,7 @@ function createCrowd(options) {
477
2391
  return list[0];
478
2392
  };
479
2393
  const T = (i) => types[Z.type[i]];
480
- const traits = (i) => T(i).traits ?? {};
2394
+ const traits = (i) => T(i).traits ?? NO_TRAITS;
481
2395
  const special = (i) => {
482
2396
  const tr = traits(i);
483
2397
  Z.y[i] = 0;
@@ -560,13 +2474,18 @@ function createCrowd(options) {
560
2474
  Z.wz[i] = z;
561
2475
  Z.color[i] = RO.color || pack.palette[Math.floor(R() * pack.palette.length)];
562
2476
  Z.skin[i] = R();
2477
+ seatPhysics(Z, i, { seed: seed32, serial: serial++, baseMass: physics.contact?.baseMass ?? 70, personality: physics.personality });
563
2478
  special(i);
564
2479
  emit("spawn", { agent: i, type: C.id });
565
2480
  return i;
566
2481
  };
567
2482
  const remove = (i) => {
568
- if (Z.state[i] === STATE.PILE_STEP) stepRelease(i);
2483
+ if (Z.state[i] === STATE.PILE_STEP) {
2484
+ if (engine.active) releasePileStep(i);
2485
+ else stepRelease(i);
2486
+ }
569
2487
  const j = --Z.n;
2488
+ engine.remap(i, j);
570
2489
  if (i === j) {
571
2490
  emit("remove", { agent: i, moved: -1 });
572
2491
  return;
@@ -600,9 +2519,10 @@ function createCrowd(options) {
600
2519
  flow.c = 0;
601
2520
  flow.t = 0;
602
2521
  };
603
- const flowStep = (budget) => {
2522
+ const flowStep = (budget, cellsPerStep) => {
604
2523
  if (!flow.phase) return;
605
2524
  const t0 = clock();
2525
+ let cellsLeft = cellsPerStep ?? Infinity;
606
2526
  const D = flow.dist;
607
2527
  const Q = flow.q;
608
2528
  const F = flow.back;
@@ -613,6 +2533,7 @@ function createCrowd(options) {
613
2533
  flow.phase = 2;
614
2534
  break;
615
2535
  }
2536
+ if (--cellsLeft < 0) return;
616
2537
  const c = Q[flow.qh++];
617
2538
  const cx = c % FW;
618
2539
  const cz = c / FW | 0;
@@ -632,13 +2553,14 @@ function createCrowd(options) {
632
2553
  if (flow.qt < Q.length) Q[flow.qt++] = k;
633
2554
  }
634
2555
  }
635
- if ((++n & 2047) === 0 && clock() - t0 > budget) {
2556
+ if (cellsPerStep === void 0 && (++n & 2047) === 0 && clock() - t0 > budget) {
636
2557
  flow.t += clock() - t0;
637
2558
  return;
638
2559
  }
639
2560
  }
640
2561
  const ww = W[2] - W[0] + 1;
641
2562
  while (flow.phase === 2) {
2563
+ if (--cellsLeft < 0) return;
642
2564
  const j = flow.c++;
643
2565
  if (j >= ww * (W[3] - W[1] + 1)) {
644
2566
  const tmp = flow.dir;
@@ -675,7 +2597,7 @@ function createCrowd(options) {
675
2597
  }
676
2598
  F[c * 2] = bx;
677
2599
  F[c * 2 + 1] = bz;
678
- if ((j & 8191) === 0 && clock() - t0 > budget) {
2600
+ if (cellsPerStep === void 0 && (j & 8191) === 0 && clock() - t0 > budget) {
679
2601
  flow.t += clock() - t0;
680
2602
  return;
681
2603
  }
@@ -686,14 +2608,14 @@ function createCrowd(options) {
686
2608
  if (c < 0) return 0;
687
2609
  const k = occ[c];
688
2610
  if (k === 2 || k === 1 && occH[c] <= _y + 0.05) return occH[c];
689
- return pile[c];
2611
+ return moundOn ? pile[c] + moundHeight[c] / 1e3 : pile[c];
690
2612
  };
691
2613
  const blockedAt = (i, x, z) => {
692
2614
  const c = cellOf(x, z);
693
2615
  if (c < 0) return true;
694
2616
  const k = occ[c];
695
2617
  const y = Z.y[i];
696
- if (k === 0 || k === 3) return pile[c] > y + 0.5 && Z.state[i] !== STATE.AIRBORNE;
2618
+ if (k === 0 || k === 3) return (moundOn ? pile[c] + moundHeight[c] / 1e3 : pile[c]) > y + 0.5 && Z.state[i] !== STATE.AIRBORNE;
697
2619
  if (k === 2 && y + 0.5 >= occH[c]) return false;
698
2620
  if (Z.state[i] === STATE.AIRBORNE && occH[c] <= y) return false;
699
2621
  return true;
@@ -713,6 +2635,7 @@ function createCrowd(options) {
713
2635
  pileN++;
714
2636
  Z.state[i] = STATE.PILE_STEP;
715
2637
  Z.stepCell[i] = c;
2638
+ Z.stepDir[i] = dir;
716
2639
  Z.vx[i] = Z.vz[i] = 0;
717
2640
  Z.y[i] = h - N.pileStep;
718
2641
  Z.stateTimer[i] = 6 + R() * 6;
@@ -736,6 +2659,11 @@ function createCrowd(options) {
736
2659
  Z.stateTimer[i] = 1.2;
737
2660
  }
738
2661
  };
2662
+ const pileColumns = [];
2663
+ const releasePileStep = (i) => {
2664
+ if (Z.stepCell[i] >= 0) pileColumns.push({ cell: Z.stepCell[i], dir: Z.stepDir[i] });
2665
+ stepRelease(i);
2666
+ };
739
2667
  const voice = (i) => traits(i).voice ?? "groan";
740
2668
  const groan = (i, d) => {
741
2669
  const r2 = N.alertRadius * N.alertRadius;
@@ -769,9 +2697,9 @@ function createCrowd(options) {
769
2697
  const attack = (i, d, tdt) => {
770
2698
  Z.attackTimer[i] -= tdt;
771
2699
  if (Z.attackTimer[i] > 0) return;
772
- Z.attackTimer[i] = N.attackEvery;
2700
+ Z.attackTimer[i] = N.attackEvery / Z.aggression[i];
773
2701
  const C = T(i);
774
- const tr = C.traits ?? {};
2702
+ const tr = C.traits ?? NO_TRAITS;
775
2703
  if (tr.heavySwing && R() < tr.heavySwing.chance) {
776
2704
  Z.state[i] = STATE.SWING;
777
2705
  Z.stateTimer[i] = tr.heavySwing.windUp;
@@ -780,7 +2708,7 @@ function createCrowd(options) {
780
2708
  const noJaw = Z.missing[i] & 512;
781
2709
  const noArms = (Z.missing[i] & 6) === 6;
782
2710
  const P = target;
783
- if (P.state.grabbedBy < 0 && !noArms && !Z.crawl[i] && R() < C.bite * (P.state.crouch ? N.crouchBite : 1)) {
2711
+ if (P.state.grabbedBy < 0 && !noArms && !Z.crawl[i] && R() < C.bite * (P.state.crouch ? N.crouchBite : 1) * Z.aggression[i]) {
784
2712
  P.grab(i, !!noJaw, !!tr.drownGrab && water(P.state.x, P.state.z));
785
2713
  Z.state[i] = STATE.GRAB;
786
2714
  } else {
@@ -791,7 +2719,7 @@ function createCrowd(options) {
791
2719
  };
792
2720
  const effect = (payload) => emit("effect", payload);
793
2721
  const blood = (x, y, z, age, amount, died, agent) => effect({ effect: "blood", point: [x, y, z], age, amount, died, ...agent === void 0 ? {} : { agent } });
794
- const damage = (i, hit) => {
2722
+ const damageWith = (i, hit, draw, deferDeath = false) => {
795
2723
  const C = T(i);
796
2724
  const A = pack.ages[Z.age[i]];
797
2725
  const RO = pack.outfits[Z.outfit[i]];
@@ -818,7 +2746,7 @@ function createCrowd(options) {
818
2746
  } else {
819
2747
  Z.hp[i] -= dano * 3;
820
2748
  Z.headHp[i] -= dano;
821
- if (dano > N.jawDamage && R() < N.jawChance) Z.missing[i] |= 512;
2749
+ if (dano > N.jawDamage && draw() < N.jawChance) Z.missing[i] |= 512;
822
2750
  }
823
2751
  } else if (regiao === 1) {
824
2752
  let m = C.multipliers.torso;
@@ -848,7 +2776,8 @@ function createCrowd(options) {
848
2776
  if (Z.hp[i] <= 0) morreu = true;
849
2777
  blood(Z.x[i], regiao === 0 ? Z.height[i] * 1.65 : regiao === 1 ? Z.height[i] * 1.2 : Z.height[i] * 0.7, Z.z[i], Z.age[i], regiao === 0 ? 120 : 30, morreu, i);
850
2778
  if (morreu) {
851
- kill(i, headshot);
2779
+ if (deferDeath) deferKill(i);
2780
+ else kill(i, headshot, draw);
852
2781
  return { died: morreu, headshot };
853
2782
  }
854
2783
  const st = Z.state[i];
@@ -865,14 +2794,15 @@ function createCrowd(options) {
865
2794
  }
866
2795
  return { died: morreu };
867
2796
  };
868
- function kill(i, headshot = false) {
2797
+ const damage = (i, hit) => damageWith(i, hit, R);
2798
+ function kill(i, headshot = false, draw = R) {
869
2799
  stats.kills++;
870
2800
  if (headshot) stats.heads++;
871
2801
  const tr = traits(i);
872
2802
  if (tr.burst) burstAt(i, Z.x[i], Z.z[i]);
873
2803
  const C = T(i);
874
2804
  emit("kill", { agent: i, type: C.id, headshot, x: Z.x[i], z: Z.z[i] });
875
- corpseAdd(i);
2805
+ if (Z.balance[i] !== BALANCE.THROWN) corpseAdd(i, draw);
876
2806
  if (target && target.state.grabbedBy === i) target.release();
877
2807
  remove(i);
878
2808
  }
@@ -891,24 +2821,23 @@ function createCrowd(options) {
891
2821
  }
892
2822
  emitNoise(x, z, b.noise);
893
2823
  };
894
- const corpseAdd = (i) => {
895
- corpses.add({
896
- x: Z.x[i],
897
- z: Z.z[i],
898
- yaw: Z.renderYaw[i],
899
- height: Z.height[i],
900
- build: Z.build[i],
901
- named: Z.named[i],
902
- missing: Z.missing[i],
903
- color: Z.color[i],
904
- variant: Z.variant[i],
905
- skin: Z.skin[i],
906
- seed: R() * 10
907
- });
908
- };
2824
+ const corpseOf = (i, seed) => ({
2825
+ x: Z.x[i],
2826
+ z: Z.z[i],
2827
+ yaw: Z.renderYaw[i],
2828
+ height: Z.height[i],
2829
+ build: Z.build[i],
2830
+ named: Z.named[i],
2831
+ missing: Z.missing[i],
2832
+ color: Z.color[i],
2833
+ variant: Z.variant[i],
2834
+ skin: Z.skin[i],
2835
+ seed
2836
+ });
2837
+ const corpseAdd = (i, draw = R) => void corpses.add(corpseOf(i, draw() * 10));
909
2838
  const ability = (i, d, dx, dz, tdt, b, alive) => {
910
2839
  const C = T(i);
911
- const tr = C.traits ?? {};
2840
+ const tr = C.traits ?? NO_TRAITS;
912
2841
  const st = Z.state[i];
913
2842
  const P = target;
914
2843
  if (st === STATE.DORMANT) {
@@ -1114,6 +3043,7 @@ function createCrowd(options) {
1114
3043
  if (tr.submerged && water(Z.x[i], Z.z[i])) return tr.submerged.swim;
1115
3044
  return Z.chaseSpeed[i];
1116
3045
  };
3046
+ const physicsDraw = (i, salt) => engine.active ? () => unit(Z.identity[i], engine.ctx.step, salt) : R;
1117
3047
  const hurtOther = (j, amount) => {
1118
3048
  Z.hp[j] -= amount;
1119
3049
  blood(Z.x[j], Z.height[j] * 1.2, Z.z[j], Z.age[j], 30, false, j);
@@ -1125,7 +3055,7 @@ function createCrowd(options) {
1125
3055
  const killQFlush = () => {
1126
3056
  if (!killQ.length) return;
1127
3057
  killQ.sort((a, b) => b - a);
1128
- for (const j of killQ) if (j < Z.n) kill(j, false);
3058
+ for (const j of killQ) if (j < Z.n) kill(j, false, physicsDraw(j, SALT.CORPSE));
1129
3059
  killQ.length = 0;
1130
3060
  };
1131
3061
  const frame = (i, dt, d, alive) => {
@@ -1148,9 +3078,14 @@ function createCrowd(options) {
1148
3078
  Z.hp[i] *= tr.waterWeak.keep;
1149
3079
  effect({ effect: "cloud", point: [Z.x[i], 0, Z.z[i]], radius: 1.5, color: tr.waterWeak.cloud, amount: 3, agent: i });
1150
3080
  }
3081
+ if (engine.active && (Z.balance[i] === BALANCE.FALLEN || Z.balance[i] === BALANCE.GETTING_UP)) return;
1151
3082
  const c = cellOf(Z.x[i], Z.z[i]);
1152
3083
  const gy = groundAt(c, Z.y[i]);
1153
3084
  if (st === STATE.DORMANT || Z.floorLocked[i]) return;
3085
+ if (engine.active && st !== STATE.AIRBORNE && Z.vy[i] === 0 && Z.y[i] > gy + 0.02 && Z.y[i] - gy <= WALK_DOWN) {
3086
+ Z.y[i] = Math.max(gy, Z.y[i] - N.climbSpeed * dt);
3087
+ return;
3088
+ }
1154
3089
  if (st === STATE.AIRBORNE || Z.y[i] > gy + 0.02 || Z.vy[i] > 0) {
1155
3090
  Z.vy[i] -= N.gravity * dt;
1156
3091
  Z.y[i] += Z.vy[i] * dt;
@@ -1158,6 +3093,9 @@ function createCrowd(options) {
1158
3093
  const fall = -Z.vy[i];
1159
3094
  Z.y[i] = gy;
1160
3095
  Z.vy[i] = 0;
3096
+ const collapsed = st === STATE.AIRBORNE && Z.stateTimer[i] === COLLAPSE_MARK;
3097
+ if (collapsed) Z.stateTimer[i] = 0;
3098
+ if (engine.active && balanceOn && (st !== STATE.AIRBORNE || collapsed)) engine.balance.landed(i, fall * fall / (2 * N.gravity));
1161
3099
  if (st === STATE.AIRBORNE) {
1162
3100
  Z.state[i] = STATE.CHASE;
1163
3101
  if (tr.pounce && alive && d < tr.pounce.landRange && target) {
@@ -1181,6 +3119,9 @@ function createCrowd(options) {
1181
3119
  const pz = P ? P.state.z : context.flowTarget?.[1] ?? 0;
1182
3120
  const alive = !!P && P.state.alive && !P.state.turned;
1183
3121
  viewYaw = context.viewYaw ?? (P ? P.state.yaw - Math.PI : 0);
3122
+ const bandX = context.bandCentre?.[0] ?? px;
3123
+ const bandZ = context.bandCentre?.[1] ?? pz;
3124
+ engine.beginStep(dt, { x: bandX, z: bandZ });
1184
3125
  const fx0 = context.flowTarget?.[0] ?? px;
1185
3126
  const fz0 = context.flowTarget?.[1] ?? pz;
1186
3127
  const pc = cellOf(fx0, fz0);
@@ -1192,14 +3133,18 @@ function createCrowd(options) {
1192
3133
  flowStart(fx0, fz0);
1193
3134
  flow.cell = pc;
1194
3135
  }
1195
- flowStep(N.flowBudgetMs);
1196
- hHead.fill(-1);
3136
+ flowStep(N.flowBudgetMs, flowCells);
3137
+ if (engine.active) {
3138
+ for (let k = 0; k < hashTouchedN; k++) hHead[hashTouched[k]] = -1;
3139
+ hashTouchedN = 0;
3140
+ } else hHead.fill(-1);
1197
3141
  for (let i = 0; i < Z.n; i++) {
1198
- if (Z.band[i] !== 0) continue;
3142
+ if (Z.band[i] !== 0 || engine.active && Z.balance[i] === BALANCE.THROWN) continue;
1199
3143
  const hx = Math.floor(Z.x[i] + HALF);
1200
3144
  const hz = Math.floor(Z.z[i] + HALF);
1201
3145
  if (hx < 0 || hz < 0 || hx >= HN || hz >= HN) continue;
1202
3146
  const h = hz * HN + hx;
3147
+ if (engine.active && hHead[h] === -1) hashTouched[hashTouchedN++] = h;
1203
3148
  hNext[i] = hHead[h];
1204
3149
  hHead[h] = i;
1205
3150
  }
@@ -1211,13 +3156,16 @@ function createCrowd(options) {
1211
3156
  const dx = Z.x[i] - px;
1212
3157
  const dz = Z.z[i] - pz;
1213
3158
  const d = Math.sqrt(dx * dx + dz * dz);
3159
+ const bandD = context.bandCentre ? Math.hypot(Z.x[i] - bandX, Z.z[i] - bandZ) : d;
1214
3160
  let b = Z.band[i];
1215
- while (b > 0 && d < BANDS[b - 1].max - HYST) b--;
1216
- while (b < 3 && d > BANDS[b].max + HYST) b++;
3161
+ while (b > 0 && bandD < BANDS[b - 1].max - HYST) b--;
3162
+ while (b < 3 && bandD > BANDS[b].max + HYST) b++;
1217
3163
  Z.band[i] = b;
1218
3164
  B[b]++;
3165
+ if (engine.active && Z.balance[i] === BALANCE.THROWN) continue;
3166
+ const grounded = engine.active && balanceOn && (Z.balance[i] === BALANCE.FALLEN || Z.balance[i] === BALANCE.GETTING_UP);
1219
3167
  const C = T(i);
1220
- const tr = C.traits ?? {};
3168
+ const tr = C.traits ?? NO_TRAITS;
1221
3169
  if (Z.state[i] === STATE.STUNNED) {
1222
3170
  Z.stateTimer[i] -= dt;
1223
3171
  if (Z.stateTimer[i] <= 0) Z.state[i] = STATE.CHASE;
@@ -1241,8 +3189,12 @@ function createCrowd(options) {
1241
3189
  Z.lost[i] = 0;
1242
3190
  }
1243
3191
  }
1244
- Z.next[i] -= dt;
1245
- if (Z.next[i] <= 0) {
3192
+ if (!grounded) Z.next[i] -= dt;
3193
+ else {
3194
+ Z.vx[i] = Z.vz[i] = Z.dvx[i] = Z.dvz[i] = Z.aimX[i] = Z.aimZ[i] = 0;
3195
+ Z.steering[i] = 0;
3196
+ }
3197
+ if (Z.next[i] <= 0 && !grounded) {
1246
3198
  const tdt = BANDS[b].dt;
1247
3199
  Z.next[i] += tdt;
1248
3200
  if (alive && b <= 1 && Z.state[i] <= 1) {
@@ -1289,6 +3241,7 @@ function createCrowd(options) {
1289
3241
  Z.stateTimer[i] = tr.caller.windUp;
1290
3242
  }
1291
3243
  Z.steering[i] = 0;
3244
+ Z.aimX[i] = Z.aimZ[i] = 0;
1292
3245
  if (!ability(i, d, dx, dz, tdt, b, alive)) {
1293
3246
  let tx = 0;
1294
3247
  let tz = 0;
@@ -1325,7 +3278,7 @@ function createCrowd(options) {
1325
3278
  if (!alive) sp = Z.wanderSpeed[i];
1326
3279
  if (d < N.attackRange && alive && Math.abs(P.state.y - Z.y[i]) < N.attackHeight) {
1327
3280
  sp = 0;
1328
- if (slots < N.attackSlots) {
3281
+ if (slots < N.attackSlots - (Z.aggression[i] < 1 ? RESERVED_SLOTS : 0)) {
1329
3282
  slots++;
1330
3283
  attack(i, d, tdt);
1331
3284
  }
@@ -1347,6 +3300,7 @@ function createCrowd(options) {
1347
3300
  }
1348
3301
  if (Z.crawl[i]) sp = Math.min(sp, N.crawlSpeed);
1349
3302
  else if (Z.limp[i]) sp *= N.limpSpeed;
3303
+ if (Z.balance[i] === BALANCE.STAGGERING) sp *= staggerSpeed;
1350
3304
  let sx = 0;
1351
3305
  let sz = 0;
1352
3306
  if (b === 0) {
@@ -1358,7 +3312,7 @@ function createCrowd(options) {
1358
3312
  const qz = hz + oz;
1359
3313
  if (qx < 0 || qz < 0 || qx >= HN || qz >= HN) continue;
1360
3314
  for (let j = hHead[qz * HN + qx]; j !== -1; j = hNext[j]) {
1361
- if (j === i || Z.state[j] === STATE.PILE_STEP || Z.state[j] === STATE.DORMANT) continue;
3315
+ if (j === i || Z.state[j] === STATE.PILE_STEP || Z.state[j] === STATE.DORMANT || balanceOn && (Z.balance[j] === BALANCE.FALLEN || Z.balance[j] === BALANCE.GETTING_UP)) continue;
1362
3316
  const ex = Z.x[i] - Z.x[j];
1363
3317
  const ez = Z.z[i] - Z.z[j];
1364
3318
  const e2 = ex * ex + ez * ez;
@@ -1373,8 +3327,20 @@ function createCrowd(options) {
1373
3327
  }
1374
3328
  }
1375
3329
  if (Z.state[i] <= 2) {
1376
- Z.dvx[i] = tx * sp + sx * N.separationPush;
1377
- Z.dvz[i] = tz * sp + sz * N.separationPush;
3330
+ if (separationMax !== Infinity) {
3331
+ const push = Math.hypot(sx, sz) * N.separationPush;
3332
+ if (push > separationMax) {
3333
+ sx *= separationMax / push;
3334
+ sz *= separationMax / push;
3335
+ }
3336
+ }
3337
+ if (engine.active && sp > 0.05) {
3338
+ Z.aimX[i] = tx;
3339
+ Z.aimZ[i] = tz;
3340
+ }
3341
+ const slow = engine.densitySlowdown(i, b);
3342
+ Z.dvx[i] = (tx * sp + sx * N.separationPush) * slow;
3343
+ Z.dvz[i] = (tz * sp + sz * N.separationPush) * slow;
1378
3344
  Z.steering[i] = 1;
1379
3345
  if (sp > 0.05) Z.yaw[i] = Math.atan2(tx, tz);
1380
3346
  else if (d < 3 && alive) Z.yaw[i] = Math.atan2(-dx, -dz);
@@ -1386,17 +3352,17 @@ function createCrowd(options) {
1386
3352
  Z.vx[i] += (Z.dvx[i] - Z.vx[i]) * k;
1387
3353
  Z.vz[i] += (Z.dvz[i] - Z.vz[i]) * k;
1388
3354
  }
1389
- const nx = Z.x[i] + Z.vx[i] * dt;
1390
- const nz = Z.z[i] + Z.vz[i] * dt;
3355
+ const nx = Z.x[i] + (Z.vx[i] + Z.pushX[i]) * dt;
3356
+ const nz = Z.z[i] + (Z.vz[i] + Z.pushZ[i]) * dt;
1391
3357
  let hitCell = -1;
1392
3358
  if (!blockedAt(i, nx, Z.z[i])) Z.x[i] = nx;
1393
3359
  else {
1394
- if (Z.state[i] !== STATE.AIRBORNE) Z.vx[i] = 0;
3360
+ if (Z.state[i] !== STATE.AIRBORNE) Z.vx[i] = Z.pushX[i] = 0;
1395
3361
  hitCell = cellOf(nx, Z.z[i]);
1396
3362
  }
1397
3363
  if (!blockedAt(i, Z.x[i], nz)) Z.z[i] = nz;
1398
3364
  else {
1399
- if (Z.state[i] !== STATE.AIRBORNE) Z.vz[i] = 0;
3365
+ if (Z.state[i] !== STATE.AIRBORNE) Z.vz[i] = Z.pushZ[i] = 0;
1400
3366
  hitCell = cellOf(Z.x[i], nz);
1401
3367
  }
1402
3368
  if (hitCell >= 0 && Z.state[i] === STATE.CHASE && b === 0) {
@@ -1408,7 +3374,7 @@ function createCrowd(options) {
1408
3374
  Z.vx[i] = Math.sin(Z.yaw[i]) * s2;
1409
3375
  Z.vz[i] = Math.cos(Z.yaw[i]) * s2;
1410
3376
  } else if (occ[hitCell] === 2 || occ[hitCell] === 0 && pile[hitCell] > 0) {
1411
- Z.stuck[i] += dt;
3377
+ Z.stuck[i] += engine.active && balanceOn ? dt * (1 + engine.pressureRatio(i)) : dt;
1412
3378
  if (Z.stuck[i] > 0.35 + 0.5 * Z.skin[i]) {
1413
3379
  Z.stuck[i] = 0;
1414
3380
  tryStep(i, hitCell);
@@ -1431,12 +3397,18 @@ function createCrowd(options) {
1431
3397
  if (goal && solidAt(Z.x[i] + Math.sin(Z.renderYaw[i]) * 0.62, Z.z[i] + Math.cos(Z.renderYaw[i]) * 0.62, 1)) goal = 0.1;
1432
3398
  Z.arms[i] += (goal - Z.arms[i]) * Math.min(1, dt * (goal > Z.arms[i] ? 3 : 5));
1433
3399
  }
1434
- Z.phase[i] += dt * (Z.renderSpeed[i] < 0.05 ? 0.15 : N.cadenceBase + N.cadencePerSpeed * Z.renderSpeed[i]);
3400
+ Z.phase[i] += dt * (Z.renderSpeed[i] < 0.05 ? 0.15 : N.cadenceBase + N.cadencePerSpeed * Z.renderSpeed[i]) * Z.cadenceFactor[i];
3401
+ if (physics.deterministic) {
3402
+ const attacking = Z.state[i] === STATE.CHASE && alive;
3403
+ Z.attackPose[i] = attacking && (Z.x[i] - P.state.x) ** 2 + (Z.z[i] - P.state.z) ** 2 < (Z.attackPose[i] ? ATTACK_LEAVE : ATTACK_ENTER) ? 1 : 0;
3404
+ }
1435
3405
  if (spd > 0.35 && Z.state[i] <= 1 && !Z.attackPose[i]) Z.yaw[i] = Math.atan2(Z.vx[i], Z.vz[i]);
1436
3406
  let dy = Z.yaw[i] - Z.renderYaw[i];
1437
3407
  dy -= Math.round(dy / TAU_APPROX) * TAU_APPROX;
1438
3408
  Z.renderYaw[i] += dy * Math.min(1, dt * (Z.state[i] === STATE.AIRBORNE ? 20 : 9));
1439
3409
  }
3410
+ engine.endStep();
3411
+ if (pileColumns.length) collapsePileColumns(Z, pileColumns, FW * FW, stepRelease);
1440
3412
  killQFlush();
1441
3413
  noises.length = 0;
1442
3414
  stats.slots = slots;
@@ -1500,6 +3472,31 @@ function createCrowd(options) {
1500
3472
  if (m) emit("message", { id: "migration", seconds: 4 });
1501
3473
  }
1502
3474
  };
3475
+ const engine = createPhysics(
3476
+ {
3477
+ store: Z,
3478
+ grid,
3479
+ doors,
3480
+ gravity: N.gravity,
3481
+ attackEvery: N.attackEvery,
3482
+ types,
3483
+ moundHeight,
3484
+ corpsesMax: N.corpsesMax,
3485
+ typeOf: T,
3486
+ target: () => target,
3487
+ emit,
3488
+ damage: (i, hit, draw) => damageWith(i, hit, draw, true),
3489
+ deferKill,
3490
+ releasePileStep,
3491
+ corpseOf,
3492
+ corpseList: () => corpses.entries,
3493
+ addCorpse: (corpse) => void corpses.add(corpse),
3494
+ groundAt,
3495
+ pileBlast
3496
+ },
3497
+ physics,
3498
+ stats.physics
3499
+ );
1503
3500
  const crowd = {
1504
3501
  store: Z,
1505
3502
  pack,
@@ -1509,6 +3506,9 @@ function createCrowd(options) {
1509
3506
  numbers: N,
1510
3507
  stats,
1511
3508
  pile,
3509
+ mound: moundHeight,
3510
+ thrown: engine.thrown,
3511
+ blast: (blast) => engine.blasts.enqueue(blast),
1512
3512
  pileCount: () => pileN,
1513
3513
  trail,
1514
3514
  variantsOf,
@@ -1525,6 +3525,7 @@ function createCrowd(options) {
1525
3525
  spawn: (x, z, type = -1) => spawn(x, z, type),
1526
3526
  remove,
1527
3527
  update,
3528
+ dispose: () => engine.dispose(),
1528
3529
  emitNoise,
1529
3530
  damage,
1530
3531
  kill: (i, headshot = false) => kill(i, headshot),
@@ -2286,23 +4287,33 @@ function bindInfection(crowd, model, options) {
2286
4287
  }
2287
4288
  export {
2288
4289
  AGENT_FIELDS,
4290
+ BALANCE,
2289
4291
  CELL,
2290
4292
  CIVILIAN_DEFAULTS,
2291
4293
  CROWD_DEFAULTS,
4294
+ CROWD_PHYSICS_DEFAULTS,
2292
4295
  DOOR_DEFAULTS,
4296
+ FALL_CAUSE,
2293
4297
  HERD_DEFAULTS,
2294
4298
  HERD_ROLES,
2295
4299
  MISSING,
2296
4300
  STATE,
4301
+ attachPhysicsWorker,
2297
4302
  bindInfection,
2298
4303
  carrionCenter,
4304
+ contactStep,
2299
4305
  createCivilians,
4306
+ createContactView,
2300
4307
  createCrowd,
2301
4308
  createDoors,
2302
4309
  createFlock,
2303
4310
  createHerd,
2304
4311
  createNavGrid,
2305
4312
  createRng,
2306
- createStore
4313
+ createStore,
4314
+ identityOf,
4315
+ mix32,
4316
+ resolvePhysics,
4317
+ unit
2307
4318
  };
2308
4319
  //# sourceMappingURL=index.js.map