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