@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.global.js
CHANGED
|
@@ -22,27 +22,54 @@ var GameCrowd = (() => {
|
|
|
22
22
|
var index_exports = {};
|
|
23
23
|
__export(index_exports, {
|
|
24
24
|
AGENT_FIELDS: () => AGENT_FIELDS,
|
|
25
|
+
BALANCE: () => BALANCE,
|
|
25
26
|
CELL: () => CELL,
|
|
26
27
|
CIVILIAN_DEFAULTS: () => CIVILIAN_DEFAULTS,
|
|
27
28
|
CROWD_DEFAULTS: () => CROWD_DEFAULTS,
|
|
29
|
+
CROWD_PHYSICS_DEFAULTS: () => CROWD_PHYSICS_DEFAULTS,
|
|
28
30
|
DOOR_DEFAULTS: () => DOOR_DEFAULTS,
|
|
31
|
+
FALL_CAUSE: () => FALL_CAUSE,
|
|
29
32
|
HERD_DEFAULTS: () => HERD_DEFAULTS,
|
|
30
33
|
HERD_ROLES: () => HERD_ROLES,
|
|
31
34
|
MISSING: () => MISSING,
|
|
32
35
|
STATE: () => STATE,
|
|
36
|
+
attachPhysicsWorker: () => attachPhysicsWorker,
|
|
33
37
|
bindInfection: () => bindInfection,
|
|
34
38
|
carrionCenter: () => carrionCenter,
|
|
39
|
+
contactStep: () => contactStep,
|
|
35
40
|
createCivilians: () => createCivilians,
|
|
41
|
+
createContactView: () => createContactView,
|
|
36
42
|
createCrowd: () => createCrowd,
|
|
37
43
|
createDoors: () => createDoors,
|
|
38
44
|
createFlock: () => createFlock,
|
|
39
45
|
createHerd: () => createHerd,
|
|
40
46
|
createNavGrid: () => createNavGrid,
|
|
41
47
|
createRng: () => createRng,
|
|
42
|
-
createStore: () => createStore
|
|
48
|
+
createStore: () => createStore,
|
|
49
|
+
identityOf: () => identityOf,
|
|
50
|
+
mix32: () => mix32,
|
|
51
|
+
resolvePhysics: () => resolvePhysics,
|
|
52
|
+
unit: () => unit
|
|
43
53
|
});
|
|
44
54
|
|
|
45
55
|
// ../game-contracts/dist/index.js
|
|
56
|
+
var DEFAULT_BLAST_SHOVE = { coupling: 0.02, maxSpeed: 20, lift: 0.3, minSpeed: 0.3, falloffPower: 2 };
|
|
57
|
+
function blastShove(blast, target, options = {}) {
|
|
58
|
+
const o = { ...DEFAULT_BLAST_SHOVE, ...options };
|
|
59
|
+
const dx = target.position[0] - blast.point[0];
|
|
60
|
+
const dy = target.position[1] - blast.point[1];
|
|
61
|
+
const dz = target.position[2] - blast.point[2];
|
|
62
|
+
const distance = Math.hypot(dx, dy, dz);
|
|
63
|
+
if (!(blast.radius > 0) || distance > blast.radius) return void 0;
|
|
64
|
+
const falloff = (1 - distance / blast.radius) ** o.falloffPower;
|
|
65
|
+
const mass = Math.max(target.mass, 0.05);
|
|
66
|
+
const speed = Math.min(o.maxSpeed, Math.sqrt(2 * blast.energy * (blast.reach ?? 1) * o.coupling / mass)) * falloff;
|
|
67
|
+
if (!(speed >= o.minSpeed)) return void 0;
|
|
68
|
+
const direction = distance > 1e-4 ? [dx / distance, dy / distance + o.lift, dz / distance] : [0, 1, 0];
|
|
69
|
+
const length = Math.hypot(direction[0], direction[1], direction[2]);
|
|
70
|
+
return { speed, direction: [direction[0] / length, direction[1] / length, direction[2] / length], distance, falloff };
|
|
71
|
+
}
|
|
72
|
+
var DEFAULT_COST_ALLOCATION_BYTES = 1024 * 1024;
|
|
46
73
|
var MISSING_BITS = {
|
|
47
74
|
head: 1,
|
|
48
75
|
armL: 2,
|
|
@@ -55,6 +82,54 @@ var GameCrowd = (() => {
|
|
|
55
82
|
legRBroken: 256,
|
|
56
83
|
jaw: 512
|
|
57
84
|
};
|
|
85
|
+
var DEFERRED_QUEUE_ID = {
|
|
86
|
+
DESTRUCTION_EVENTS: "destruction.events",
|
|
87
|
+
BREACH_PAINT: "floorplan.breach-paint",
|
|
88
|
+
AUDIO_VOICES: "audio.voices",
|
|
89
|
+
VEHICLE_CREASE: "vehicles.crease"
|
|
90
|
+
};
|
|
91
|
+
var DEFERRED_PRIORITY = {
|
|
92
|
+
[DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]: { priority: 300, phase: "present" },
|
|
93
|
+
[DEFERRED_QUEUE_ID.BREACH_PAINT]: { priority: 250, phase: "present" },
|
|
94
|
+
[DEFERRED_QUEUE_ID.AUDIO_VOICES]: { priority: 200, phase: "idle", reserveMs: 1 },
|
|
95
|
+
[DEFERRED_QUEUE_ID.VEHICLE_CREASE]: { priority: 100, phase: "idle" }
|
|
96
|
+
};
|
|
97
|
+
var SURFACE_IDS = ["concrete", "asphalt", "wood", "tile", "dirt", "grass", "gravel", "sand", "metal", "water", "glass", "carpet"];
|
|
98
|
+
var GAIT_IDS = ["walk", "run"];
|
|
99
|
+
var LANDING_IDS = ["light", "heavy"];
|
|
100
|
+
var BODY_SOUND_IDS = [
|
|
101
|
+
"jump",
|
|
102
|
+
"cloth_rustle",
|
|
103
|
+
"body_fall",
|
|
104
|
+
"fall_damage",
|
|
105
|
+
"player_hurt",
|
|
106
|
+
"player_breath_run",
|
|
107
|
+
"player_death",
|
|
108
|
+
"bite",
|
|
109
|
+
"grab",
|
|
110
|
+
"punch_swing",
|
|
111
|
+
"punch_hit_body",
|
|
112
|
+
"punch_hit_face",
|
|
113
|
+
"punch_hit_object",
|
|
114
|
+
"kick_swing",
|
|
115
|
+
"kick_hit",
|
|
116
|
+
"kick_hit_object",
|
|
117
|
+
"shove_two_hands",
|
|
118
|
+
"knife_swing",
|
|
119
|
+
"knife_stab",
|
|
120
|
+
"knife_slash",
|
|
121
|
+
"knife_hit_object",
|
|
122
|
+
"blunt_swing",
|
|
123
|
+
"blunt_hit",
|
|
124
|
+
"blunt_hit_object",
|
|
125
|
+
"axe_swing",
|
|
126
|
+
"axe_chop",
|
|
127
|
+
"axe_hit_object"
|
|
128
|
+
];
|
|
129
|
+
var footstepSoundId = (surface, gait) => `footstep_${surface}_${gait}`;
|
|
130
|
+
var FOOTSTEP_SOUND_IDS = SURFACE_IDS.flatMap((surface) => GAIT_IDS.map((gait) => footstepSoundId(surface, gait)));
|
|
131
|
+
var LAND_SOUND_IDS = SURFACE_IDS.flatMap((surface) => LANDING_IDS.map((landing) => `land_${surface}_${landing}`));
|
|
132
|
+
var BODY_FOLEY_IDS = [...FOOTSTEP_SOUND_IDS, ...LAND_SOUND_IDS, ...BODY_SOUND_IDS];
|
|
58
133
|
|
|
59
134
|
// ../game-characters/dist/index.js
|
|
60
135
|
var __defProp2 = Object.defineProperty;
|
|
@@ -1527,6 +1602,11 @@ var GameCrowd = (() => {
|
|
|
1527
1602
|
|
|
1528
1603
|
// src/grid.ts
|
|
1529
1604
|
var CELL = { FREE: 0, SOLID: 1, LOW_WALL: 2, OPEN_DOOR: 3, GLASS: 4, CLOSED_DOOR: 5 };
|
|
1605
|
+
var createCellOf = (half, cell, n) => (x, z) => {
|
|
1606
|
+
const gx = Math.floor((x + half) / cell);
|
|
1607
|
+
const gz = Math.floor((z + half) / cell);
|
|
1608
|
+
return gx < 0 || gz < 0 || gx >= n || gz >= n ? -1 : gz * n + gx;
|
|
1609
|
+
};
|
|
1530
1610
|
var BORDER_WIDTH = 1;
|
|
1531
1611
|
var BORDER_HEIGHT = 50;
|
|
1532
1612
|
var LOS_STEP = 0.5;
|
|
@@ -1538,11 +1618,7 @@ var GameCrowd = (() => {
|
|
|
1538
1618
|
const n = size / cell;
|
|
1539
1619
|
const occ = new Uint8Array(n * n);
|
|
1540
1620
|
const occH = new Float32Array(n * n);
|
|
1541
|
-
const cellOf = (
|
|
1542
|
-
const gx = Math.floor((x + half) / cell);
|
|
1543
|
-
const gz = Math.floor((z + half) / cell);
|
|
1544
|
-
return gx < 0 || gz < 0 || gx >= n || gz >= n ? -1 : gz * n + gx;
|
|
1545
|
-
};
|
|
1621
|
+
const cellOf = createCellOf(half, cell, n);
|
|
1546
1622
|
const solidAt = (x, z, y = 0.5) => {
|
|
1547
1623
|
const c = cellOf(x, z);
|
|
1548
1624
|
return c < 0 || occ[c] !== 0 && occ[c] !== 3 && occH[c] > y;
|
|
@@ -1596,6 +1672,1864 @@ var GameCrowd = (() => {
|
|
|
1596
1672
|
};
|
|
1597
1673
|
}
|
|
1598
1674
|
|
|
1675
|
+
// src/identity.ts
|
|
1676
|
+
var mix32 = (value) => {
|
|
1677
|
+
let hash = value >>> 0;
|
|
1678
|
+
hash ^= hash >>> 16;
|
|
1679
|
+
hash = Math.imul(hash, 2246822507);
|
|
1680
|
+
hash ^= hash >>> 13;
|
|
1681
|
+
hash = Math.imul(hash, 3266489909);
|
|
1682
|
+
hash ^= hash >>> 16;
|
|
1683
|
+
return hash >>> 0;
|
|
1684
|
+
};
|
|
1685
|
+
var unit = (identity, step, salt) => (mix32(mix32(identity ^ salt) + Math.imul(step + 1, 2654435769) >>> 0) >>> 0) / 4294967296;
|
|
1686
|
+
var identityOf = (seed, serial) => mix32(seed ^ mix32(serial));
|
|
1687
|
+
var SALT = {
|
|
1688
|
+
FALL: 62481,
|
|
1689
|
+
TRIP: 29209,
|
|
1690
|
+
LIMB: 110848,
|
|
1691
|
+
REGION: 189968,
|
|
1692
|
+
JAW: 239536,
|
|
1693
|
+
CORPSE: 311518,
|
|
1694
|
+
SPIN: 364922,
|
|
1695
|
+
SPEED: 413981,
|
|
1696
|
+
REACTION: 466972,
|
|
1697
|
+
STABILITY: 569886,
|
|
1698
|
+
TRIP_FACTOR: 605021,
|
|
1699
|
+
DRIVE: 655571,
|
|
1700
|
+
CADENCE: 728142,
|
|
1701
|
+
AGGRESSION: 797335,
|
|
1702
|
+
IDLE: 867848,
|
|
1703
|
+
GAIT: 937753
|
|
1704
|
+
};
|
|
1705
|
+
|
|
1706
|
+
// src/types.ts
|
|
1707
|
+
var STATE = {
|
|
1708
|
+
WANDER: 0,
|
|
1709
|
+
CHASE: 1,
|
|
1710
|
+
GRAB: 2,
|
|
1711
|
+
STUNNED: 3,
|
|
1712
|
+
CALLING: 4,
|
|
1713
|
+
AIRBORNE: 5,
|
|
1714
|
+
DORMANT: 6,
|
|
1715
|
+
SWELLING: 7,
|
|
1716
|
+
SWING: 8,
|
|
1717
|
+
EATING: 9,
|
|
1718
|
+
PILE_STEP: 10,
|
|
1719
|
+
SEEKING_SOUND: 12
|
|
1720
|
+
};
|
|
1721
|
+
var BALANCE = { STANDING: 0, STAGGERING: 1, FALLEN: 2, GETTING_UP: 3, THROWN: 4 };
|
|
1722
|
+
var FALL_CAUSE = { NONE: 0, PRESSURE: 1, SHOVE: 2, TRIP: 3, LANDING: 4, COLLAPSE: 5, BLAST: 6 };
|
|
1723
|
+
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 };
|
|
1724
|
+
|
|
1725
|
+
// src/physics/balance.ts
|
|
1726
|
+
var GRAVITY_FORCE = 9.8;
|
|
1727
|
+
var GROUND_MASS_FACTOR = 3;
|
|
1728
|
+
var TRIP_MIN_SPEED = 0.5;
|
|
1729
|
+
var TRIP_REFERENCE_SPEED = 1.4;
|
|
1730
|
+
var TRIP_MOUND = 150;
|
|
1731
|
+
var TRIP_CLEARANCE = 0.1;
|
|
1732
|
+
var COLLAPSE_STAGGER = 0.6;
|
|
1733
|
+
var COLLAPSE_FALL = 1;
|
|
1734
|
+
var CAUSE_ORDER = [FALL_CAUSE.PRESSURE, FALL_CAUSE.SHOVE, FALL_CAUSE.TRIP, FALL_CAUSE.LANDING, FALL_CAUSE.COLLAPSE, FALL_CAUSE.BLAST];
|
|
1735
|
+
var CHAIN_WINDOW_STEPS = 240;
|
|
1736
|
+
var CARRY_STEPS = 24;
|
|
1737
|
+
var isCarried = (cause) => cause !== FALL_CAUSE.PRESSURE && cause !== FALL_CAUSE.TRIP;
|
|
1738
|
+
function createBalance(ctx) {
|
|
1739
|
+
const { store } = ctx;
|
|
1740
|
+
const requests = [];
|
|
1741
|
+
const balanceOptions = () => ctx.options.balance;
|
|
1742
|
+
const stabilityOf = (i) => (ctx.typeOf(i).balance?.stability ?? 1) * store.stability[i];
|
|
1743
|
+
const canFall = (i) => ctx.typeOf(i).balance?.canFall !== false;
|
|
1744
|
+
const criticalOf = (i) => balanceOptions().criticalFactor * store.mass[i] * GRAVITY_FORCE * stabilityOf(i);
|
|
1745
|
+
const requestFall = (i, cause, score) => {
|
|
1746
|
+
if (!canFall(i)) return;
|
|
1747
|
+
requests.push({ index: i, identity: store.identity[i], cause, score, born: ctx.step });
|
|
1748
|
+
};
|
|
1749
|
+
const stagger = (i) => {
|
|
1750
|
+
if (store.balance[i] !== BALANCE.STANDING || !canFall(i)) return;
|
|
1751
|
+
store.balance[i] = BALANCE.STAGGERING;
|
|
1752
|
+
store.balanceTimer[i] = 0;
|
|
1753
|
+
};
|
|
1754
|
+
const shove = (i, speed, cause) => {
|
|
1755
|
+
const options = balanceOptions();
|
|
1756
|
+
const knockSpeed = options.shoveFall * stabilityOf(i);
|
|
1757
|
+
if (store.balance[i] !== BALANCE.STANDING && store.balance[i] !== BALANCE.STAGGERING) return;
|
|
1758
|
+
if (speed > knockSpeed) requestFall(i, cause, speed / knockSpeed);
|
|
1759
|
+
else if (speed > options.shoveStagger) stagger(i);
|
|
1760
|
+
};
|
|
1761
|
+
const countsAsChainFall = (i, cause) => {
|
|
1762
|
+
if (cause === FALL_CAUSE.PRESSURE || cause === FALL_CAUSE.BLAST) return false;
|
|
1763
|
+
if (ctx.step - ctx.lastBlastStep > CHAIN_WINDOW_STEPS) return false;
|
|
1764
|
+
const direct = store.blastStep[i] === ctx.lastBlastStep ? store.directShove[i] : 0;
|
|
1765
|
+
return direct < balanceOptions().shoveStagger;
|
|
1766
|
+
};
|
|
1767
|
+
const goDown = (i, cause) => {
|
|
1768
|
+
ctx.stats.falls[CAUSE_ORDER.indexOf(cause)]++;
|
|
1769
|
+
if (countsAsChainFall(i, cause)) ctx.stats.fallsBeyondBlast++;
|
|
1770
|
+
if (store.state[i] === STATE.PILE_STEP) ctx.releasePileStep(i);
|
|
1771
|
+
store.balance[i] = BALANCE.FALLEN;
|
|
1772
|
+
store.balanceTimer[i] = 0;
|
|
1773
|
+
store.fallCause[i] = cause;
|
|
1774
|
+
store.vx[i] = store.vz[i] = 0;
|
|
1775
|
+
store.pushX[i] /= GROUND_MASS_FACTOR;
|
|
1776
|
+
store.pushZ[i] /= GROUND_MASS_FACTOR;
|
|
1777
|
+
store.aimX[i] = store.aimZ[i] = 0;
|
|
1778
|
+
const target = ctx.target();
|
|
1779
|
+
if (target && target.state.grabbedBy === i) target.release();
|
|
1780
|
+
ctx.emit("fall", { agent: i, cause, x: store.x[i], z: store.z[i], identity: store.identity[i] });
|
|
1781
|
+
};
|
|
1782
|
+
const flush = () => {
|
|
1783
|
+
if (!requests.length) return;
|
|
1784
|
+
const options = balanceOptions();
|
|
1785
|
+
requests.sort((a, b) => b.score - a.score || a.identity - b.identity);
|
|
1786
|
+
let fallen = ctx.stats.fallen;
|
|
1787
|
+
let fallsLeft = options.maxFallsPerStep;
|
|
1788
|
+
const waiting = [];
|
|
1789
|
+
for (const request of requests) {
|
|
1790
|
+
const i = request.index;
|
|
1791
|
+
if (i >= store.n || store.identity[i] !== request.identity) continue;
|
|
1792
|
+
const balance = store.balance[i];
|
|
1793
|
+
if (balance !== BALANCE.STANDING && balance !== BALANCE.STAGGERING) continue;
|
|
1794
|
+
if (fallsLeft > 0 && fallen < options.maxFallen) {
|
|
1795
|
+
goDown(i, request.cause);
|
|
1796
|
+
fallsLeft--;
|
|
1797
|
+
fallen++;
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
stagger(i);
|
|
1801
|
+
if (isCarried(request.cause) && ctx.step - request.born < CARRY_STEPS) waiting.push(request);
|
|
1802
|
+
}
|
|
1803
|
+
requests.length = 0;
|
|
1804
|
+
requests.push(...waiting);
|
|
1805
|
+
};
|
|
1806
|
+
const hasBodyOnTop = (i) => {
|
|
1807
|
+
const cell = ctx.grid.cellOf(store.x[i], store.z[i]);
|
|
1808
|
+
if (cell < 0) return false;
|
|
1809
|
+
const options = ctx.options.mound;
|
|
1810
|
+
if (!options) return false;
|
|
1811
|
+
const top = store.y[i] + options.bodyThickness * store.build[i];
|
|
1812
|
+
return ctx.moundAt(cell) / 1e3 > top + 0.05 || ctx.weightOn(i) > 0;
|
|
1813
|
+
};
|
|
1814
|
+
const stepStanding = (i, ratio, contactShove, options) => {
|
|
1815
|
+
if (contactShove > options.shoveFall * stabilityOf(i)) return requestFall(i, FALL_CAUSE.SHOVE, contactShove / (options.shoveFall * stabilityOf(i)));
|
|
1816
|
+
if (ratio > options.staggerEnter || contactShove > options.shoveStagger) stagger(i);
|
|
1817
|
+
else tryTrip(i);
|
|
1818
|
+
};
|
|
1819
|
+
const tryTrip = (i) => {
|
|
1820
|
+
const cell = ctx.grid.cellOf(store.x[i], store.z[i]);
|
|
1821
|
+
if (cell < 0 || cell === ctx.grid.cellOf(store.prevX[i], store.prevZ[i])) return;
|
|
1822
|
+
const mound = ctx.moundAt(cell);
|
|
1823
|
+
if (mound < TRIP_MOUND || store.y[i] >= mound / 1e3 - TRIP_CLEARANCE) return;
|
|
1824
|
+
const speed = Math.hypot(store.vx[i] + store.pushX[i], store.vz[i] + store.pushZ[i]);
|
|
1825
|
+
if (speed <= TRIP_MIN_SPEED) return;
|
|
1826
|
+
const chance = (ctx.typeOf(i).balance?.trip ?? 0.04) * store.tripFactor[i] * (speed / TRIP_REFERENCE_SPEED);
|
|
1827
|
+
if (unit(store.identity[i], ctx.step, SALT.TRIP) < chance) requestFall(i, FALL_CAUSE.TRIP, 1);
|
|
1828
|
+
};
|
|
1829
|
+
const stepStaggering = (i, ratio, contactShove, options) => {
|
|
1830
|
+
store.balanceTimer[i] += ctx.h;
|
|
1831
|
+
if (contactShove > options.shoveFall * stabilityOf(i)) return requestFall(i, FALL_CAUSE.SHOVE, contactShove / (options.shoveFall * stabilityOf(i)));
|
|
1832
|
+
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);
|
|
1833
|
+
if (ratio < options.staggerLeave && store.balanceTimer[i] >= options.staggerMin) {
|
|
1834
|
+
store.balance[i] = BALANCE.STANDING;
|
|
1835
|
+
store.balanceTimer[i] = 0;
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
const stepFallen = (i, ratio, options) => {
|
|
1839
|
+
store.balanceTimer[i] += ctx.h;
|
|
1840
|
+
if (store.balanceTimer[i] < options.fallenMin + store.reaction[i] || ratio >= options.releaseFactor || hasBodyOnTop(i)) return;
|
|
1841
|
+
store.balance[i] = BALANCE.GETTING_UP;
|
|
1842
|
+
store.balanceTimer[i] = 0;
|
|
1843
|
+
};
|
|
1844
|
+
const step = (options, contactShove) => {
|
|
1845
|
+
const smoothing = 1 - Math.exp(-ctx.h / options.pressureTau);
|
|
1846
|
+
let fallen = 0;
|
|
1847
|
+
let staggering = 0;
|
|
1848
|
+
for (let i = 0; i < store.n; i++) {
|
|
1849
|
+
const state = store.balance[i];
|
|
1850
|
+
if (state === BALANCE.THROWN) continue;
|
|
1851
|
+
store.pressure[i] += (store.field[i] - store.pressure[i]) * smoothing;
|
|
1852
|
+
const ratio = store.pressure[i] / criticalOf(i);
|
|
1853
|
+
if (state === BALANCE.STANDING) stepStanding(i, ratio, contactShove[i], options);
|
|
1854
|
+
else if (state === BALANCE.STAGGERING) stepStaggering(i, ratio, contactShove[i], options);
|
|
1855
|
+
else if (state === BALANCE.FALLEN) stepFallen(i, ratio, options);
|
|
1856
|
+
else if ((store.balanceTimer[i] += ctx.h) >= options.getUpSeconds) {
|
|
1857
|
+
store.balance[i] = BALANCE.STANDING;
|
|
1858
|
+
store.balanceTimer[i] = 0;
|
|
1859
|
+
}
|
|
1860
|
+
if (store.balance[i] === BALANCE.STAGGERING) staggering++;
|
|
1861
|
+
else if (store.balance[i] === BALANCE.FALLEN || store.balance[i] === BALANCE.GETTING_UP) fallen++;
|
|
1862
|
+
}
|
|
1863
|
+
ctx.stats.fallen = fallen;
|
|
1864
|
+
ctx.stats.staggering = staggering;
|
|
1865
|
+
};
|
|
1866
|
+
const landed = (i, height) => {
|
|
1867
|
+
if (store.balance[i] !== BALANCE.STANDING && store.balance[i] !== BALANCE.STAGGERING) return;
|
|
1868
|
+
if (height > COLLAPSE_FALL) requestFall(i, FALL_CAUSE.COLLAPSE, height / COLLAPSE_FALL);
|
|
1869
|
+
else if (height > COLLAPSE_STAGGER) stagger(i);
|
|
1870
|
+
};
|
|
1871
|
+
return { requestFall, stagger, shove, step, flush, landed, pending: () => requests.length };
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
// src/physics/blast.ts
|
|
1875
|
+
var CENTRE_HEIGHT = 1;
|
|
1876
|
+
var SHELTERED = 0.5;
|
|
1877
|
+
var DISMEMBER_SHARE = 0.5;
|
|
1878
|
+
var LIMB_CHANCE = 0.5;
|
|
1879
|
+
var STUN_BASE = 0.45;
|
|
1880
|
+
var BLOOD_PER_BODY = 60;
|
|
1881
|
+
var LIMBS = [
|
|
1882
|
+
{ bit: MISSING.ARM_L, region: 2 },
|
|
1883
|
+
{ bit: MISSING.ARM_R, region: 3 },
|
|
1884
|
+
{ bit: MISSING.LEG_L, region: 4 },
|
|
1885
|
+
{ bit: MISSING.LEG_R, region: 5 }
|
|
1886
|
+
];
|
|
1887
|
+
function createBlasts(ctx, hooks) {
|
|
1888
|
+
const { store } = ctx;
|
|
1889
|
+
const options = ctx.options.blast;
|
|
1890
|
+
const queue = [];
|
|
1891
|
+
const emitLimb = (tally, i, region) => {
|
|
1892
|
+
if (tally.limbEffects++ >= options.limbEffectsPerBlast) return;
|
|
1893
|
+
ctx.emit("effect", { effect: "limb", point: [store.x[i], store.height[i] * 0.9, store.z[i]], agent: i, region, severed: true });
|
|
1894
|
+
};
|
|
1895
|
+
const emitBlood = (tally, i) => {
|
|
1896
|
+
if (tally.bloodEffects++ >= options.bloodEffectsPerBlast) {
|
|
1897
|
+
tally.bloodSum += BLOOD_PER_BODY;
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
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 });
|
|
1901
|
+
};
|
|
1902
|
+
const kill = (i, blast, distance, tally) => {
|
|
1903
|
+
tally.killed++;
|
|
1904
|
+
store.hp[i] = 0;
|
|
1905
|
+
if (blast.lethalRadius !== void 0 && distance < DISMEMBER_SHARE * blast.lethalRadius) {
|
|
1906
|
+
for (let k = 0; k < LIMBS.length; k++) {
|
|
1907
|
+
if (unit(store.identity[i], ctx.step, SALT.LIMB + k) >= LIMB_CHANCE) continue;
|
|
1908
|
+
store.missing[i] |= LIMBS[k].bit;
|
|
1909
|
+
emitLimb(tally, i, LIMBS[k].region);
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
emitBlood(tally, i);
|
|
1913
|
+
ctx.deferKill(i);
|
|
1914
|
+
};
|
|
1915
|
+
const hurt = (i, blast, distance, index, reach = 1) => {
|
|
1916
|
+
const falloff = (1 - distance / blast.radius) ** 2 * reach;
|
|
1917
|
+
const amount = blast.energy / 1e3 * options.pointsPerKilojoule * falloff;
|
|
1918
|
+
const draw = (salt) => () => unit(store.identity[i], ctx.step, salt);
|
|
1919
|
+
if (ctx.damage(i, { region: 1, amount, distance, cut: falloff }, draw(SALT.JAW + index)).died) return true;
|
|
1920
|
+
const region = Math.floor(unit(store.identity[i], ctx.step, SALT.REGION + index) * 6);
|
|
1921
|
+
return ctx.damage(i, { region, amount: amount / 2, distance, cut: falloff }, draw(SALT.JAW + 8 + index)).died;
|
|
1922
|
+
};
|
|
1923
|
+
const shoveOf = (i, blasts, reaches) => {
|
|
1924
|
+
const position = [store.x[i], store.y[i] + CENTRE_HEIGHT * store.height[i], store.z[i]];
|
|
1925
|
+
let vx = 0;
|
|
1926
|
+
let vy = 0;
|
|
1927
|
+
let vz = 0;
|
|
1928
|
+
let falloff = 0;
|
|
1929
|
+
for (let b = 0; b < blasts.length; b++) {
|
|
1930
|
+
const blast = blasts[b];
|
|
1931
|
+
const shove = blastShove({ point: blast.point, energy: blast.energy, radius: blast.radius, reach: reaches?.[b] ?? 1 }, { position, mass: store.mass[i] }, options.shove);
|
|
1932
|
+
if (!shove) continue;
|
|
1933
|
+
vx += shove.speed * shove.direction[0];
|
|
1934
|
+
vy += shove.speed * shove.direction[1];
|
|
1935
|
+
vz += shove.speed * shove.direction[2];
|
|
1936
|
+
falloff = Math.max(falloff, shove.falloff);
|
|
1937
|
+
}
|
|
1938
|
+
return vx || vy || vz ? { vx, vy, vz, falloff } : void 0;
|
|
1939
|
+
};
|
|
1940
|
+
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]);
|
|
1941
|
+
const applyShove = (i, shove, candidates, tallies, blasts) => {
|
|
1942
|
+
const horizontal = Math.hypot(shove.vx, shove.vz);
|
|
1943
|
+
const speed = Math.hypot(horizontal, shove.vy);
|
|
1944
|
+
store.directShove[i] = horizontal;
|
|
1945
|
+
store.blastStep[i] = ctx.step;
|
|
1946
|
+
const { balance, thrown } = hooks;
|
|
1947
|
+
if (thrown && speed >= options.throwSpeed && (store.x[i] - ctx.centre.x) ** 2 + (store.z[i] - ctx.centre.z) ** 2 <= ctx.options.thrown.radius ** 2) {
|
|
1948
|
+
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) });
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
if (store.hp[i] <= 0) return;
|
|
1952
|
+
if (!balance) {
|
|
1953
|
+
store.state[i] = STATE.STUNNED;
|
|
1954
|
+
store.stateTimer[i] = STUN_BASE + shove.falloff;
|
|
1955
|
+
store.vx[i] = shove.vx;
|
|
1956
|
+
store.vz[i] = shove.vz;
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
store.pushX[i] += shove.vx;
|
|
1960
|
+
store.pushZ[i] += shove.vz;
|
|
1961
|
+
balance.shove(i, horizontal, FALL_CAUSE.BLAST);
|
|
1962
|
+
for (let b = 0; b < blasts.length; b++) if (distanceTo(i, blasts[b]) <= blasts[b].radius && horizontal > ctx.options.balance.shoveFall) tallies[b].fallen++;
|
|
1963
|
+
};
|
|
1964
|
+
const affect = (i, blasts, tallies, candidates) => {
|
|
1965
|
+
let dead = store.hp[i] <= 0;
|
|
1966
|
+
const reaches = blasts.map(
|
|
1967
|
+
(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
|
|
1968
|
+
);
|
|
1969
|
+
for (let b = 0; b < blasts.length && !dead; b++) {
|
|
1970
|
+
const blast = blasts[b];
|
|
1971
|
+
const distance = distanceTo(i, blast);
|
|
1972
|
+
if (distance > blast.radius) continue;
|
|
1973
|
+
tallies[b].reached++;
|
|
1974
|
+
if (blast.lethalRadius !== void 0 && distance < blast.lethalRadius && reaches[b] >= SHELTERED) kill(i, blast, distance, tallies[b]);
|
|
1975
|
+
else if (hurt(i, blast, distance, b, reaches[b])) tallies[b].killed++;
|
|
1976
|
+
dead = store.hp[i] <= 0 || dead;
|
|
1977
|
+
}
|
|
1978
|
+
const shove = shoveOf(i, blasts, reaches);
|
|
1979
|
+
if (shove) applyShove(i, shove, candidates, tallies, blasts);
|
|
1980
|
+
};
|
|
1981
|
+
const resolve = () => {
|
|
1982
|
+
if (!queue.length) return;
|
|
1983
|
+
const blasts = queue.splice(0, options.maxBlastsPerStep);
|
|
1984
|
+
const tallies = blasts.map(() => ({ reached: 0, killed: 0, fallen: 0, limbEffects: 0, bloodEffects: 0, bloodSum: 0 }));
|
|
1985
|
+
const candidates = [];
|
|
1986
|
+
ctx.lastBlastStep = ctx.step;
|
|
1987
|
+
ctx.stats.blastChecks = store.n * blasts.length;
|
|
1988
|
+
for (let i = 0; i < store.n; i++) if (store.balance[i] !== BALANCE.THROWN) affect(i, blasts, tallies, candidates);
|
|
1989
|
+
const thrown = hooks.thrown?.assign(candidates) ?? 0;
|
|
1990
|
+
blasts.forEach((blast, b) => {
|
|
1991
|
+
const tally = tallies[b];
|
|
1992
|
+
if (tally.bloodSum > 0) ctx.emit("effect", { effect: "blood", point: [blast.point[0], blast.point[1], blast.point[2]], amount: tally.bloodSum, died: true });
|
|
1993
|
+
hooks.pileBlast(blast.point[0], blast.point[2], blast.radius);
|
|
1994
|
+
ctx.emit("blasted", {
|
|
1995
|
+
point: [blast.point[0], blast.point[1], blast.point[2]],
|
|
1996
|
+
radius: blast.radius,
|
|
1997
|
+
reached: tally.reached,
|
|
1998
|
+
killed: tally.killed,
|
|
1999
|
+
thrown: b === 0 ? thrown : 0,
|
|
2000
|
+
fallen: tally.fallen,
|
|
2001
|
+
...blast.chain === void 0 ? {} : { chain: blast.chain }
|
|
2002
|
+
});
|
|
2003
|
+
if (blast.chain !== void 0) ctx.stats.chainLinks++;
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
return { enqueue: (blast) => void queue.push(blast), resolve, pending: () => queue.length };
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
// src/physics/contact.ts
|
|
2010
|
+
var GROUND_MASS_FACTOR2 = 3;
|
|
2011
|
+
var PAIR_MARGIN = 0.15;
|
|
2012
|
+
var MAX_LEVEL_GAP = 0.15;
|
|
2013
|
+
var PAIRS_PER_AGENT = 12;
|
|
2014
|
+
var MIN_DISTANCE = 1e-6;
|
|
2015
|
+
var createContactWorkspace = (capacity, grid) => ({
|
|
2016
|
+
head: new Int32Array(Math.floor(grid.size) ** 2).fill(-1),
|
|
2017
|
+
next: new Int32Array(capacity),
|
|
2018
|
+
touched: new Int32Array(capacity),
|
|
2019
|
+
touchedCount: 0,
|
|
2020
|
+
pairI: new Int32Array(capacity * PAIRS_PER_AGENT),
|
|
2021
|
+
pairJ: new Int32Array(capacity * PAIRS_PER_AGENT),
|
|
2022
|
+
pairCount: 0,
|
|
2023
|
+
deltaX: new Float32Array(capacity),
|
|
2024
|
+
deltaZ: new Float32Array(capacity),
|
|
2025
|
+
count: new Uint8Array(capacity),
|
|
2026
|
+
pushBeforeX: new Float32Array(capacity),
|
|
2027
|
+
pushBeforeZ: new Float32Array(capacity),
|
|
2028
|
+
shove: new Float32Array(capacity),
|
|
2029
|
+
takesPart: new Uint8Array(capacity),
|
|
2030
|
+
radiusOf: new Float64Array(capacity),
|
|
2031
|
+
massOf: new Float64Array(capacity),
|
|
2032
|
+
pairReach: new Float64Array(capacity * PAIRS_PER_AGENT),
|
|
2033
|
+
pairDistance: new Float64Array(capacity * PAIRS_PER_AGENT),
|
|
2034
|
+
pairNormalX: new Float64Array(capacity * PAIRS_PER_AGENT),
|
|
2035
|
+
pairNormalZ: new Float64Array(capacity * PAIRS_PER_AGENT)
|
|
2036
|
+
});
|
|
2037
|
+
var createContactView = (store, grid) => ({ n: 0, store, grid, workspace: createContactWorkspace(store.x.length, grid) });
|
|
2038
|
+
var takesPartInContact = (store, i) => store.band[i] === 0 && store.balance[i] !== BALANCE.THROWN && store.state[i] !== STATE.PILE_STEP && store.state[i] !== STATE.DORMANT;
|
|
2039
|
+
var isOnGround = (store, i) => store.balance[i] === BALANCE.FALLEN || store.balance[i] === BALANCE.GETTING_UP;
|
|
2040
|
+
var effectiveMass = (store, i) => store.mass[i] * (isOnGround(store, i) ? GROUND_MASS_FACTOR2 : 1);
|
|
2041
|
+
var bodyRadius = (store, i, radius) => radius * store.build[i];
|
|
2042
|
+
function readBodies(view, radius) {
|
|
2043
|
+
const { store, workspace } = view;
|
|
2044
|
+
const { takesPart, radiusOf, massOf } = workspace;
|
|
2045
|
+
for (let i = 0; i < view.n; i++) {
|
|
2046
|
+
const takes = takesPartInContact(store, i);
|
|
2047
|
+
takesPart[i] = takes ? 1 : 0;
|
|
2048
|
+
radiusOf[i] = bodyRadius(store, i, radius);
|
|
2049
|
+
massOf[i] = effectiveMass(store, i);
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
function rebuildHash(view) {
|
|
2053
|
+
const { store, grid, workspace } = view;
|
|
2054
|
+
const side = Math.floor(grid.size);
|
|
2055
|
+
for (let k = 0; k < workspace.touchedCount; k++) workspace.head[workspace.touched[k]] = -1;
|
|
2056
|
+
workspace.touchedCount = 0;
|
|
2057
|
+
for (let i = 0; i < view.n; i++) {
|
|
2058
|
+
if (!workspace.takesPart[i]) continue;
|
|
2059
|
+
const hx = Math.floor(store.x[i] + grid.half);
|
|
2060
|
+
const hz = Math.floor(store.z[i] + grid.half);
|
|
2061
|
+
if (hx < 0 || hz < 0 || hx >= side || hz >= side) continue;
|
|
2062
|
+
const cell = hz * side + hx;
|
|
2063
|
+
if (workspace.head[cell] === -1) workspace.touched[workspace.touchedCount++] = cell;
|
|
2064
|
+
workspace.next[i] = workspace.head[cell];
|
|
2065
|
+
workspace.head[cell] = i;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
var FORWARD_CELLS = [1, 0, -1, 1, 0, 1, 1, 1];
|
|
2069
|
+
function addPairsOfCell(view, i, cell, sameCell) {
|
|
2070
|
+
const { store, workspace } = view;
|
|
2071
|
+
const { head, next, radiusOf, pairI, pairJ } = workspace;
|
|
2072
|
+
const { x, z, y } = store;
|
|
2073
|
+
const xi = x[i];
|
|
2074
|
+
const zi = z[i];
|
|
2075
|
+
const yi = y[i];
|
|
2076
|
+
const reachBase = radiusOf[i] + PAIR_MARGIN;
|
|
2077
|
+
for (let j = head[cell]; j !== -1; j = next[j]) {
|
|
2078
|
+
if (sameCell && j <= i || Math.abs(yi - y[j]) > MAX_LEVEL_GAP) continue;
|
|
2079
|
+
const reach = reachBase + radiusOf[j];
|
|
2080
|
+
const ex = x[j] - xi;
|
|
2081
|
+
const ez = z[j] - zi;
|
|
2082
|
+
if (ex * ex + ez * ez >= reach * reach || workspace.pairCount >= pairI.length) continue;
|
|
2083
|
+
pairI[workspace.pairCount] = i;
|
|
2084
|
+
pairJ[workspace.pairCount++] = j;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
function collectPairs(view) {
|
|
2088
|
+
const { store, grid, workspace } = view;
|
|
2089
|
+
const side = Math.floor(grid.size);
|
|
2090
|
+
workspace.pairCount = 0;
|
|
2091
|
+
for (let i = 0; i < view.n; i++) {
|
|
2092
|
+
if (!workspace.takesPart[i]) continue;
|
|
2093
|
+
const hx = Math.floor(store.x[i] + grid.half);
|
|
2094
|
+
const hz = Math.floor(store.z[i] + grid.half);
|
|
2095
|
+
if (hx < 0 || hz < 0 || hx >= side || hz >= side) continue;
|
|
2096
|
+
addPairsOfCell(view, i, hz * side + hx, true);
|
|
2097
|
+
for (let k = 0; k < FORWARD_CELLS.length; k += 2) {
|
|
2098
|
+
const qx = hx + FORWARD_CELLS[k];
|
|
2099
|
+
const qz = hz + FORWARD_CELLS[k + 1];
|
|
2100
|
+
if (qx >= 0 && qz >= 0 && qx < side && qz < side) addPairsOfCell(view, i, qz * side + qx, false);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
function measurePairs(view) {
|
|
2105
|
+
const { store, workspace } = view;
|
|
2106
|
+
const { pairI, pairJ, radiusOf, pairReach, pairDistance, pairNormalX, pairNormalZ } = workspace;
|
|
2107
|
+
const { x, z, identity } = store;
|
|
2108
|
+
for (let p = 0; p < workspace.pairCount; p++) {
|
|
2109
|
+
const i = pairI[p];
|
|
2110
|
+
const j = pairJ[p];
|
|
2111
|
+
const reach = radiusOf[i] + radiusOf[j];
|
|
2112
|
+
const ex = x[i] - x[j];
|
|
2113
|
+
const ez = z[i] - z[j];
|
|
2114
|
+
const distance = Math.sqrt(ex * ex + ez * ez);
|
|
2115
|
+
pairReach[p] = reach;
|
|
2116
|
+
pairDistance[p] = distance;
|
|
2117
|
+
if (distance >= reach) continue;
|
|
2118
|
+
if (distance > MIN_DISTANCE) {
|
|
2119
|
+
pairNormalX[p] = ex / distance;
|
|
2120
|
+
pairNormalZ[p] = ez / distance;
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
const angle = 2 * Math.PI * unit(identity[i] ^ identity[j], 0, SALT.SPIN);
|
|
2124
|
+
pairNormalX[p] = Math.cos(angle);
|
|
2125
|
+
pairNormalZ[p] = Math.sin(angle);
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
function solveImpulse(store, workspace, p) {
|
|
2129
|
+
if (workspace.pairDistance[p] >= workspace.pairReach[p]) return;
|
|
2130
|
+
const i = workspace.pairI[p];
|
|
2131
|
+
const j = workspace.pairJ[p];
|
|
2132
|
+
const nx = workspace.pairNormalX[p];
|
|
2133
|
+
const nz = workspace.pairNormalZ[p];
|
|
2134
|
+
const { vx, vz, pushX, pushZ } = store;
|
|
2135
|
+
const closing = (vx[i] + pushX[i] - vx[j] - pushX[j]) * nx + (vz[i] + pushZ[i] - vz[j] - pushZ[j]) * nz;
|
|
2136
|
+
if (closing >= 0) return;
|
|
2137
|
+
const massI = workspace.massOf[i];
|
|
2138
|
+
const massJ = workspace.massOf[j];
|
|
2139
|
+
const impulse = -closing * massI * massJ / (massI + massJ);
|
|
2140
|
+
pushX[i] += impulse * nx / massI;
|
|
2141
|
+
pushZ[i] += impulse * nz / massI;
|
|
2142
|
+
pushX[j] -= impulse * nx / massJ;
|
|
2143
|
+
pushZ[j] -= impulse * nz / massJ;
|
|
2144
|
+
}
|
|
2145
|
+
function accumulateCorrection(workspace, p, relaxation) {
|
|
2146
|
+
const distance = workspace.pairDistance[p];
|
|
2147
|
+
const reach = workspace.pairReach[p];
|
|
2148
|
+
if (distance >= reach) return;
|
|
2149
|
+
const i = workspace.pairI[p];
|
|
2150
|
+
const j = workspace.pairJ[p];
|
|
2151
|
+
const massI = workspace.massOf[i];
|
|
2152
|
+
const massJ = workspace.massOf[j];
|
|
2153
|
+
const move = relaxation * (reach - distance);
|
|
2154
|
+
const nx = workspace.pairNormalX[p];
|
|
2155
|
+
const nz = workspace.pairNormalZ[p];
|
|
2156
|
+
workspace.deltaX[i] += move * nx * (massJ / (massI + massJ));
|
|
2157
|
+
workspace.deltaZ[i] += move * nz * (massJ / (massI + massJ));
|
|
2158
|
+
workspace.deltaX[j] -= move * nx * (massI / (massI + massJ));
|
|
2159
|
+
workspace.deltaZ[j] -= move * nz * (massI / (massI + massJ));
|
|
2160
|
+
workspace.count[i]++;
|
|
2161
|
+
workspace.count[j]++;
|
|
2162
|
+
}
|
|
2163
|
+
var isOpen = (grid, x, z) => {
|
|
2164
|
+
const cell = grid.cellOf(x, z);
|
|
2165
|
+
return cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3);
|
|
2166
|
+
};
|
|
2167
|
+
function applyCorrection(view, agent, params) {
|
|
2168
|
+
const { store, grid, workspace } = view;
|
|
2169
|
+
const count = workspace.count[agent];
|
|
2170
|
+
if (!count) return;
|
|
2171
|
+
workspace.count[agent] = 0;
|
|
2172
|
+
const cap = params.maxCorrection;
|
|
2173
|
+
const dx = Math.max(-cap, Math.min(cap, workspace.deltaX[agent] / count));
|
|
2174
|
+
const dz = Math.max(-cap, Math.min(cap, workspace.deltaZ[agent] / count));
|
|
2175
|
+
if (isOpen(grid, store.x[agent] + dx, store.z[agent])) store.x[agent] += dx;
|
|
2176
|
+
if (isOpen(grid, store.x[agent], store.z[agent] + dz)) store.z[agent] += dz;
|
|
2177
|
+
}
|
|
2178
|
+
function resetCorrection(workspace, agent) {
|
|
2179
|
+
workspace.deltaX[agent] = 0;
|
|
2180
|
+
workspace.deltaZ[agent] = 0;
|
|
2181
|
+
workspace.count[agent] = 0;
|
|
2182
|
+
}
|
|
2183
|
+
function correctPositions(view, params) {
|
|
2184
|
+
const { workspace } = view;
|
|
2185
|
+
for (let p = 0; p < workspace.pairCount; p++) {
|
|
2186
|
+
resetCorrection(workspace, workspace.pairI[p]);
|
|
2187
|
+
resetCorrection(workspace, workspace.pairJ[p]);
|
|
2188
|
+
}
|
|
2189
|
+
for (let p = 0; p < workspace.pairCount; p++) accumulateCorrection(workspace, p, params.relaxation);
|
|
2190
|
+
for (let p = 0; p < workspace.pairCount; p++) {
|
|
2191
|
+
applyCorrection(view, workspace.pairI[p], params);
|
|
2192
|
+
applyCorrection(view, workspace.pairJ[p], params);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function measureOverlaps(view, out) {
|
|
2196
|
+
const { store, workspace } = view;
|
|
2197
|
+
for (let p = 0; p < workspace.pairCount; p++) {
|
|
2198
|
+
const i = workspace.pairI[p];
|
|
2199
|
+
const j = workspace.pairJ[p];
|
|
2200
|
+
if (isOnGround(store, i) && isOnGround(store, j)) continue;
|
|
2201
|
+
const reach = workspace.radiusOf[i] + workspace.radiusOf[j];
|
|
2202
|
+
const overlap = reach - Math.hypot(store.x[i] - store.x[j], store.z[i] - store.z[j]);
|
|
2203
|
+
if (overlap <= 0) continue;
|
|
2204
|
+
out.contacts++;
|
|
2205
|
+
out.overlapSum += overlap;
|
|
2206
|
+
if (overlap > out.maxOverlap) out.maxOverlap = overlap;
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
function dampPush(view, params) {
|
|
2210
|
+
const { store, workspace } = view;
|
|
2211
|
+
const lambda = Math.exp(-params.friction * params.h);
|
|
2212
|
+
for (let i = 0; i < view.n; i++) {
|
|
2213
|
+
workspace.shove[i] = Math.hypot(store.pushX[i] - workspace.pushBeforeX[i], store.pushZ[i] - workspace.pushBeforeZ[i]);
|
|
2214
|
+
store.pushX[i] *= lambda;
|
|
2215
|
+
store.pushZ[i] *= lambda;
|
|
2216
|
+
const speed = Math.hypot(store.pushX[i], store.pushZ[i]);
|
|
2217
|
+
if (speed <= params.maxPush) continue;
|
|
2218
|
+
store.pushX[i] *= params.maxPush / speed;
|
|
2219
|
+
store.pushZ[i] *= params.maxPush / speed;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function prepareHash(view, radius) {
|
|
2223
|
+
readBodies(view, radius);
|
|
2224
|
+
rebuildHash(view);
|
|
2225
|
+
}
|
|
2226
|
+
function contactStep(view, params, out) {
|
|
2227
|
+
const { store, workspace } = view;
|
|
2228
|
+
out.contacts = 0;
|
|
2229
|
+
out.maxOverlap = 0;
|
|
2230
|
+
out.overlapSum = 0;
|
|
2231
|
+
for (let i = 0; i < view.n; i++) {
|
|
2232
|
+
workspace.pushBeforeX[i] = store.pushX[i];
|
|
2233
|
+
workspace.pushBeforeZ[i] = store.pushZ[i];
|
|
2234
|
+
}
|
|
2235
|
+
readBodies(view, params.radius);
|
|
2236
|
+
rebuildHash(view);
|
|
2237
|
+
collectPairs(view);
|
|
2238
|
+
for (let iteration = 0; iteration < params.iterations; iteration++) {
|
|
2239
|
+
measurePairs(view);
|
|
2240
|
+
for (let p = 0; p < workspace.pairCount; p++) solveImpulse(store, workspace, p);
|
|
2241
|
+
for (let p = workspace.pairCount - 1; p >= 0; p--) solveImpulse(store, workspace, p);
|
|
2242
|
+
correctPositions(view, params);
|
|
2243
|
+
}
|
|
2244
|
+
measureOverlaps(view, out);
|
|
2245
|
+
dampPush(view, params);
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
// src/physics/context.ts
|
|
2249
|
+
var createPhysicsStats = () => ({
|
|
2250
|
+
contacts: 0,
|
|
2251
|
+
maxOverlap: 0,
|
|
2252
|
+
meanOverlap: 0,
|
|
2253
|
+
falls: [0, 0, 0, 0, 0, 0],
|
|
2254
|
+
fallsBeyondBlast: 0,
|
|
2255
|
+
fallen: 0,
|
|
2256
|
+
staggering: 0,
|
|
2257
|
+
climbers: 0,
|
|
2258
|
+
moundMax: 0,
|
|
2259
|
+
thrownActive: 0,
|
|
2260
|
+
thrownPeak: 0,
|
|
2261
|
+
blastChecks: 0,
|
|
2262
|
+
chainLinks: 0,
|
|
2263
|
+
fieldMax: 0,
|
|
2264
|
+
playerPressure: 0
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// src/physics/density.ts
|
|
2268
|
+
var DECISION_STEPS = 12;
|
|
2269
|
+
var FAR_BAND = 1;
|
|
2270
|
+
function createDensity(ctx, options) {
|
|
2271
|
+
const { store, grid } = ctx;
|
|
2272
|
+
const side = Math.ceil(grid.size / options.cell);
|
|
2273
|
+
const bodies = new Uint16Array(side * side);
|
|
2274
|
+
const touched = [];
|
|
2275
|
+
const half = grid.half;
|
|
2276
|
+
const cellOf = (i) => {
|
|
2277
|
+
const gx = Math.floor((store.x[i] + half) / options.cell);
|
|
2278
|
+
const gz = Math.floor((store.z[i] + half) / options.cell);
|
|
2279
|
+
return gx < 0 || gz < 0 || gx >= side || gz >= side ? -1 : gz * side + gx;
|
|
2280
|
+
};
|
|
2281
|
+
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;
|
|
2282
|
+
const count = () => {
|
|
2283
|
+
for (let k = 0; k < touched.length; k++) bodies[touched[k]] = 0;
|
|
2284
|
+
touched.length = 0;
|
|
2285
|
+
for (let i = 0; i < store.n; i++) {
|
|
2286
|
+
if (!isWalking(i)) continue;
|
|
2287
|
+
const cell = cellOf(i);
|
|
2288
|
+
if (cell < 0) continue;
|
|
2289
|
+
if (bodies[cell] === 0) touched.push(cell);
|
|
2290
|
+
bodies[cell]++;
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
const step = () => {
|
|
2294
|
+
if (ctx.step % DECISION_STEPS === 0) count();
|
|
2295
|
+
if (!touched.length) return;
|
|
2296
|
+
for (let i = 0; i < store.n; i++) {
|
|
2297
|
+
if (!isWalking(i)) continue;
|
|
2298
|
+
const cell = cellOf(i);
|
|
2299
|
+
const around = cell < 0 ? 0 : bodies[cell];
|
|
2300
|
+
store.field[i] = around > options.capacity ? (around - options.capacity) * options.pressurePerBody : 0;
|
|
2301
|
+
}
|
|
2302
|
+
};
|
|
2303
|
+
const bodiesAround = (i) => {
|
|
2304
|
+
const cell = cellOf(i);
|
|
2305
|
+
return cell < 0 ? 0 : bodies[cell];
|
|
2306
|
+
};
|
|
2307
|
+
const slowdown = (i, band) => {
|
|
2308
|
+
if (band !== FAR_BAND) return 1;
|
|
2309
|
+
const around = bodiesAround(i);
|
|
2310
|
+
return around > options.capacity ? options.capacity / around : 1;
|
|
2311
|
+
};
|
|
2312
|
+
return { step, slowdown, bodiesAround };
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
// src/physics/mound.ts
|
|
2316
|
+
var CELLS_PER_BODY = 3;
|
|
2317
|
+
var BODY_PART_SPACING = 0.5;
|
|
2318
|
+
var CLIMB_HEIGHT = 0.25;
|
|
2319
|
+
var ELEVATED = 0.05;
|
|
2320
|
+
var NEUTRAL_MASS = 70;
|
|
2321
|
+
var NEIGHBOURS = [-1, 1, -1, 0, -1, -1, 0, -1, 0, 1, 1, -1, 1, 0, 1, 1];
|
|
2322
|
+
var BODY_IDENTITY_SCALE = 1e6;
|
|
2323
|
+
function createMound(ctx, extraBodies, height) {
|
|
2324
|
+
const { store, grid } = ctx;
|
|
2325
|
+
const options = ctx.options.mound;
|
|
2326
|
+
const balance = ctx.options.balance;
|
|
2327
|
+
const cellCount = grid.n * grid.n;
|
|
2328
|
+
const touched = new Int32Array((store.capacity + extraBodies) * CELLS_PER_BODY * 2);
|
|
2329
|
+
let touchedCount = 0;
|
|
2330
|
+
const entryCapacity = (store.capacity + extraBodies) * CELLS_PER_BODY;
|
|
2331
|
+
const entries = { count: 0, cell: new Int32Array(entryCapacity), thickness: new Uint16Array(entryCapacity), identity: new Uint32Array(entryCapacity), agent: new Int32Array(entryCapacity) };
|
|
2332
|
+
const standMass = new Float32Array(cellCount);
|
|
2333
|
+
const standTouched = new Int32Array(store.capacity);
|
|
2334
|
+
let standTouchedCount = 0;
|
|
2335
|
+
const maxMillimetres = Math.round(options.maxHeight * 1e3);
|
|
2336
|
+
const cells = [0, 0, 0];
|
|
2337
|
+
const cellsOfBody = (x, z, yaw) => {
|
|
2338
|
+
const sin = Math.sin(yaw);
|
|
2339
|
+
const cos = Math.cos(yaw);
|
|
2340
|
+
for (let part2 = 0; part2 < CELLS_PER_BODY; part2++) {
|
|
2341
|
+
const offset = (part2 - 1) * BODY_PART_SPACING;
|
|
2342
|
+
cells[part2] = grid.cellOf(x + sin * offset, z + cos * offset);
|
|
2343
|
+
}
|
|
2344
|
+
};
|
|
2345
|
+
const addBody = (x, z, yaw, millimetres, identity, agent) => {
|
|
2346
|
+
cellsOfBody(x, z, yaw);
|
|
2347
|
+
for (const cell of cells) {
|
|
2348
|
+
if (cell < 0) continue;
|
|
2349
|
+
if (height[cell] === 0) touched[touchedCount++] = cell;
|
|
2350
|
+
height[cell] += millimetres;
|
|
2351
|
+
const index = entries.count++;
|
|
2352
|
+
entries.cell[index] = cell;
|
|
2353
|
+
entries.thickness[index] = millimetres;
|
|
2354
|
+
entries.identity[index] = identity;
|
|
2355
|
+
entries.agent[index] = agent;
|
|
2356
|
+
}
|
|
2357
|
+
};
|
|
2358
|
+
const sumBodies = () => {
|
|
2359
|
+
for (let k = 0; k < touchedCount; k++) height[touched[k]] = 0;
|
|
2360
|
+
touchedCount = 0;
|
|
2361
|
+
entries.count = 0;
|
|
2362
|
+
for (let i = 0; i < store.n; i++) {
|
|
2363
|
+
const state = store.balance[i];
|
|
2364
|
+
if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
|
|
2365
|
+
addBody(store.x[i], store.z[i], store.renderYaw[i], Math.round(options.bodyThickness * store.build[i] * 1e3), store.identity[i], i);
|
|
2366
|
+
}
|
|
2367
|
+
const reach = options.corpseRadius * options.corpseRadius;
|
|
2368
|
+
for (const { data } of ctx.corpseList()) {
|
|
2369
|
+
if ((data.x - ctx.centre.x) ** 2 + (data.z - ctx.centre.z) ** 2 > reach) continue;
|
|
2370
|
+
addBody(data.x, data.z, data.yaw, Math.round(options.corpseThickness * data.build * 1e3), mix32(Math.floor(data.seed * BODY_IDENTITY_SCALE)), -1);
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
const isOpenCell = (cell) => grid.occ[cell] === 0 || grid.occ[cell] === 3;
|
|
2374
|
+
const lowestNeighbour = (cell, millimetres) => {
|
|
2375
|
+
const cx = cell % grid.n;
|
|
2376
|
+
const cz = Math.floor(cell / grid.n);
|
|
2377
|
+
let best = -1;
|
|
2378
|
+
for (let k = 0; k < NEIGHBOURS.length; k += 2) {
|
|
2379
|
+
const nx = cx + NEIGHBOURS[k];
|
|
2380
|
+
const nz = cz + NEIGHBOURS[k + 1];
|
|
2381
|
+
if (nx < 0 || nz < 0 || nx >= grid.n || nz >= grid.n) continue;
|
|
2382
|
+
const neighbour = nz * grid.n + nx;
|
|
2383
|
+
if (!isOpenCell(neighbour) || height[neighbour] + millimetres > maxMillimetres) continue;
|
|
2384
|
+
if (best < 0 || height[neighbour] < height[best] || height[neighbour] === height[best] && neighbour < best) best = neighbour;
|
|
2385
|
+
}
|
|
2386
|
+
return best;
|
|
2387
|
+
};
|
|
2388
|
+
const slideEntry = (index) => {
|
|
2389
|
+
const cell = entries.cell[index];
|
|
2390
|
+
const target = lowestNeighbour(cell, entries.thickness[index]);
|
|
2391
|
+
if (target < 0) return;
|
|
2392
|
+
if (height[target] === 0) touched[touchedCount++] = target;
|
|
2393
|
+
height[target] += entries.thickness[index];
|
|
2394
|
+
height[cell] -= entries.thickness[index];
|
|
2395
|
+
entries.cell[index] = target;
|
|
2396
|
+
const agent = entries.agent[index];
|
|
2397
|
+
if (agent < 0) return;
|
|
2398
|
+
store.x[agent] += (target % grid.n - cell % grid.n) * grid.cell;
|
|
2399
|
+
store.z[agent] += (Math.floor(target / grid.n) - Math.floor(cell / grid.n)) * grid.cell;
|
|
2400
|
+
};
|
|
2401
|
+
const capHeights = () => {
|
|
2402
|
+
let over = false;
|
|
2403
|
+
for (let k = 0; k < touchedCount && !over; k++) over = height[touched[k]] > maxMillimetres;
|
|
2404
|
+
if (!over) return;
|
|
2405
|
+
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);
|
|
2406
|
+
let current = -1;
|
|
2407
|
+
let running = 0;
|
|
2408
|
+
for (const index of order) {
|
|
2409
|
+
const cell = entries.cell[index];
|
|
2410
|
+
if (cell !== current) {
|
|
2411
|
+
current = cell;
|
|
2412
|
+
running = 0;
|
|
2413
|
+
}
|
|
2414
|
+
if (height[cell] <= maxMillimetres) continue;
|
|
2415
|
+
if (running + entries.thickness[index] <= maxMillimetres) running += entries.thickness[index];
|
|
2416
|
+
else slideEntry(index);
|
|
2417
|
+
}
|
|
2418
|
+
for (let k = 0; k < touchedCount; k++) if (height[touched[k]] > maxMillimetres) height[touched[k]] = maxMillimetres;
|
|
2419
|
+
};
|
|
2420
|
+
const seatFallen = () => {
|
|
2421
|
+
let top = 0;
|
|
2422
|
+
for (let k = 0; k < touchedCount; k++) top = Math.max(top, height[touched[k]]);
|
|
2423
|
+
ctx.stats.moundMax = top / 1e3;
|
|
2424
|
+
for (let i = 0; i < store.n; i++) {
|
|
2425
|
+
const state = store.balance[i];
|
|
2426
|
+
if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
|
|
2427
|
+
const cell = grid.cellOf(store.x[i], store.z[i]);
|
|
2428
|
+
if (cell >= 0) store.y[i] = Math.max(0, height[cell] / 1e3 - options.bodyThickness * store.build[i]);
|
|
2429
|
+
}
|
|
2430
|
+
};
|
|
2431
|
+
const weighStanding = () => {
|
|
2432
|
+
for (let k = 0; k < standTouchedCount; k++) standMass[standTouched[k]] = 0;
|
|
2433
|
+
standTouchedCount = 0;
|
|
2434
|
+
let climbers = 0;
|
|
2435
|
+
for (let i = 0; i < store.n; i++) {
|
|
2436
|
+
const state = store.balance[i];
|
|
2437
|
+
if (store.band[i] !== 0 || store.state[i] === STATE.PILE_STEP || state !== BALANCE.STANDING && state !== BALANCE.STAGGERING) continue;
|
|
2438
|
+
if (store.y[i] >= CLIMB_HEIGHT) climbers++;
|
|
2439
|
+
if (store.y[i] <= ELEVATED) continue;
|
|
2440
|
+
const cell = grid.cellOf(store.x[i], store.z[i]);
|
|
2441
|
+
if (cell < 0) continue;
|
|
2442
|
+
if (standMass[cell] === 0) standTouched[standTouchedCount++] = cell;
|
|
2443
|
+
standMass[cell] += store.mass[i];
|
|
2444
|
+
}
|
|
2445
|
+
ctx.stats.climbers = climbers;
|
|
2446
|
+
};
|
|
2447
|
+
const trample = () => {
|
|
2448
|
+
if (!standTouchedCount) return;
|
|
2449
|
+
for (let i = 0; i < store.n; i++) {
|
|
2450
|
+
const state = store.balance[i];
|
|
2451
|
+
if (store.band[i] !== 0 || state !== BALANCE.FALLEN && state !== BALANCE.GETTING_UP) continue;
|
|
2452
|
+
const mass = weightOn(i);
|
|
2453
|
+
if (!mass) continue;
|
|
2454
|
+
store.hp[i] -= balance.trampleDps * (mass / NEUTRAL_MASS) * ctx.h;
|
|
2455
|
+
if (store.hp[i] <= 0) ctx.deferKill(i);
|
|
2456
|
+
}
|
|
2457
|
+
};
|
|
2458
|
+
const step = () => {
|
|
2459
|
+
sumBodies();
|
|
2460
|
+
capHeights();
|
|
2461
|
+
seatFallen();
|
|
2462
|
+
weighStanding();
|
|
2463
|
+
trample();
|
|
2464
|
+
};
|
|
2465
|
+
const weightOn = (i) => {
|
|
2466
|
+
cellsOfBody(store.x[i], store.z[i], store.renderYaw[i]);
|
|
2467
|
+
let mass = 0;
|
|
2468
|
+
for (const cell of cells) if (cell >= 0) mass += standMass[cell];
|
|
2469
|
+
return mass;
|
|
2470
|
+
};
|
|
2471
|
+
return { height, step, weightOn };
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/physics/offload/default-worker.ts
|
|
2475
|
+
var import_meta = {};
|
|
2476
|
+
function createDefaultWorkerFactory() {
|
|
2477
|
+
if (typeof Worker === "undefined") return null;
|
|
2478
|
+
let url;
|
|
2479
|
+
try {
|
|
2480
|
+
url = new URL("./physics-worker.js", import_meta.url);
|
|
2481
|
+
} catch {
|
|
2482
|
+
return null;
|
|
2483
|
+
}
|
|
2484
|
+
return () => {
|
|
2485
|
+
const worker = new Worker(url, { type: "module" });
|
|
2486
|
+
return {
|
|
2487
|
+
postMessage: (message, transfer) => worker.postMessage(message, transfer ?? []),
|
|
2488
|
+
onMessage: (listener) => worker.addEventListener("message", (event) => listener(event.data)),
|
|
2489
|
+
terminate: () => worker.terminate()
|
|
2490
|
+
};
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
// src/physics/offload/frame.ts
|
|
2495
|
+
var F32_INPUTS = ["x", "z", "y", "vx", "vz", "pushX", "pushZ", "mass", "build", "aimX", "aimZ", "driveFactor", "fieldPrev"];
|
|
2496
|
+
var U8_INPUTS = ["band", "balance", "state", "type"];
|
|
2497
|
+
var F32_OUTPUTS = ["deltaX", "deltaZ", "deltaPushX", "deltaPushZ", "deltaVx", "deltaVz", "field", "shove", "doorLoad"];
|
|
2498
|
+
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 };
|
|
2499
|
+
var align8 = (bytes) => Math.ceil(bytes / 8) * 8;
|
|
2500
|
+
var frameBytes = ({ capacity, cells }) => HEADER.size * 8 + (F32_INPUTS.length + F32_OUTPUTS.length + 2) * align8(capacity * 4) + U8_INPUTS.length * align8(capacity) + align8(cells);
|
|
2501
|
+
function createFrame(buffer, shape) {
|
|
2502
|
+
const { capacity, cells } = shape;
|
|
2503
|
+
let offset = 0;
|
|
2504
|
+
const take = (make, bytesPerElement, length) => {
|
|
2505
|
+
const view = make(buffer, offset, length);
|
|
2506
|
+
offset += align8(length * bytesPerElement);
|
|
2507
|
+
return view;
|
|
2508
|
+
};
|
|
2509
|
+
const f64 = (b, o, l) => new Float64Array(b, o, l);
|
|
2510
|
+
const f32 = (b, o, l) => new Float32Array(b, o, l);
|
|
2511
|
+
const u8 = (b, o, l) => new Uint8Array(b, o, l);
|
|
2512
|
+
const header = take(f64, 8, HEADER.size);
|
|
2513
|
+
const floats = {};
|
|
2514
|
+
for (const key of [...F32_INPUTS, ...F32_OUTPUTS]) floats[key] = take(f32, 4, capacity);
|
|
2515
|
+
const identity = take((b, o, l) => new Uint32Array(b, o, l), 4, capacity);
|
|
2516
|
+
const doorCell = take((b, o, l) => new Int32Array(b, o, l), 4, capacity);
|
|
2517
|
+
const bytes = {};
|
|
2518
|
+
for (const key of U8_INPUTS) bytes[key] = take(u8, 1, capacity);
|
|
2519
|
+
const occ = take(u8, 1, cells);
|
|
2520
|
+
return { header, f32: floats, u8: bytes, identity, doorCell, occ };
|
|
2521
|
+
}
|
|
2522
|
+
var INPUT_KEYS = { f32: F32_INPUTS, u8: U8_INPUTS };
|
|
2523
|
+
|
|
2524
|
+
// src/physics/offload/offload.ts
|
|
2525
|
+
var isOpen2 = (grid, x, z) => {
|
|
2526
|
+
const cell = grid.cellOf(x, z);
|
|
2527
|
+
return cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3);
|
|
2528
|
+
};
|
|
2529
|
+
function writeSnapshot(frame, source, h, player) {
|
|
2530
|
+
const { store, grid } = source;
|
|
2531
|
+
const n = store.n;
|
|
2532
|
+
for (const key of INPUT_KEYS.f32) frame.f32[key].set(store[key].subarray(0, n));
|
|
2533
|
+
for (const key of INPUT_KEYS.u8) frame.u8[key].set(store[key].subarray(0, n));
|
|
2534
|
+
frame.identity.set(store.identity.subarray(0, n));
|
|
2535
|
+
frame.occ.set(grid.occ);
|
|
2536
|
+
frame.header[HEADER.n] = n;
|
|
2537
|
+
frame.header[HEADER.h] = h;
|
|
2538
|
+
frame.header[HEADER.playerOn] = player ? 1 : 0;
|
|
2539
|
+
frame.header[HEADER.playerX] = player?.x ?? 0;
|
|
2540
|
+
frame.header[HEADER.playerZ] = player?.z ?? 0;
|
|
2541
|
+
}
|
|
2542
|
+
function createOffload(runner, host, latency = 1) {
|
|
2543
|
+
const { store, grid, stats, contact } = host;
|
|
2544
|
+
const queue = [];
|
|
2545
|
+
const spare = Array.from({ length: latency }, () => ({ count: 0, origin: null, originBuffer: new Int32Array(store.capacity) }));
|
|
2546
|
+
const result = { playerLoad: 0 };
|
|
2547
|
+
const apply = (frame, pending) => {
|
|
2548
|
+
const { f32, header } = frame;
|
|
2549
|
+
const { origin, count } = pending;
|
|
2550
|
+
for (let agent = 0; agent < store.n; agent++) {
|
|
2551
|
+
const from = origin ? origin[agent] : agent < count ? agent : -1;
|
|
2552
|
+
if (from < 0) {
|
|
2553
|
+
contact.workspace.shove[agent] = 0;
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
const dx = f32.deltaX[from];
|
|
2557
|
+
const dz = f32.deltaZ[from];
|
|
2558
|
+
if (dx !== 0 && isOpen2(grid, store.x[agent] + dx, store.z[agent])) store.x[agent] += dx;
|
|
2559
|
+
if (dz !== 0 && isOpen2(grid, store.x[agent], store.z[agent] + dz)) store.z[agent] += dz;
|
|
2560
|
+
store.pushX[agent] += f32.deltaPushX[from];
|
|
2561
|
+
store.pushZ[agent] += f32.deltaPushZ[from];
|
|
2562
|
+
store.vx[agent] += f32.deltaVx[from];
|
|
2563
|
+
store.vz[agent] += f32.deltaVz[from];
|
|
2564
|
+
store.field[agent] = f32.field[from];
|
|
2565
|
+
store.fieldPrev[agent] = f32.field[from];
|
|
2566
|
+
contact.workspace.shove[agent] = f32.shove[from];
|
|
2567
|
+
const cell = frame.doorCell[from];
|
|
2568
|
+
if (cell >= 0) host.addDoorLoad(cell, f32.doorLoad[from]);
|
|
2569
|
+
}
|
|
2570
|
+
stats.contacts = header[HEADER.contacts];
|
|
2571
|
+
stats.maxOverlap = header[HEADER.maxOverlap];
|
|
2572
|
+
stats.meanOverlap = header[HEADER.contacts] ? header[HEADER.overlapSum] / header[HEADER.contacts] : 0;
|
|
2573
|
+
stats.fieldMax = header[HEADER.fieldMax];
|
|
2574
|
+
result.playerLoad = header[HEADER.playerLoad];
|
|
2575
|
+
};
|
|
2576
|
+
const step = (h, player) => {
|
|
2577
|
+
result.playerLoad = 0;
|
|
2578
|
+
if (queue.length >= latency) {
|
|
2579
|
+
const frame2 = runner.settle();
|
|
2580
|
+
if (frame2) {
|
|
2581
|
+
const pending = queue.shift();
|
|
2582
|
+
apply(frame2, pending);
|
|
2583
|
+
pending.origin = null;
|
|
2584
|
+
spare.push(pending);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
const frame = runner.frame();
|
|
2588
|
+
if (frame && spare.length) {
|
|
2589
|
+
writeSnapshot(frame, { store, grid }, h, player);
|
|
2590
|
+
runner.submit();
|
|
2591
|
+
const pending = spare.shift();
|
|
2592
|
+
pending.count = store.n;
|
|
2593
|
+
queue.push(pending);
|
|
2594
|
+
}
|
|
2595
|
+
return result;
|
|
2596
|
+
};
|
|
2597
|
+
const remap = (removed, moved) => {
|
|
2598
|
+
queue.forEach((pending) => {
|
|
2599
|
+
if (!pending.origin) {
|
|
2600
|
+
pending.origin = pending.originBuffer.fill(-1);
|
|
2601
|
+
for (let agent = 0; agent < pending.count; agent++) pending.origin[agent] = agent;
|
|
2602
|
+
}
|
|
2603
|
+
pending.origin[removed] = removed === moved ? -1 : pending.origin[moved];
|
|
2604
|
+
if (removed !== moved) pending.origin[moved] = -1;
|
|
2605
|
+
});
|
|
2606
|
+
};
|
|
2607
|
+
return { step, remap, dispose: () => runner.dispose() };
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
// src/physics/pressure.ts
|
|
2611
|
+
var TOUCH_SLACK = 0.08;
|
|
2612
|
+
var WALL_PROBE = 0.12;
|
|
2613
|
+
var PLAYER_REACH = 1.3;
|
|
2614
|
+
var PLAYER_RADIUS_SQUARED = PLAYER_REACH * PLAYER_REACH;
|
|
2615
|
+
var createPressureWorkspace = (capacity, pairCapacity) => ({
|
|
2616
|
+
weightSum: new Float32Array(capacity),
|
|
2617
|
+
pressing: new Uint8Array(capacity),
|
|
2618
|
+
wallKind: new Uint8Array(capacity),
|
|
2619
|
+
wallDoor: new Int32Array(capacity),
|
|
2620
|
+
playerWeight: new Float32Array(capacity),
|
|
2621
|
+
cutExcess: new Float32Array(capacity),
|
|
2622
|
+
cutX: new Float32Array(capacity),
|
|
2623
|
+
cutZ: new Float32Array(capacity),
|
|
2624
|
+
pairWeightForward: new Float32Array(pairCapacity),
|
|
2625
|
+
pairWeightBackward: new Float32Array(pairCapacity),
|
|
2626
|
+
canPress: new Uint8Array(capacity)
|
|
2627
|
+
});
|
|
2628
|
+
var isPressing = (store, i) => takesPartInContact(store, i) && (store.balance[i] === BALANCE.STANDING || store.balance[i] === BALANCE.STAGGERING);
|
|
2629
|
+
function readPressing(view, workspace) {
|
|
2630
|
+
for (let i = 0; i < view.n; i++) workspace.canPress[i] = isPressing(view.store, i) ? 1 : 0;
|
|
2631
|
+
}
|
|
2632
|
+
var frontWeight = (alignment, cone) => alignment > cone ? (alignment - cone) / (1 - cone) : 0;
|
|
2633
|
+
var hasAim = (store, i) => store.aimX[i] !== 0 || store.aimZ[i] !== 0;
|
|
2634
|
+
function noteCut(store, workspace, i, j, nx, nz) {
|
|
2635
|
+
const excess = (store.vx[i] - store.vx[j]) * nx + (store.vz[i] - store.vz[j]) * nz;
|
|
2636
|
+
if (excess <= workspace.cutExcess[i]) return;
|
|
2637
|
+
workspace.cutExcess[i] = excess;
|
|
2638
|
+
workspace.cutX[i] = nx;
|
|
2639
|
+
workspace.cutZ[i] = nz;
|
|
2640
|
+
}
|
|
2641
|
+
function weighPairs(view, workspace, params) {
|
|
2642
|
+
const { store } = view;
|
|
2643
|
+
for (let p = 0; p < view.workspace.pairCount; p++) {
|
|
2644
|
+
workspace.pairWeightForward[p] = 0;
|
|
2645
|
+
workspace.pairWeightBackward[p] = 0;
|
|
2646
|
+
const i = view.workspace.pairI[p];
|
|
2647
|
+
const j = view.workspace.pairJ[p];
|
|
2648
|
+
if (!workspace.canPress[i] || !workspace.canPress[j]) continue;
|
|
2649
|
+
const ex = store.x[j] - store.x[i];
|
|
2650
|
+
const ez = store.z[j] - store.z[i];
|
|
2651
|
+
const distance = Math.sqrt(ex * ex + ez * ez);
|
|
2652
|
+
if (distance < 1e-6 || distance > (store.build[i] + store.build[j]) * params.radius + TOUCH_SLACK) continue;
|
|
2653
|
+
const nx = ex / distance;
|
|
2654
|
+
const nz = ez / distance;
|
|
2655
|
+
const forward = frontWeight(store.aimX[i] * nx + store.aimZ[i] * nz, params.frontCone);
|
|
2656
|
+
const backward = frontWeight(-(store.aimX[j] * nx + store.aimZ[j] * nz), params.frontCone);
|
|
2657
|
+
if (forward > 0) {
|
|
2658
|
+
workspace.pressing[i] = 1;
|
|
2659
|
+
noteCut(store, workspace, i, j, nx, nz);
|
|
2660
|
+
}
|
|
2661
|
+
if (backward > 0) {
|
|
2662
|
+
workspace.pressing[j] = 1;
|
|
2663
|
+
noteCut(store, workspace, j, i, -nx, -nz);
|
|
2664
|
+
}
|
|
2665
|
+
if (forward > 0 && backward > 0) continue;
|
|
2666
|
+
workspace.pairWeightForward[p] = forward;
|
|
2667
|
+
workspace.pairWeightBackward[p] = backward;
|
|
2668
|
+
workspace.weightSum[i] += forward;
|
|
2669
|
+
workspace.weightSum[j] += backward;
|
|
2670
|
+
}
|
|
2671
|
+
}
|
|
2672
|
+
function weighWalls(view, workspace, params, doorAt) {
|
|
2673
|
+
const { store, grid } = view;
|
|
2674
|
+
for (let i = 0; i < view.n; i++) {
|
|
2675
|
+
if (!workspace.canPress[i] || !hasAim(store, i)) continue;
|
|
2676
|
+
const reach = store.build[i] * params.radius + WALL_PROBE;
|
|
2677
|
+
const cell = grid.cellOf(store.x[i] + store.aimX[i] * reach, store.z[i] + store.aimZ[i] * reach);
|
|
2678
|
+
if (cell >= 0 && (grid.occ[cell] === 0 || grid.occ[cell] === 3)) continue;
|
|
2679
|
+
workspace.wallKind[i] = 1;
|
|
2680
|
+
workspace.wallDoor[i] = cell >= 0 && grid.occ[cell] === 5 ? doorAt(cell) : -1;
|
|
2681
|
+
workspace.pressing[i] = 1;
|
|
2682
|
+
workspace.weightSum[i] += 1;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function weighPlayer(view, workspace, params, player) {
|
|
2686
|
+
const { store } = view;
|
|
2687
|
+
for (let i = 0; i < view.n; i++) {
|
|
2688
|
+
if (!workspace.canPress[i]) continue;
|
|
2689
|
+
const ex = player.x - store.x[i];
|
|
2690
|
+
const ez = player.z - store.z[i];
|
|
2691
|
+
const squared = ex * ex + ez * ez;
|
|
2692
|
+
if (squared > PLAYER_RADIUS_SQUARED || squared < 1e-6) continue;
|
|
2693
|
+
const distance = Math.sqrt(squared);
|
|
2694
|
+
const weight = hasAim(store, i) ? frontWeight((store.aimX[i] * ex + store.aimZ[i] * ez) / distance, params.frontCone) : 1;
|
|
2695
|
+
workspace.playerWeight[i] = weight;
|
|
2696
|
+
if (weight > 0) workspace.pressing[i] = 1;
|
|
2697
|
+
workspace.weightSum[i] += weight;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
function clearWorkspace(view, workspace) {
|
|
2701
|
+
const { store } = view;
|
|
2702
|
+
for (let i = 0; i < view.n; i++) {
|
|
2703
|
+
workspace.weightSum[i] = 0;
|
|
2704
|
+
workspace.pressing[i] = 0;
|
|
2705
|
+
workspace.wallKind[i] = 0;
|
|
2706
|
+
workspace.playerWeight[i] = 0;
|
|
2707
|
+
workspace.cutExcess[i] = 0;
|
|
2708
|
+
store.field[i] = 0;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
function ownDrive(store, context, workspace, i) {
|
|
2712
|
+
if (!hasAim(store, i) || !workspace.pressing[i]) return 0;
|
|
2713
|
+
return context.driveForceOfType[store.type[i]] * store.driveFactor[i];
|
|
2714
|
+
}
|
|
2715
|
+
function spreadField(view, workspace, params, context, out) {
|
|
2716
|
+
const { store } = view;
|
|
2717
|
+
for (let i = 0; i < view.n; i++) {
|
|
2718
|
+
if (!workspace.canPress[i]) continue;
|
|
2719
|
+
store.field[i] = ownDrive(store, context, workspace, i);
|
|
2720
|
+
if (store.field[i] > 0) out.drivers++;
|
|
2721
|
+
}
|
|
2722
|
+
for (let p = 0; p < view.workspace.pairCount; p++) {
|
|
2723
|
+
const i = view.workspace.pairI[p];
|
|
2724
|
+
const j = view.workspace.pairJ[p];
|
|
2725
|
+
const forward = workspace.pairWeightForward[p];
|
|
2726
|
+
const backward = workspace.pairWeightBackward[p];
|
|
2727
|
+
if (forward > 0) store.field[j] += params.transmission * store.fieldPrev[i] * forward * forward / workspace.weightSum[i];
|
|
2728
|
+
if (backward > 0) store.field[i] += params.transmission * store.fieldPrev[j] * backward * backward / workspace.weightSum[j];
|
|
2729
|
+
}
|
|
2730
|
+
for (let i = 0; i < view.n; i++) {
|
|
2731
|
+
if (!workspace.canPress[i]) continue;
|
|
2732
|
+
const passed = params.transmission * store.fieldPrev[i] / (workspace.weightSum[i] || 1);
|
|
2733
|
+
if (workspace.wallKind[i] && workspace.wallDoor[i] >= 0) {
|
|
2734
|
+
if (context.doorOut) {
|
|
2735
|
+
context.doorOut.cell[i] = workspace.wallDoor[i];
|
|
2736
|
+
context.doorOut.load[i] = passed * params.h;
|
|
2737
|
+
} else context.doorLoad[workspace.wallDoor[i]] += passed * params.h;
|
|
2738
|
+
}
|
|
2739
|
+
if (workspace.playerWeight[i] > 0) out.playerLoad += passed * workspace.playerWeight[i] * workspace.playerWeight[i];
|
|
2740
|
+
if (store.field[i] > params.cap) store.field[i] = params.cap;
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
function applyCuts(view, workspace) {
|
|
2744
|
+
const { store } = view;
|
|
2745
|
+
for (let i = 0; i < view.n; i++) {
|
|
2746
|
+
const excess = workspace.cutExcess[i];
|
|
2747
|
+
if (excess <= 0) continue;
|
|
2748
|
+
store.vx[i] -= excess * workspace.cutX[i];
|
|
2749
|
+
store.vz[i] -= excess * workspace.cutZ[i];
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
function pressureStep(view, workspace, params, context, out) {
|
|
2753
|
+
out.playerLoad = 0;
|
|
2754
|
+
out.drivers = 0;
|
|
2755
|
+
clearWorkspace(view, workspace);
|
|
2756
|
+
readPressing(view, workspace);
|
|
2757
|
+
weighPairs(view, workspace, params);
|
|
2758
|
+
weighWalls(view, workspace, params, context.doorAt);
|
|
2759
|
+
if (context.player) weighPlayer(view, workspace, params, context.player);
|
|
2760
|
+
spreadField(view, workspace, params, context, out);
|
|
2761
|
+
applyCuts(view, workspace);
|
|
2762
|
+
for (let i = 0; i < view.n; i++) view.store.fieldPrev[i] = view.store.field[i];
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
// src/physics/offload/kernel.ts
|
|
2766
|
+
var PAIRS_PER_AGENT2 = 12;
|
|
2767
|
+
var STORE_KEYS = ["x", "z", "y", "vx", "vz", "pushX", "pushZ", "mass", "build", "aimX", "aimZ", "driveFactor", "fieldPrev"];
|
|
2768
|
+
function createKernel(setup) {
|
|
2769
|
+
const { capacity } = setup;
|
|
2770
|
+
const side = Math.floor(setup.gridSize / setup.gridCell);
|
|
2771
|
+
const half = setup.gridSize / 2;
|
|
2772
|
+
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) };
|
|
2773
|
+
const driveForceOfType = Float32Array.from(setup.driveForceOfType);
|
|
2774
|
+
const contactResult = { contacts: 0, maxOverlap: 0, overlapSum: 0 };
|
|
2775
|
+
const pressureResult = { playerLoad: 0, drivers: 0 };
|
|
2776
|
+
const workspace = setup.pressure ? createPressureWorkspace(capacity, capacity * PAIRS_PER_AGENT2) : null;
|
|
2777
|
+
const cellOf = createCellOf(half, setup.gridCell, side);
|
|
2778
|
+
const doorLoadUnused = new Float32Array(0);
|
|
2779
|
+
const store = {};
|
|
2780
|
+
const grid = { size: setup.gridSize, half, occ: new Uint8Array(0), cellOf };
|
|
2781
|
+
const view = { n: 0, store, grid, workspace: createContactWorkspace(capacity, grid) };
|
|
2782
|
+
const bind = (frame) => {
|
|
2783
|
+
Object.assign(store, frame.u8, { identity: frame.identity, field: frame.f32.field });
|
|
2784
|
+
for (const key of STORE_KEYS) store[key] = frame.f32[key];
|
|
2785
|
+
grid.occ = frame.occ;
|
|
2786
|
+
return view;
|
|
2787
|
+
};
|
|
2788
|
+
const run = (frame) => {
|
|
2789
|
+
const { header } = frame;
|
|
2790
|
+
const n = header[HEADER.n];
|
|
2791
|
+
const contact = bind(frame);
|
|
2792
|
+
contact.n = n;
|
|
2793
|
+
for (let i = 0; i < n; i++) {
|
|
2794
|
+
before.x[i] = frame.f32.x[i];
|
|
2795
|
+
before.z[i] = frame.f32.z[i];
|
|
2796
|
+
before.pushX[i] = frame.f32.pushX[i];
|
|
2797
|
+
before.pushZ[i] = frame.f32.pushZ[i];
|
|
2798
|
+
before.vx[i] = frame.f32.vx[i];
|
|
2799
|
+
before.vz[i] = frame.f32.vz[i];
|
|
2800
|
+
}
|
|
2801
|
+
const h = header[HEADER.h];
|
|
2802
|
+
const params = { ...setup.contact, h };
|
|
2803
|
+
contactStep(contact, params, contactResult);
|
|
2804
|
+
header[HEADER.contacts] = contactResult.contacts;
|
|
2805
|
+
header[HEADER.maxOverlap] = contactResult.maxOverlap;
|
|
2806
|
+
header[HEADER.overlapSum] = contactResult.overlapSum;
|
|
2807
|
+
frame.doorCell.fill(-1, 0, n);
|
|
2808
|
+
frame.f32.doorLoad.fill(0, 0, n);
|
|
2809
|
+
if (workspace && setup.pressure) {
|
|
2810
|
+
const player = header[HEADER.playerOn] ? { x: header[HEADER.playerX], z: header[HEADER.playerZ] } : void 0;
|
|
2811
|
+
const doorOut = { cell: frame.doorCell, load: frame.f32.doorLoad };
|
|
2812
|
+
pressureStep(contact, workspace, { ...setup.pressure, h, radius: setup.contact.radius }, { driveForceOfType, doorAt: (cell) => cell, doorLoad: doorLoadUnused, player, doorOut }, pressureResult);
|
|
2813
|
+
let top = 0;
|
|
2814
|
+
for (let i = 0; i < n; i++) top = Math.max(top, frame.f32.field[i]);
|
|
2815
|
+
header[HEADER.fieldMax] = top;
|
|
2816
|
+
} else header[HEADER.fieldMax] = 0;
|
|
2817
|
+
header[HEADER.playerLoad] = pressureResult.playerLoad;
|
|
2818
|
+
header[HEADER.drivers] = pressureResult.drivers;
|
|
2819
|
+
const { f32 } = frame;
|
|
2820
|
+
for (let i = 0; i < n; i++) {
|
|
2821
|
+
f32.deltaX[i] = f32.x[i] - before.x[i];
|
|
2822
|
+
f32.deltaZ[i] = f32.z[i] - before.z[i];
|
|
2823
|
+
f32.deltaPushX[i] = f32.pushX[i] - before.pushX[i];
|
|
2824
|
+
f32.deltaPushZ[i] = f32.pushZ[i] - before.pushZ[i];
|
|
2825
|
+
f32.deltaVx[i] = f32.vx[i] - before.vx[i];
|
|
2826
|
+
f32.deltaVz[i] = f32.vz[i] - before.vz[i];
|
|
2827
|
+
f32.shove[i] = contact.workspace.shove[i];
|
|
2828
|
+
}
|
|
2829
|
+
};
|
|
2830
|
+
return { run };
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
// src/physics/offload/worker-host.ts
|
|
2834
|
+
var CONTROL = { IDLE: 0, SUBMITTED: 1, DONE: 2, STOP: 3 };
|
|
2835
|
+
function attachPhysicsWorker(port) {
|
|
2836
|
+
let kernel = null;
|
|
2837
|
+
let shape = null;
|
|
2838
|
+
port.onMessage((data) => {
|
|
2839
|
+
const message = data;
|
|
2840
|
+
if (message.type === "init") {
|
|
2841
|
+
kernel = createKernel(message.setup);
|
|
2842
|
+
shape = message.shape;
|
|
2843
|
+
if (message.frames && message.control) runShared(kernel, message.frames.map((buffer) => createFrame(buffer, message.shape)), new Int32Array(message.control));
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
if (!kernel || !shape) return;
|
|
2847
|
+
const frame = createFrame(message.buffer, shape);
|
|
2848
|
+
kernel.run(frame);
|
|
2849
|
+
port.postMessage({ type: "done", buffer: message.buffer }, [message.buffer]);
|
|
2850
|
+
});
|
|
2851
|
+
}
|
|
2852
|
+
function runShared(kernel, frames, control) {
|
|
2853
|
+
for (let turn = 0; ; turn++) {
|
|
2854
|
+
const slot = turn % frames.length;
|
|
2855
|
+
for (; ; ) {
|
|
2856
|
+
const state = Atomics.load(control, slot);
|
|
2857
|
+
if (state === CONTROL.STOP) return;
|
|
2858
|
+
if (state === CONTROL.SUBMITTED) break;
|
|
2859
|
+
Atomics.wait(control, slot, state);
|
|
2860
|
+
}
|
|
2861
|
+
kernel.run(frames[slot]);
|
|
2862
|
+
Atomics.store(control, slot, CONTROL.DONE);
|
|
2863
|
+
Atomics.notify(control, slot);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
|
|
2867
|
+
// src/physics/offload/runner.ts
|
|
2868
|
+
var WAIT_LIMIT_MS = 5e3;
|
|
2869
|
+
var canBlock = (() => {
|
|
2870
|
+
try {
|
|
2871
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 1, 0);
|
|
2872
|
+
return true;
|
|
2873
|
+
} catch {
|
|
2874
|
+
return false;
|
|
2875
|
+
}
|
|
2876
|
+
})();
|
|
2877
|
+
var isSharedAvailable = () => typeof SharedArrayBuffer !== "undefined" && globalThis.crossOriginIsolated !== false;
|
|
2878
|
+
function createInlineRunner(setup, shape, slots = 1) {
|
|
2879
|
+
const kernel = createKernel(setup);
|
|
2880
|
+
const frames = Array.from({ length: slots }, () => createFrame(new ArrayBuffer(frameBytes(shape)), shape));
|
|
2881
|
+
let submitted = 0;
|
|
2882
|
+
let settled = 0;
|
|
2883
|
+
const runner = {
|
|
2884
|
+
transport: "inline",
|
|
2885
|
+
get inFlight() {
|
|
2886
|
+
return submitted - settled;
|
|
2887
|
+
},
|
|
2888
|
+
frame: () => submitted - settled < slots ? frames[submitted % slots] : null,
|
|
2889
|
+
submit: () => kernel.run(frames[submitted++ % slots]),
|
|
2890
|
+
settle: () => submitted > settled ? frames[settled++ % slots] : null,
|
|
2891
|
+
dispose: () => void 0
|
|
2892
|
+
};
|
|
2893
|
+
return runner;
|
|
2894
|
+
}
|
|
2895
|
+
function createSharedRunner(setup, shape, factory, slots = 1) {
|
|
2896
|
+
const buffers = Array.from({ length: slots }, () => new SharedArrayBuffer(frameBytes(shape)));
|
|
2897
|
+
const control = new Int32Array(new SharedArrayBuffer(4 * slots));
|
|
2898
|
+
const frames = buffers.map((buffer) => createFrame(buffer, shape));
|
|
2899
|
+
const worker = factory();
|
|
2900
|
+
worker.postMessage({ type: "init", setup, shape, frames: buffers, control: control.buffer });
|
|
2901
|
+
let submitted = 0;
|
|
2902
|
+
let settled = 0;
|
|
2903
|
+
return {
|
|
2904
|
+
transport: "shared",
|
|
2905
|
+
get inFlight() {
|
|
2906
|
+
return submitted - settled;
|
|
2907
|
+
},
|
|
2908
|
+
frame: () => submitted - settled < slots ? frames[submitted % slots] : null,
|
|
2909
|
+
submit: () => {
|
|
2910
|
+
const slot = submitted++ % slots;
|
|
2911
|
+
Atomics.store(control, slot, CONTROL.SUBMITTED);
|
|
2912
|
+
Atomics.notify(control, slot);
|
|
2913
|
+
},
|
|
2914
|
+
settle: () => {
|
|
2915
|
+
if (submitted === settled) return null;
|
|
2916
|
+
const slot = settled++ % slots;
|
|
2917
|
+
const started = Date.now();
|
|
2918
|
+
while (Atomics.load(control, slot) !== CONTROL.DONE) {
|
|
2919
|
+
if (Date.now() - started > WAIT_LIMIT_MS) throw new Error("crowd physics worker did not answer");
|
|
2920
|
+
if (canBlock) Atomics.wait(control, slot, CONTROL.SUBMITTED, 50);
|
|
2921
|
+
}
|
|
2922
|
+
Atomics.store(control, slot, CONTROL.IDLE);
|
|
2923
|
+
return frames[slot];
|
|
2924
|
+
},
|
|
2925
|
+
dispose: () => {
|
|
2926
|
+
for (let slot = 0; slot < slots; slot++) {
|
|
2927
|
+
Atomics.store(control, slot, CONTROL.STOP);
|
|
2928
|
+
Atomics.notify(control, slot);
|
|
2929
|
+
}
|
|
2930
|
+
worker.terminate();
|
|
2931
|
+
}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
function createTransferRunner(setup, shape, factory, slots = 1) {
|
|
2935
|
+
const worker = factory();
|
|
2936
|
+
const free = Array.from({ length: slots }, () => new ArrayBuffer(frameBytes(shape)));
|
|
2937
|
+
const done = [];
|
|
2938
|
+
const views = /* @__PURE__ */ new WeakMap();
|
|
2939
|
+
const viewOf = (buffer) => {
|
|
2940
|
+
let view = views.get(buffer);
|
|
2941
|
+
if (!view) views.set(buffer, view = createFrame(buffer, shape));
|
|
2942
|
+
return view;
|
|
2943
|
+
};
|
|
2944
|
+
let inFlight = 0;
|
|
2945
|
+
worker.postMessage({ type: "init", setup, shape });
|
|
2946
|
+
worker.onMessage((data) => {
|
|
2947
|
+
const message = data;
|
|
2948
|
+
if (message.type === "done") done.push(message.buffer);
|
|
2949
|
+
});
|
|
2950
|
+
return {
|
|
2951
|
+
transport: "transfer",
|
|
2952
|
+
get inFlight() {
|
|
2953
|
+
return inFlight;
|
|
2954
|
+
},
|
|
2955
|
+
frame: () => free.length ? viewOf(free[0]) : null,
|
|
2956
|
+
submit: () => {
|
|
2957
|
+
const buffer = free.shift();
|
|
2958
|
+
if (!buffer) return;
|
|
2959
|
+
worker.postMessage({ type: "frame", buffer }, [buffer]);
|
|
2960
|
+
inFlight++;
|
|
2961
|
+
},
|
|
2962
|
+
settle: () => {
|
|
2963
|
+
const buffer = done.shift();
|
|
2964
|
+
if (!buffer) return null;
|
|
2965
|
+
inFlight--;
|
|
2966
|
+
free.push(buffer);
|
|
2967
|
+
return viewOf(buffer);
|
|
2968
|
+
},
|
|
2969
|
+
dispose: () => worker.terminate()
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// src/physics/player-pressure.ts
|
|
2974
|
+
var KNOCK_SECONDS = 1.5;
|
|
2975
|
+
var KNOCK_COOLDOWN = 2;
|
|
2976
|
+
var CRUSH_BLOW_EVERY = 0.25;
|
|
2977
|
+
function createPlayerPressure(ctx) {
|
|
2978
|
+
const options = ctx.options.playerPressure;
|
|
2979
|
+
const tau = ctx.options.balance?.pressureTau ?? 0.3;
|
|
2980
|
+
let knockCooldown = 0;
|
|
2981
|
+
let crushed = 0;
|
|
2982
|
+
let crushClock = 0;
|
|
2983
|
+
const crush = (excess) => {
|
|
2984
|
+
crushed += options.crushDamagePerSecond * excess * ctx.h;
|
|
2985
|
+
crushClock += ctx.h;
|
|
2986
|
+
if (crushClock < CRUSH_BLOW_EVERY) return;
|
|
2987
|
+
crushClock = 0;
|
|
2988
|
+
if (crushed <= 0) return;
|
|
2989
|
+
ctx.target()?.hurt(crushed);
|
|
2990
|
+
crushed = 0;
|
|
2991
|
+
};
|
|
2992
|
+
const step = (load) => {
|
|
2993
|
+
ctx.stats.playerPressure += (load - ctx.stats.playerPressure) * (1 - Math.exp(-ctx.h / tau));
|
|
2994
|
+
if (!options.enabled) return;
|
|
2995
|
+
const target = ctx.target();
|
|
2996
|
+
knockCooldown = Math.max(0, knockCooldown - ctx.h);
|
|
2997
|
+
if (!target || !target.state.alive || ctx.stats.playerPressure <= options.knockThreshold) {
|
|
2998
|
+
crushed = 0;
|
|
2999
|
+
return;
|
|
3000
|
+
}
|
|
3001
|
+
if (knockCooldown === 0) {
|
|
3002
|
+
target.knock(KNOCK_SECONDS);
|
|
3003
|
+
knockCooldown = KNOCK_COOLDOWN;
|
|
3004
|
+
}
|
|
3005
|
+
crush(ctx.stats.playerPressure / options.knockThreshold - 1);
|
|
3006
|
+
};
|
|
3007
|
+
return { step };
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/physics/thrown.ts
|
|
3011
|
+
var FREE_SLOT = -2;
|
|
3012
|
+
var CARRIED_BODY = -1;
|
|
3013
|
+
var MAX_SPIN = 8;
|
|
3014
|
+
var SWEEP_DISTANCE = 0.25;
|
|
3015
|
+
var LANDING_DAMAGE_SPEED = 6;
|
|
3016
|
+
var CENTRE_HEIGHT2 = 1;
|
|
3017
|
+
var MAX_POOL = 127;
|
|
3018
|
+
var createThrownPool = (size) => {
|
|
3019
|
+
const f32 = () => new Float32Array(size);
|
|
3020
|
+
const pool = {
|
|
3021
|
+
size,
|
|
3022
|
+
agent: new Int32Array(size).fill(FREE_SLOT),
|
|
3023
|
+
identity: new Uint32Array(size),
|
|
3024
|
+
corpse: Array.from({ length: size }, () => null),
|
|
3025
|
+
x: f32(),
|
|
3026
|
+
y: f32(),
|
|
3027
|
+
z: f32(),
|
|
3028
|
+
prevX: f32(),
|
|
3029
|
+
prevY: f32(),
|
|
3030
|
+
prevZ: f32(),
|
|
3031
|
+
vx: f32(),
|
|
3032
|
+
vy: f32(),
|
|
3033
|
+
vz: f32(),
|
|
3034
|
+
pitch: f32(),
|
|
3035
|
+
roll: f32(),
|
|
3036
|
+
spinPitch: f32(),
|
|
3037
|
+
spinRoll: f32(),
|
|
3038
|
+
active: () => pool.agent.reduce((count, agent) => count + (agent === FREE_SLOT ? 0 : 1), 0)
|
|
3039
|
+
};
|
|
3040
|
+
return pool;
|
|
3041
|
+
};
|
|
3042
|
+
var byFastest = (a, b, identityOf2) => b.speed - a.speed || identityOf2(a.index) - identityOf2(b.index);
|
|
3043
|
+
var byNearest = (a, b, identityOf2) => a.distance - b.distance || identityOf2(a.index) - identityOf2(b.index);
|
|
3044
|
+
function chooseCandidates(candidates, free, options, identityOf2) {
|
|
3045
|
+
if (candidates.length <= free) return candidates;
|
|
3046
|
+
if (options.priority === "fastest") return candidates.sort((a, b) => byFastest(a, b, identityOf2)).slice(0, free);
|
|
3047
|
+
if (options.priority === "nearest") return candidates.sort((a, b) => byNearest(a, b, identityOf2)).slice(0, free);
|
|
3048
|
+
const fast = Math.round(free * options.mixedRatio);
|
|
3049
|
+
const sorted = candidates.sort((a, b) => byFastest(a, b, identityOf2));
|
|
3050
|
+
const rest = sorted.slice(fast).sort((a, b) => byNearest(a, b, identityOf2));
|
|
3051
|
+
return [...sorted.slice(0, fast), ...rest.slice(0, free - fast)];
|
|
3052
|
+
}
|
|
3053
|
+
function createThrown(ctx) {
|
|
3054
|
+
const { store, grid } = ctx;
|
|
3055
|
+
const options = ctx.options.thrown;
|
|
3056
|
+
const size = Math.min(MAX_POOL, options.pool);
|
|
3057
|
+
const pool = createThrownPool(size);
|
|
3058
|
+
const identityOf2 = (i) => store.identity[i];
|
|
3059
|
+
const assign = (candidates) => {
|
|
3060
|
+
let free = 0;
|
|
3061
|
+
for (let slot2 = 0; slot2 < size; slot2++) if (pool.agent[slot2] === FREE_SLOT) free++;
|
|
3062
|
+
const chosen = chooseCandidates(candidates, free, options, identityOf2);
|
|
3063
|
+
let slot = 0;
|
|
3064
|
+
for (const candidate of chosen) {
|
|
3065
|
+
while (pool.agent[slot] !== FREE_SLOT) slot++;
|
|
3066
|
+
const i = candidate.index;
|
|
3067
|
+
pool.agent[slot] = i;
|
|
3068
|
+
pool.identity[slot] = store.identity[i];
|
|
3069
|
+
pool.corpse[slot] = null;
|
|
3070
|
+
pool.x[slot] = pool.prevX[slot] = store.x[i];
|
|
3071
|
+
pool.y[slot] = pool.prevY[slot] = store.y[i] + CENTRE_HEIGHT2 * store.height[i];
|
|
3072
|
+
pool.z[slot] = pool.prevZ[slot] = store.z[i];
|
|
3073
|
+
pool.vx[slot] = candidate.vx;
|
|
3074
|
+
pool.vy[slot] = candidate.vy;
|
|
3075
|
+
pool.vz[slot] = candidate.vz;
|
|
3076
|
+
pool.pitch[slot] = pool.roll[slot] = 0;
|
|
3077
|
+
pool.spinPitch[slot] = (unit(store.identity[i], ctx.step, SALT.SPIN) * 2 - 1) * MAX_SPIN;
|
|
3078
|
+
pool.spinRoll[slot] = (unit(store.identity[i], ctx.step, SALT.SPIN + 1) * 2 - 1) * MAX_SPIN;
|
|
3079
|
+
if (store.state[i] === STATE.PILE_STEP) ctx.releasePileStep(i);
|
|
3080
|
+
store.balance[i] = BALANCE.THROWN;
|
|
3081
|
+
store.thrown[i] = slot;
|
|
3082
|
+
store.vx[i] = store.vz[i] = store.pushX[i] = store.pushZ[i] = 0;
|
|
3083
|
+
store.aimX[i] = store.aimZ[i] = 0;
|
|
3084
|
+
}
|
|
3085
|
+
return chosen.length;
|
|
3086
|
+
};
|
|
3087
|
+
const remap = (removed, moved) => {
|
|
3088
|
+
var _a;
|
|
3089
|
+
for (let slot = 0; slot < size; slot++) {
|
|
3090
|
+
const agent = pool.agent[slot];
|
|
3091
|
+
if (agent === removed) {
|
|
3092
|
+
(_a = pool.corpse)[slot] ?? (_a[slot] = ctx.corpseOf(removed, unit(pool.identity[slot], ctx.step, SALT.CORPSE) * 10));
|
|
3093
|
+
pool.agent[slot] = CARRIED_BODY;
|
|
3094
|
+
} else if (agent === moved && moved !== removed) pool.agent[slot] = removed;
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
const sweepWall = (x0, z0, x1, z1, y) => {
|
|
3098
|
+
const cell = grid.cell;
|
|
3099
|
+
let gx = Math.floor((x0 + grid.half) / cell);
|
|
3100
|
+
let gz = Math.floor((z0 + grid.half) / cell);
|
|
3101
|
+
const dx = x1 - x0;
|
|
3102
|
+
const dz = z1 - z0;
|
|
3103
|
+
const stepX = dx > 0 ? 1 : -1;
|
|
3104
|
+
const stepZ = dz > 0 ? 1 : -1;
|
|
3105
|
+
const deltaX = dx === 0 ? Infinity : Math.abs(cell / dx);
|
|
3106
|
+
const deltaZ = dz === 0 ? Infinity : Math.abs(cell / dz);
|
|
3107
|
+
let nextX = dx === 0 ? Infinity : ((dx > 0 ? gx + 1 : gx) * cell - grid.half - x0) / dx;
|
|
3108
|
+
let nextZ = dz === 0 ? Infinity : ((dz > 0 ? gz + 1 : gz) * cell - grid.half - z0) / dz;
|
|
3109
|
+
let t = 0;
|
|
3110
|
+
while (Math.min(nextX, nextZ) <= 1) {
|
|
3111
|
+
if (nextX < nextZ) {
|
|
3112
|
+
t = nextX;
|
|
3113
|
+
nextX += deltaX;
|
|
3114
|
+
gx += stepX;
|
|
3115
|
+
} else {
|
|
3116
|
+
t = nextZ;
|
|
3117
|
+
nextZ += deltaZ;
|
|
3118
|
+
gz += stepZ;
|
|
3119
|
+
}
|
|
3120
|
+
const hitX = x0 + dx * t;
|
|
3121
|
+
const hitZ = z0 + dz * t;
|
|
3122
|
+
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) };
|
|
3123
|
+
}
|
|
3124
|
+
return void 0;
|
|
3125
|
+
};
|
|
3126
|
+
const fly = (slot) => {
|
|
3127
|
+
pool.prevX[slot] = pool.x[slot];
|
|
3128
|
+
pool.prevY[slot] = pool.y[slot];
|
|
3129
|
+
pool.prevZ[slot] = pool.z[slot];
|
|
3130
|
+
pool.vy[slot] -= ctx.gravity * ctx.h;
|
|
3131
|
+
pool.pitch[slot] += pool.spinPitch[slot] * ctx.h;
|
|
3132
|
+
pool.roll[slot] += pool.spinRoll[slot] * ctx.h;
|
|
3133
|
+
const nx = pool.x[slot] + pool.vx[slot] * ctx.h;
|
|
3134
|
+
const nz = pool.z[slot] + pool.vz[slot] * ctx.h;
|
|
3135
|
+
const ny = pool.y[slot] + pool.vy[slot] * ctx.h;
|
|
3136
|
+
const speed = Math.hypot(pool.vx[slot], pool.vy[slot], pool.vz[slot]);
|
|
3137
|
+
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;
|
|
3138
|
+
pool.x[slot] = wall ? wall.x : nx;
|
|
3139
|
+
pool.z[slot] = wall ? wall.z : nz;
|
|
3140
|
+
pool.y[slot] = ny;
|
|
3141
|
+
if (wall) pool.vx[slot] = pool.vz[slot] = 0;
|
|
3142
|
+
};
|
|
3143
|
+
const massOf = (slot) => {
|
|
3144
|
+
const agent = pool.agent[slot];
|
|
3145
|
+
if (agent >= 0) return store.mass[agent];
|
|
3146
|
+
const body = pool.corpse[slot];
|
|
3147
|
+
return ctx.options.contact.baseMass * body.height * body.build * body.build;
|
|
3148
|
+
};
|
|
3149
|
+
const knockNeighbours = (contact, balance, slot) => {
|
|
3150
|
+
const side = Math.floor(grid.size);
|
|
3151
|
+
const x = pool.x[slot];
|
|
3152
|
+
const z = pool.z[slot];
|
|
3153
|
+
const hx = Math.floor(x + grid.half);
|
|
3154
|
+
const hz = Math.floor(z + grid.half);
|
|
3155
|
+
const mass = massOf(slot);
|
|
3156
|
+
const speed = Math.hypot(pool.vx[slot], pool.vz[slot]);
|
|
3157
|
+
const reach = options.landingRadius * options.landingRadius;
|
|
3158
|
+
for (let oz = -1; oz <= 1; oz++)
|
|
3159
|
+
for (let ox = -1; ox <= 1; ox++) {
|
|
3160
|
+
if (hx + ox < 0 || hz + oz < 0 || hx + ox >= side || hz + oz >= side) continue;
|
|
3161
|
+
for (let j = contact.workspace.head[(hz + oz) * side + hx + ox]; j !== -1; j = contact.workspace.next[j]) {
|
|
3162
|
+
const ex = store.x[j] - x;
|
|
3163
|
+
const ez = store.z[j] - z;
|
|
3164
|
+
if (j === pool.agent[slot] || ex * ex + ez * ez > reach || !takesPartInContact(store, j)) continue;
|
|
3165
|
+
const away = Math.hypot(ex, ez);
|
|
3166
|
+
const along = away > 1e-6 ? [ex / away, ez / away] : [pool.vx[slot] / (speed || 1), pool.vz[slot] / (speed || 1)];
|
|
3167
|
+
const delta = mass / (mass + store.mass[j]) * speed;
|
|
3168
|
+
store.pushX[j] += delta * along[0];
|
|
3169
|
+
store.pushZ[j] += delta * along[1];
|
|
3170
|
+
balance.shove(j, delta, FALL_CAUSE.LANDING);
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
const land = (contact, balance, slot, ground) => {
|
|
3175
|
+
knockNeighbours(contact, balance, slot);
|
|
3176
|
+
const x = pool.x[slot];
|
|
3177
|
+
const z = pool.z[slot];
|
|
3178
|
+
ctx.emit("sound", { sound: "impact", volume: 1.2, position: [x, ground, z], distance: Math.hypot(x - ctx.centre.x, z - ctx.centre.z) });
|
|
3179
|
+
ctx.emit("effect", { effect: "dust", point: [x, ground, z], color: 9075302 });
|
|
3180
|
+
const agent = pool.agent[slot];
|
|
3181
|
+
if (agent >= 0) {
|
|
3182
|
+
store.balance[agent] = BALANCE.FALLEN;
|
|
3183
|
+
store.balanceTimer[agent] = 0;
|
|
3184
|
+
store.fallCause[agent] = FALL_CAUSE.BLAST;
|
|
3185
|
+
store.x[agent] = x;
|
|
3186
|
+
store.z[agent] = z;
|
|
3187
|
+
store.y[agent] = ground;
|
|
3188
|
+
store.vy[agent] = 0;
|
|
3189
|
+
store.thrown[agent] = -1;
|
|
3190
|
+
const amount = Math.max(0, -pool.vy[slot] - LANDING_DAMAGE_SPEED) * options.fallDamage;
|
|
3191
|
+
if (amount > 0) ctx.damage(agent, { region: 1, amount, distance: 0 }, () => unit(store.identity[agent], ctx.step, SALT.JAW));
|
|
3192
|
+
} else {
|
|
3193
|
+
ctx.emit("effect", { effect: "blood", point: [x, ground + 0.1, z], amount: 30, died: true });
|
|
3194
|
+
ctx.addCorpse({ ...pool.corpse[slot], x, z });
|
|
3195
|
+
}
|
|
3196
|
+
pool.agent[slot] = FREE_SLOT;
|
|
3197
|
+
pool.corpse[slot] = null;
|
|
3198
|
+
};
|
|
3199
|
+
const step = (contact, balance) => {
|
|
3200
|
+
let active = 0;
|
|
3201
|
+
for (let slot = 0; slot < size; slot++) {
|
|
3202
|
+
if (pool.agent[slot] === FREE_SLOT) continue;
|
|
3203
|
+
fly(slot);
|
|
3204
|
+
const cell = grid.cellOf(pool.x[slot], pool.z[slot]);
|
|
3205
|
+
const ground = cell < 0 ? 0 : ctx.groundAt(cell, pool.y[slot]);
|
|
3206
|
+
const agent = pool.agent[slot];
|
|
3207
|
+
if (pool.y[slot] <= ground) {
|
|
3208
|
+
land(contact, balance, slot, ground);
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
active++;
|
|
3212
|
+
if (agent < 0) continue;
|
|
3213
|
+
store.x[agent] = pool.x[slot];
|
|
3214
|
+
store.z[agent] = pool.z[slot];
|
|
3215
|
+
store.y[agent] = Math.max(ground, pool.y[slot] - CENTRE_HEIGHT2 * store.height[agent]);
|
|
3216
|
+
}
|
|
3217
|
+
ctx.stats.thrownActive = active;
|
|
3218
|
+
ctx.stats.thrownPeak = Math.max(ctx.stats.thrownPeak, active);
|
|
3219
|
+
};
|
|
3220
|
+
return { pool, assign, step, remap };
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
// src/physics/engine.ts
|
|
3224
|
+
var DEFAULT_DRIVE_FORCE = 250;
|
|
3225
|
+
var GRAVITY_FORCE2 = 9.8;
|
|
3226
|
+
var EXTRA_BODIES = 500;
|
|
3227
|
+
var PAIRS_PER_AGENT3 = 12;
|
|
3228
|
+
function createPhysics(base, options, stats = createPhysicsStats()) {
|
|
3229
|
+
const moundOn = options.mound !== null;
|
|
3230
|
+
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 };
|
|
3231
|
+
const { store, grid } = base;
|
|
3232
|
+
const balance = options.balance ? createBalance(ctx) : null;
|
|
3233
|
+
const thrown = options.thrown ? createThrown(ctx) : null;
|
|
3234
|
+
const contact = options.contact ? createContactView(store, grid) : null;
|
|
3235
|
+
const pressure = options.pressure && contact ? createPressureWorkspace(store.capacity, store.capacity * PAIRS_PER_AGENT3) : null;
|
|
3236
|
+
const mound = options.mound ? createMound(ctx, Math.max(EXTRA_BODIES, base.corpsesMax), base.moundHeight) : null;
|
|
3237
|
+
if (mound) ctx.weightOn = mound.weightOn;
|
|
3238
|
+
const density = options.density && balance ? createDensity(ctx, options.density) : null;
|
|
3239
|
+
const playerPressure = pressure ? createPlayerPressure(ctx) : null;
|
|
3240
|
+
const blasts = createBlasts(ctx, { balance, thrown, pileBlast: base.pileBlast });
|
|
3241
|
+
const contactResult = { contacts: 0, maxOverlap: 0, overlapSum: 0 };
|
|
3242
|
+
const pressureResult = { playerLoad: 0, drivers: 0 };
|
|
3243
|
+
const driveForceOfType = Float32Array.from(base.types, (type) => type.balance?.driveForce ?? DEFAULT_DRIVE_FORCE);
|
|
3244
|
+
const offload = options.worker !== "off" && contact ? createOffloadFor() : null;
|
|
3245
|
+
let doorLoad = new Float32Array(0);
|
|
3246
|
+
let doorSince = new Float32Array(0);
|
|
3247
|
+
const keepPositions = options.contact !== null || options.deterministic;
|
|
3248
|
+
function createRunner() {
|
|
3249
|
+
const gridCells = grid.occ.length;
|
|
3250
|
+
const shape = { capacity: store.capacity, cells: gridCells };
|
|
3251
|
+
const setup = { capacity: store.capacity, gridSize: grid.size, gridCell: grid.cell, contact: options.contact, pressure: options.pressure, driveForceOfType: Array.from(driveForceOfType) };
|
|
3252
|
+
const transport = options.workerTransport ?? (isSharedAvailable() ? "shared" : "transfer");
|
|
3253
|
+
const slots = options.workerLatency;
|
|
3254
|
+
if (transport === "inline") return createInlineRunner(setup, shape, slots);
|
|
3255
|
+
const factory = options.workerFactory ?? createDefaultWorkerFactory();
|
|
3256
|
+
if (!factory) {
|
|
3257
|
+
if (options.worker === "on") throw new Error('crowd physics: worker "on" but no Worker can be made here (give physics.workerFactory)');
|
|
3258
|
+
return null;
|
|
3259
|
+
}
|
|
3260
|
+
return transport === "shared" ? createSharedRunner(setup, shape, factory, slots) : createTransferRunner(setup, shape, factory, slots);
|
|
3261
|
+
}
|
|
3262
|
+
function createOffloadFor() {
|
|
3263
|
+
const runner = createRunner();
|
|
3264
|
+
if (!runner) return null;
|
|
3265
|
+
const host = {
|
|
3266
|
+
store,
|
|
3267
|
+
grid,
|
|
3268
|
+
stats,
|
|
3269
|
+
contact,
|
|
3270
|
+
addDoorLoad: (cell, load) => {
|
|
3271
|
+
const door = ctx.doors.at(cell);
|
|
3272
|
+
if (door < 0) return;
|
|
3273
|
+
growDoorArrays();
|
|
3274
|
+
doorLoad[door] += load;
|
|
3275
|
+
}
|
|
3276
|
+
};
|
|
3277
|
+
return createOffload(runner, host, options.workerLatency);
|
|
3278
|
+
}
|
|
3279
|
+
const beginStep = (h, centre) => {
|
|
3280
|
+
ctx.step++;
|
|
3281
|
+
ctx.h = h;
|
|
3282
|
+
ctx.centre = { x: centre.x, z: centre.z };
|
|
3283
|
+
if (keepPositions) {
|
|
3284
|
+
for (let i = 0; i < store.n; i++) {
|
|
3285
|
+
store.prevX[i] = store.x[i];
|
|
3286
|
+
store.prevZ[i] = store.z[i];
|
|
3287
|
+
store.prevY[i] = store.y[i];
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
blasts.resolve();
|
|
3291
|
+
};
|
|
3292
|
+
const hitDoors = () => {
|
|
3293
|
+
for (let door = 0; door < ctx.doors.list.length; door++) {
|
|
3294
|
+
if (doorLoad[door] <= 0) continue;
|
|
3295
|
+
doorSince[door] += ctx.h;
|
|
3296
|
+
if (doorSince[door] < ctx.attackEvery) continue;
|
|
3297
|
+
ctx.doors.hit(door, doorLoad[door] * options.doorPressureDamage);
|
|
3298
|
+
doorLoad[door] = 0;
|
|
3299
|
+
doorSince[door] = 0;
|
|
3300
|
+
}
|
|
3301
|
+
};
|
|
3302
|
+
const growDoorArrays = () => {
|
|
3303
|
+
if (doorLoad.length >= ctx.doors.list.length) return;
|
|
3304
|
+
const length = Math.max(64, ctx.doors.list.length * 2);
|
|
3305
|
+
doorLoad = Float32Array.from({ length }, (_, door) => doorLoad[door] ?? 0);
|
|
3306
|
+
doorSince = Float32Array.from({ length }, (_, door) => doorSince[door] ?? 0);
|
|
3307
|
+
};
|
|
3308
|
+
const runPressure = (view, workspace) => {
|
|
3309
|
+
growDoorArrays();
|
|
3310
|
+
const target = ctx.target();
|
|
3311
|
+
const player = options.playerPressure.enabled && target && target.state.alive ? { x: target.state.x, z: target.state.z } : void 0;
|
|
3312
|
+
pressureStep(view, workspace, { ...options.pressure, h: ctx.h, radius: options.contact.radius }, { driveForceOfType, doorAt: ctx.doors.at, doorLoad, player }, pressureResult);
|
|
3313
|
+
let top = 0;
|
|
3314
|
+
for (let i = 0; i < store.n; i++) top = Math.max(top, store.field[i]);
|
|
3315
|
+
stats.fieldMax = top;
|
|
3316
|
+
hitDoors();
|
|
3317
|
+
};
|
|
3318
|
+
const stepOffloaded = (running, view) => {
|
|
3319
|
+
growDoorArrays();
|
|
3320
|
+
const target = ctx.target();
|
|
3321
|
+
const player = options.playerPressure.enabled && target && target.state.alive ? { x: target.state.x, z: target.state.z } : void 0;
|
|
3322
|
+
const result = running.step(ctx.h, player);
|
|
3323
|
+
pressureResult.playerLoad = result.playerLoad;
|
|
3324
|
+
prepareHash(view, options.contact.radius);
|
|
3325
|
+
if (pressure) hitDoors();
|
|
3326
|
+
};
|
|
3327
|
+
const measureLean = () => {
|
|
3328
|
+
const maxPush = options.contact.maxPush;
|
|
3329
|
+
for (let i = 0; i < store.n; i++) {
|
|
3330
|
+
store.leanX[i] = store.pushX[i] / maxPush;
|
|
3331
|
+
store.leanZ[i] = store.pushZ[i] / maxPush;
|
|
3332
|
+
}
|
|
3333
|
+
};
|
|
3334
|
+
const endStep = () => {
|
|
3335
|
+
if (!contact) return;
|
|
3336
|
+
contact.n = store.n;
|
|
3337
|
+
if (offload) stepOffloaded(offload, contact);
|
|
3338
|
+
else {
|
|
3339
|
+
contactStep(contact, { ...options.contact, h: ctx.h }, contactResult);
|
|
3340
|
+
stats.contacts = contactResult.contacts;
|
|
3341
|
+
stats.maxOverlap = contactResult.maxOverlap;
|
|
3342
|
+
stats.meanOverlap = contactResult.contacts ? contactResult.overlapSum / contactResult.contacts : 0;
|
|
3343
|
+
if (pressure) runPressure(contact, pressure);
|
|
3344
|
+
}
|
|
3345
|
+
density?.step();
|
|
3346
|
+
measureLean();
|
|
3347
|
+
balance?.step(options.balance, contact.workspace.shove);
|
|
3348
|
+
mound?.step();
|
|
3349
|
+
thrown?.step(contact, balance);
|
|
3350
|
+
balance?.flush();
|
|
3351
|
+
playerPressure?.step(pressureResult.playerLoad);
|
|
3352
|
+
};
|
|
3353
|
+
const criticalOf = (i) => options.balance.criticalFactor * store.mass[i] * GRAVITY_FORCE2 * (base.typeOf(i).balance?.stability ?? 1) * store.stability[i];
|
|
3354
|
+
return {
|
|
3355
|
+
ctx,
|
|
3356
|
+
active: contact !== null,
|
|
3357
|
+
stats,
|
|
3358
|
+
blasts,
|
|
3359
|
+
thrown: thrown?.pool ?? createThrownPool(0),
|
|
3360
|
+
contact,
|
|
3361
|
+
balance,
|
|
3362
|
+
beginStep,
|
|
3363
|
+
endStep,
|
|
3364
|
+
remap: (removed, moved) => {
|
|
3365
|
+
thrown?.remap(removed, moved);
|
|
3366
|
+
offload?.remap(removed, moved);
|
|
3367
|
+
},
|
|
3368
|
+
dispose: () => offload?.dispose(),
|
|
3369
|
+
densitySlowdown: (i, band) => density?.slowdown(i, band) ?? 1,
|
|
3370
|
+
pressureRatio: (i) => options.balance ? store.pressure[i] / criticalOf(i) : 0
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/physics/pile-column.ts
|
|
3375
|
+
var COLLAPSE_MARK = -7;
|
|
3376
|
+
function collapsePileColumns(store, columns, cellCount, release) {
|
|
3377
|
+
const owners = /* @__PURE__ */ new Map();
|
|
3378
|
+
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);
|
|
3379
|
+
for (const column of columns.splice(0)) {
|
|
3380
|
+
if (column.dir === 0) continue;
|
|
3381
|
+
for (let k = 1, cell = column.cell - column.dir; cell >= 0 && cell < cellCount; k++, cell = column.cell - column.dir * k) {
|
|
3382
|
+
const j = owners.get(cell);
|
|
3383
|
+
if (j === void 0) break;
|
|
3384
|
+
owners.delete(cell);
|
|
3385
|
+
release(j);
|
|
3386
|
+
store.state[j] = STATE.AIRBORNE;
|
|
3387
|
+
store.vy[j] = 0;
|
|
3388
|
+
store.stateTimer[j] = COLLAPSE_MARK;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
// src/personality.ts
|
|
3394
|
+
var GAIT_WEIGHTS = [6, 1, 1, 1, 1, 2];
|
|
3395
|
+
var triangular = (identity, salt, range) => range[0] + (range[1] - range[0]) * ((unit(identity, 0, salt) + unit(identity, 1, salt)) / 2);
|
|
3396
|
+
var weighted = (identity, salt, weights) => {
|
|
3397
|
+
const total = weights.reduce((sum, weight) => sum + weight, 0);
|
|
3398
|
+
let pick = unit(identity, 0, salt) * total;
|
|
3399
|
+
for (let index = 0; index < weights.length; index++) {
|
|
3400
|
+
pick -= weights[index];
|
|
3401
|
+
if (pick < 0) return index;
|
|
3402
|
+
}
|
|
3403
|
+
return weights.length - 1;
|
|
3404
|
+
};
|
|
3405
|
+
function applyNeutralPersonality(store, i) {
|
|
3406
|
+
store.stability[i] = store.tripFactor[i] = store.driveFactor[i] = store.cadenceFactor[i] = store.aggression[i] = 1;
|
|
3407
|
+
store.reaction[i] = 0;
|
|
3408
|
+
store.idle[i] = 0;
|
|
3409
|
+
store.gaitStyle[i] = 0;
|
|
3410
|
+
}
|
|
3411
|
+
function applyPersonality(store, i, options) {
|
|
3412
|
+
const identity = store.identity[i];
|
|
3413
|
+
const speed = triangular(identity, SALT.SPEED, options.speed);
|
|
3414
|
+
store.chaseSpeed[i] *= speed;
|
|
3415
|
+
store.wanderSpeed[i] *= speed;
|
|
3416
|
+
store.reaction[i] = triangular(identity, SALT.REACTION, options.reaction);
|
|
3417
|
+
store.stability[i] = triangular(identity, SALT.STABILITY, options.stability);
|
|
3418
|
+
store.tripFactor[i] = triangular(identity, SALT.TRIP_FACTOR, options.trip);
|
|
3419
|
+
store.driveFactor[i] = triangular(identity, SALT.DRIVE, options.drive);
|
|
3420
|
+
store.cadenceFactor[i] = triangular(identity, SALT.CADENCE, options.cadence);
|
|
3421
|
+
store.aggression[i] = triangular(identity, SALT.AGGRESSION, options.aggression);
|
|
3422
|
+
store.idle[i] = weighted(identity, SALT.IDLE, options.idle);
|
|
3423
|
+
store.gaitStyle[i] = weighted(identity, SALT.GAIT, GAIT_WEIGHTS);
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3426
|
+
// src/physics/seat.ts
|
|
3427
|
+
function seatPhysics(store, i, born) {
|
|
3428
|
+
store.identity[i] = identityOf(born.seed, born.serial);
|
|
3429
|
+
store.mass[i] = born.baseMass * store.height[i] * store.build[i] ** 2;
|
|
3430
|
+
store.balance[i] = BALANCE.STANDING;
|
|
3431
|
+
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;
|
|
3432
|
+
store.fallCause[i] = 0;
|
|
3433
|
+
store.blastStep[i] = -1;
|
|
3434
|
+
store.thrown[i] = -1;
|
|
3435
|
+
store.wounds[i] = 0;
|
|
3436
|
+
store.stepDir[i] = 0;
|
|
3437
|
+
store.prevX[i] = store.x[i];
|
|
3438
|
+
store.prevZ[i] = store.z[i];
|
|
3439
|
+
store.prevY[i] = 0;
|
|
3440
|
+
if (born.personality) applyPersonality(store, i, born.personality);
|
|
3441
|
+
else applyNeutralPersonality(store, i);
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
// src/physics/options.ts
|
|
3445
|
+
var CONTACT_DEFAULTS = { radius: 0.25, iterations: 2, friction: 6, maxPush: 3, maxCorrection: 0.1, relaxation: 1, baseMass: 70, separationMax: 1.5 };
|
|
3446
|
+
var PRESSURE_DEFAULTS = { transmission: 0.97, cap: 6e3, frontCone: 0.5, staggerSpeed: 0.35 };
|
|
3447
|
+
var BALANCE_DEFAULTS = {
|
|
3448
|
+
criticalFactor: 2,
|
|
3449
|
+
staggerEnter: 0.7,
|
|
3450
|
+
staggerLeave: 0.4,
|
|
3451
|
+
staggerMin: 0.5,
|
|
3452
|
+
fallRate: 1.5,
|
|
3453
|
+
shoveStagger: 1.2,
|
|
3454
|
+
shoveFall: 2.5,
|
|
3455
|
+
fallenMin: 1.5,
|
|
3456
|
+
releaseFactor: 0.3,
|
|
3457
|
+
getUpSeconds: 1.2,
|
|
3458
|
+
pressureTau: 0.3,
|
|
3459
|
+
maxFallsPerStep: 8,
|
|
3460
|
+
maxFallen: 300,
|
|
3461
|
+
trampleDps: 6
|
|
3462
|
+
};
|
|
3463
|
+
var MOUND_DEFAULTS = { bodyThickness: 0.28, corpseThickness: 0.25, maxHeight: 1.6, corpseRadius: 35 };
|
|
3464
|
+
var THROWN_DEFAULTS = { pool: 32, radius: 30, landingRadius: 1.2, fallDamage: 8, priority: "mixed", mixedRatio: 0.5 };
|
|
3465
|
+
var DENSITY_DEFAULTS = { cell: 2, capacity: 4, pressurePerBody: 150 };
|
|
3466
|
+
var BLAST_DEFAULTS = { pointsPerKilojoule: 0.6, throwSpeed: 4, maxBlastsPerStep: 8, limbEffectsPerBlast: 24, bloodEffectsPerBlast: 32, shove: {} };
|
|
3467
|
+
var PERSONALITY_DEFAULTS = {
|
|
3468
|
+
speed: [0.9, 1.1],
|
|
3469
|
+
reaction: [0.05, 0.35],
|
|
3470
|
+
stability: [0.75, 1.25],
|
|
3471
|
+
trip: [0.5, 1.5],
|
|
3472
|
+
drive: [0.7, 1.3],
|
|
3473
|
+
cadence: [0.85, 1.15],
|
|
3474
|
+
aggression: [0.8, 1.2],
|
|
3475
|
+
idle: [3, 2, 2, 1]
|
|
3476
|
+
};
|
|
3477
|
+
var PLAYER_PRESSURE_DEFAULTS = { enabled: false, knockThreshold: 1500, crushDamagePerSecond: 6 };
|
|
3478
|
+
var DOOR_PRESSURE_DAMAGE = 4e-3;
|
|
3479
|
+
var DETERMINISTIC_FLOW_CELLS = 2e4;
|
|
3480
|
+
var CROWD_PHYSICS_DEFAULTS = {
|
|
3481
|
+
contact: CONTACT_DEFAULTS,
|
|
3482
|
+
pressure: PRESSURE_DEFAULTS,
|
|
3483
|
+
balance: BALANCE_DEFAULTS,
|
|
3484
|
+
mound: MOUND_DEFAULTS,
|
|
3485
|
+
thrown: THROWN_DEFAULTS,
|
|
3486
|
+
density: DENSITY_DEFAULTS,
|
|
3487
|
+
blast: BLAST_DEFAULTS,
|
|
3488
|
+
personality: PERSONALITY_DEFAULTS,
|
|
3489
|
+
door: { pressureDamage: DOOR_PRESSURE_DAMAGE },
|
|
3490
|
+
playerPressure: PLAYER_PRESSURE_DEFAULTS,
|
|
3491
|
+
deterministic: true
|
|
3492
|
+
};
|
|
3493
|
+
var part = (option, defaults) => {
|
|
3494
|
+
if (option === void 0 || option === false) return null;
|
|
3495
|
+
return option === true ? defaults : { ...defaults, ...option };
|
|
3496
|
+
};
|
|
3497
|
+
function resolvePhysics(physics = {}) {
|
|
3498
|
+
const contact = part(physics.contact, CONTACT_DEFAULTS);
|
|
3499
|
+
const pressure = part(physics.pressure, PRESSURE_DEFAULTS);
|
|
3500
|
+
const balance = part(physics.balance, BALANCE_DEFAULTS);
|
|
3501
|
+
const mound = part(physics.mound, MOUND_DEFAULTS);
|
|
3502
|
+
const thrown = part(physics.thrown, THROWN_DEFAULTS);
|
|
3503
|
+
const density = part(physics.density, DENSITY_DEFAULTS);
|
|
3504
|
+
const playerPressure = { ...PLAYER_PRESSURE_DEFAULTS, ...physics.playerPressure };
|
|
3505
|
+
const missing = [];
|
|
3506
|
+
if (!contact && pressure) missing.push("pressure needs contact");
|
|
3507
|
+
if (!contact && balance) missing.push("balance needs contact");
|
|
3508
|
+
if (!balance && mound) missing.push("mound needs balance");
|
|
3509
|
+
if (!balance && thrown) missing.push("thrown needs balance");
|
|
3510
|
+
if (!balance && density) missing.push("density needs balance");
|
|
3511
|
+
if (!pressure && playerPressure.enabled) missing.push("playerPressure needs pressure");
|
|
3512
|
+
if (!contact && physics.worker === "on") missing.push("worker needs contact");
|
|
3513
|
+
if (missing.length) throw new Error(`crowd physics: ${missing.join("; ")}`);
|
|
3514
|
+
return {
|
|
3515
|
+
contact,
|
|
3516
|
+
pressure,
|
|
3517
|
+
balance,
|
|
3518
|
+
mound,
|
|
3519
|
+
thrown,
|
|
3520
|
+
density,
|
|
3521
|
+
blast: { ...BLAST_DEFAULTS, ...physics.blast },
|
|
3522
|
+
personality: part(physics.personality, PERSONALITY_DEFAULTS),
|
|
3523
|
+
doorPressureDamage: physics.door?.pressureDamage ?? DOOR_PRESSURE_DAMAGE,
|
|
3524
|
+
playerPressure,
|
|
3525
|
+
deterministic: physics.deterministic ?? false,
|
|
3526
|
+
worker: physics.worker ?? "off",
|
|
3527
|
+
workerFactory: physics.workerFactory,
|
|
3528
|
+
workerTransport: physics.workerTransport,
|
|
3529
|
+
workerLatency: physics.workerLatency ?? 1
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
|
|
1599
3533
|
// src/rng.ts
|
|
1600
3534
|
function createRng(seed) {
|
|
1601
3535
|
let state = seed >>> 0;
|
|
@@ -1668,7 +3602,38 @@ var GameCrowd = (() => {
|
|
|
1668
3602
|
lod: u8(),
|
|
1669
3603
|
variant: u8(),
|
|
1670
3604
|
attackPose: u8(),
|
|
1671
|
-
arms: f32()
|
|
3605
|
+
arms: f32(),
|
|
3606
|
+
stepDir: new Int32Array(capacity),
|
|
3607
|
+
// physics (CrowdOptions.physics): they exist with it off, so the store keeps its shape; the game replays never read them
|
|
3608
|
+
pushX: f32(),
|
|
3609
|
+
pushZ: f32(),
|
|
3610
|
+
mass: f32(),
|
|
3611
|
+
pressure: f32(),
|
|
3612
|
+
field: f32(),
|
|
3613
|
+
fieldPrev: f32(),
|
|
3614
|
+
aimX: f32(),
|
|
3615
|
+
aimZ: f32(),
|
|
3616
|
+
directShove: f32(),
|
|
3617
|
+
blastStep: new Int32Array(capacity),
|
|
3618
|
+
balance: u8(),
|
|
3619
|
+
balanceTimer: f32(),
|
|
3620
|
+
fallCause: u8(),
|
|
3621
|
+
identity: new Uint32Array(capacity),
|
|
3622
|
+
stability: f32(),
|
|
3623
|
+
tripFactor: f32(),
|
|
3624
|
+
driveFactor: f32(),
|
|
3625
|
+
reaction: f32(),
|
|
3626
|
+
cadenceFactor: f32(),
|
|
3627
|
+
aggression: f32(),
|
|
3628
|
+
idle: u8(),
|
|
3629
|
+
gaitStyle: u8(),
|
|
3630
|
+
wounds: new Uint16Array(capacity),
|
|
3631
|
+
leanX: f32(),
|
|
3632
|
+
leanZ: f32(),
|
|
3633
|
+
thrown: new Int8Array(capacity),
|
|
3634
|
+
prevX: f32(),
|
|
3635
|
+
prevZ: f32(),
|
|
3636
|
+
prevY: f32()
|
|
1672
3637
|
};
|
|
1673
3638
|
};
|
|
1674
3639
|
var AGENT_FIELDS = [
|
|
@@ -1724,26 +3689,39 @@ var GameCrowd = (() => {
|
|
|
1724
3689
|
"lod",
|
|
1725
3690
|
"variant",
|
|
1726
3691
|
"attackPose",
|
|
1727
|
-
"arms"
|
|
3692
|
+
"arms",
|
|
3693
|
+
"stepDir",
|
|
3694
|
+
"pushX",
|
|
3695
|
+
"pushZ",
|
|
3696
|
+
"mass",
|
|
3697
|
+
"pressure",
|
|
3698
|
+
"field",
|
|
3699
|
+
"fieldPrev",
|
|
3700
|
+
"aimX",
|
|
3701
|
+
"aimZ",
|
|
3702
|
+
"directShove",
|
|
3703
|
+
"blastStep",
|
|
3704
|
+
"balance",
|
|
3705
|
+
"balanceTimer",
|
|
3706
|
+
"fallCause",
|
|
3707
|
+
"identity",
|
|
3708
|
+
"stability",
|
|
3709
|
+
"tripFactor",
|
|
3710
|
+
"driveFactor",
|
|
3711
|
+
"reaction",
|
|
3712
|
+
"cadenceFactor",
|
|
3713
|
+
"aggression",
|
|
3714
|
+
"idle",
|
|
3715
|
+
"gaitStyle",
|
|
3716
|
+
"wounds",
|
|
3717
|
+
"leanX",
|
|
3718
|
+
"leanZ",
|
|
3719
|
+
"thrown",
|
|
3720
|
+
"prevX",
|
|
3721
|
+
"prevZ",
|
|
3722
|
+
"prevY"
|
|
1728
3723
|
];
|
|
1729
3724
|
|
|
1730
|
-
// src/types.ts
|
|
1731
|
-
var STATE = {
|
|
1732
|
-
WANDER: 0,
|
|
1733
|
-
CHASE: 1,
|
|
1734
|
-
GRAB: 2,
|
|
1735
|
-
STUNNED: 3,
|
|
1736
|
-
CALLING: 4,
|
|
1737
|
-
AIRBORNE: 5,
|
|
1738
|
-
DORMANT: 6,
|
|
1739
|
-
SWELLING: 7,
|
|
1740
|
-
SWING: 8,
|
|
1741
|
-
EATING: 9,
|
|
1742
|
-
PILE_STEP: 10,
|
|
1743
|
-
SEEKING_SOUND: 12
|
|
1744
|
-
};
|
|
1745
|
-
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 };
|
|
1746
|
-
|
|
1747
3725
|
// src/crowd.ts
|
|
1748
3726
|
var CROWD_DEFAULTS = {
|
|
1749
3727
|
sources: defaults_default.sources,
|
|
@@ -1751,6 +3729,7 @@ var GameCrowd = (() => {
|
|
|
1751
3729
|
numbers: defaults_default.numbers
|
|
1752
3730
|
};
|
|
1753
3731
|
var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
|
|
3732
|
+
var NO_TRAITS = {};
|
|
1754
3733
|
var TAU_APPROX = 6.2832;
|
|
1755
3734
|
var TURN = 6.28;
|
|
1756
3735
|
function createCrowd(options) {
|
|
@@ -1772,10 +3751,25 @@ var GameCrowd = (() => {
|
|
|
1772
3751
|
const HALF = grid.half;
|
|
1773
3752
|
const GC = grid.cell;
|
|
1774
3753
|
const FW = grid.n;
|
|
3754
|
+
const physics = resolvePhysics(options.physics);
|
|
3755
|
+
const flowCells = options.flow?.cellsPerStep ?? (physics.deterministic ? DETERMINISTIC_FLOW_CELLS : void 0);
|
|
3756
|
+
const seed32 = (options.seed ?? 1) >>> 0;
|
|
3757
|
+
let serial = 0;
|
|
3758
|
+
const moundHeight = new Uint16Array(physics.mound ? FW * FW : 0);
|
|
3759
|
+
const moundOn = physics.mound !== null;
|
|
3760
|
+
const balanceOn = physics.balance !== null;
|
|
3761
|
+
const staggerSpeed = physics.pressure?.staggerSpeed ?? 1;
|
|
3762
|
+
const separationMax = physics.contact?.separationMax ?? Infinity;
|
|
3763
|
+
const RESERVED_SLOTS = 2;
|
|
3764
|
+
const WALK_DOWN = 0.6;
|
|
3765
|
+
const ATTACK_ENTER = 2.56;
|
|
3766
|
+
const ATTACK_LEAVE = 4.84;
|
|
1775
3767
|
const HC = N.hashCell;
|
|
1776
3768
|
const HN = Math.floor(grid.size / HC);
|
|
1777
|
-
const hHead = new Int32Array(HN * HN);
|
|
3769
|
+
const hHead = new Int32Array(HN * HN).fill(-1);
|
|
1778
3770
|
const hNext = new Int32Array(Z.capacity);
|
|
3771
|
+
const hashTouched = new Int32Array(Z.capacity);
|
|
3772
|
+
let hashTouchedN = 0;
|
|
1779
3773
|
const pile = new Float32Array(FW * FW);
|
|
1780
3774
|
let pileN = 0;
|
|
1781
3775
|
const flow = {
|
|
@@ -1808,7 +3802,7 @@ var GameCrowd = (() => {
|
|
|
1808
3802
|
}
|
|
1809
3803
|
const typeIndexById = new Map(types.map((type, index) => [type.id, index]));
|
|
1810
3804
|
const noises = [];
|
|
1811
|
-
const stats = { bands: [0, 0, 0, 0], slots: 0, kills: 0, heads: 0 };
|
|
3805
|
+
const stats = { bands: [0, 0, 0, 0], slots: 0, kills: 0, heads: 0, physics: createPhysicsStats() };
|
|
1812
3806
|
const corpses = createCorpseLedger({ max: N.corpsesMax, seconds: N.corpseSeconds });
|
|
1813
3807
|
const trail = [];
|
|
1814
3808
|
let trailLast = { x: 1e9, z: 1e9 };
|
|
@@ -1843,7 +3837,7 @@ var GameCrowd = (() => {
|
|
|
1843
3837
|
return list[0];
|
|
1844
3838
|
};
|
|
1845
3839
|
const T = (i) => types[Z.type[i]];
|
|
1846
|
-
const traits = (i) => T(i).traits ??
|
|
3840
|
+
const traits = (i) => T(i).traits ?? NO_TRAITS;
|
|
1847
3841
|
const special = (i) => {
|
|
1848
3842
|
const tr = traits(i);
|
|
1849
3843
|
Z.y[i] = 0;
|
|
@@ -1926,13 +3920,18 @@ var GameCrowd = (() => {
|
|
|
1926
3920
|
Z.wz[i] = z;
|
|
1927
3921
|
Z.color[i] = RO.color || pack.palette[Math.floor(R() * pack.palette.length)];
|
|
1928
3922
|
Z.skin[i] = R();
|
|
3923
|
+
seatPhysics(Z, i, { seed: seed32, serial: serial++, baseMass: physics.contact?.baseMass ?? 70, personality: physics.personality });
|
|
1929
3924
|
special(i);
|
|
1930
3925
|
emit("spawn", { agent: i, type: C.id });
|
|
1931
3926
|
return i;
|
|
1932
3927
|
};
|
|
1933
3928
|
const remove = (i) => {
|
|
1934
|
-
if (Z.state[i] === STATE.PILE_STEP)
|
|
3929
|
+
if (Z.state[i] === STATE.PILE_STEP) {
|
|
3930
|
+
if (engine.active) releasePileStep(i);
|
|
3931
|
+
else stepRelease(i);
|
|
3932
|
+
}
|
|
1935
3933
|
const j = --Z.n;
|
|
3934
|
+
engine.remap(i, j);
|
|
1936
3935
|
if (i === j) {
|
|
1937
3936
|
emit("remove", { agent: i, moved: -1 });
|
|
1938
3937
|
return;
|
|
@@ -1966,9 +3965,10 @@ var GameCrowd = (() => {
|
|
|
1966
3965
|
flow.c = 0;
|
|
1967
3966
|
flow.t = 0;
|
|
1968
3967
|
};
|
|
1969
|
-
const flowStep = (budget) => {
|
|
3968
|
+
const flowStep = (budget, cellsPerStep) => {
|
|
1970
3969
|
if (!flow.phase) return;
|
|
1971
3970
|
const t0 = clock();
|
|
3971
|
+
let cellsLeft = cellsPerStep ?? Infinity;
|
|
1972
3972
|
const D = flow.dist;
|
|
1973
3973
|
const Q = flow.q;
|
|
1974
3974
|
const F = flow.back;
|
|
@@ -1979,6 +3979,7 @@ var GameCrowd = (() => {
|
|
|
1979
3979
|
flow.phase = 2;
|
|
1980
3980
|
break;
|
|
1981
3981
|
}
|
|
3982
|
+
if (--cellsLeft < 0) return;
|
|
1982
3983
|
const c = Q[flow.qh++];
|
|
1983
3984
|
const cx = c % FW;
|
|
1984
3985
|
const cz = c / FW | 0;
|
|
@@ -1998,13 +3999,14 @@ var GameCrowd = (() => {
|
|
|
1998
3999
|
if (flow.qt < Q.length) Q[flow.qt++] = k;
|
|
1999
4000
|
}
|
|
2000
4001
|
}
|
|
2001
|
-
if ((++n & 2047) === 0 && clock() - t0 > budget) {
|
|
4002
|
+
if (cellsPerStep === void 0 && (++n & 2047) === 0 && clock() - t0 > budget) {
|
|
2002
4003
|
flow.t += clock() - t0;
|
|
2003
4004
|
return;
|
|
2004
4005
|
}
|
|
2005
4006
|
}
|
|
2006
4007
|
const ww = W[2] - W[0] + 1;
|
|
2007
4008
|
while (flow.phase === 2) {
|
|
4009
|
+
if (--cellsLeft < 0) return;
|
|
2008
4010
|
const j = flow.c++;
|
|
2009
4011
|
if (j >= ww * (W[3] - W[1] + 1)) {
|
|
2010
4012
|
const tmp = flow.dir;
|
|
@@ -2041,7 +4043,7 @@ var GameCrowd = (() => {
|
|
|
2041
4043
|
}
|
|
2042
4044
|
F[c * 2] = bx;
|
|
2043
4045
|
F[c * 2 + 1] = bz;
|
|
2044
|
-
if ((j & 8191) === 0 && clock() - t0 > budget) {
|
|
4046
|
+
if (cellsPerStep === void 0 && (j & 8191) === 0 && clock() - t0 > budget) {
|
|
2045
4047
|
flow.t += clock() - t0;
|
|
2046
4048
|
return;
|
|
2047
4049
|
}
|
|
@@ -2052,14 +4054,14 @@ var GameCrowd = (() => {
|
|
|
2052
4054
|
if (c < 0) return 0;
|
|
2053
4055
|
const k = occ[c];
|
|
2054
4056
|
if (k === 2 || k === 1 && occH[c] <= _y + 0.05) return occH[c];
|
|
2055
|
-
return pile[c];
|
|
4057
|
+
return moundOn ? pile[c] + moundHeight[c] / 1e3 : pile[c];
|
|
2056
4058
|
};
|
|
2057
4059
|
const blockedAt = (i, x, z) => {
|
|
2058
4060
|
const c = cellOf(x, z);
|
|
2059
4061
|
if (c < 0) return true;
|
|
2060
4062
|
const k = occ[c];
|
|
2061
4063
|
const y = Z.y[i];
|
|
2062
|
-
if (k === 0 || k === 3) return pile[c] > y + 0.5 && Z.state[i] !== STATE.AIRBORNE;
|
|
4064
|
+
if (k === 0 || k === 3) return (moundOn ? pile[c] + moundHeight[c] / 1e3 : pile[c]) > y + 0.5 && Z.state[i] !== STATE.AIRBORNE;
|
|
2063
4065
|
if (k === 2 && y + 0.5 >= occH[c]) return false;
|
|
2064
4066
|
if (Z.state[i] === STATE.AIRBORNE && occH[c] <= y) return false;
|
|
2065
4067
|
return true;
|
|
@@ -2079,6 +4081,7 @@ var GameCrowd = (() => {
|
|
|
2079
4081
|
pileN++;
|
|
2080
4082
|
Z.state[i] = STATE.PILE_STEP;
|
|
2081
4083
|
Z.stepCell[i] = c;
|
|
4084
|
+
Z.stepDir[i] = dir;
|
|
2082
4085
|
Z.vx[i] = Z.vz[i] = 0;
|
|
2083
4086
|
Z.y[i] = h - N.pileStep;
|
|
2084
4087
|
Z.stateTimer[i] = 6 + R() * 6;
|
|
@@ -2102,6 +4105,11 @@ var GameCrowd = (() => {
|
|
|
2102
4105
|
Z.stateTimer[i] = 1.2;
|
|
2103
4106
|
}
|
|
2104
4107
|
};
|
|
4108
|
+
const pileColumns = [];
|
|
4109
|
+
const releasePileStep = (i) => {
|
|
4110
|
+
if (Z.stepCell[i] >= 0) pileColumns.push({ cell: Z.stepCell[i], dir: Z.stepDir[i] });
|
|
4111
|
+
stepRelease(i);
|
|
4112
|
+
};
|
|
2105
4113
|
const voice = (i) => traits(i).voice ?? "groan";
|
|
2106
4114
|
const groan = (i, d) => {
|
|
2107
4115
|
const r2 = N.alertRadius * N.alertRadius;
|
|
@@ -2135,9 +4143,9 @@ var GameCrowd = (() => {
|
|
|
2135
4143
|
const attack = (i, d, tdt) => {
|
|
2136
4144
|
Z.attackTimer[i] -= tdt;
|
|
2137
4145
|
if (Z.attackTimer[i] > 0) return;
|
|
2138
|
-
Z.attackTimer[i] = N.attackEvery;
|
|
4146
|
+
Z.attackTimer[i] = N.attackEvery / Z.aggression[i];
|
|
2139
4147
|
const C = T(i);
|
|
2140
|
-
const tr = C.traits ??
|
|
4148
|
+
const tr = C.traits ?? NO_TRAITS;
|
|
2141
4149
|
if (tr.heavySwing && R() < tr.heavySwing.chance) {
|
|
2142
4150
|
Z.state[i] = STATE.SWING;
|
|
2143
4151
|
Z.stateTimer[i] = tr.heavySwing.windUp;
|
|
@@ -2146,7 +4154,7 @@ var GameCrowd = (() => {
|
|
|
2146
4154
|
const noJaw = Z.missing[i] & 512;
|
|
2147
4155
|
const noArms = (Z.missing[i] & 6) === 6;
|
|
2148
4156
|
const P = target;
|
|
2149
|
-
if (P.state.grabbedBy < 0 && !noArms && !Z.crawl[i] && R() < C.bite * (P.state.crouch ? N.crouchBite : 1)) {
|
|
4157
|
+
if (P.state.grabbedBy < 0 && !noArms && !Z.crawl[i] && R() < C.bite * (P.state.crouch ? N.crouchBite : 1) * Z.aggression[i]) {
|
|
2150
4158
|
P.grab(i, !!noJaw, !!tr.drownGrab && water(P.state.x, P.state.z));
|
|
2151
4159
|
Z.state[i] = STATE.GRAB;
|
|
2152
4160
|
} else {
|
|
@@ -2157,7 +4165,7 @@ var GameCrowd = (() => {
|
|
|
2157
4165
|
};
|
|
2158
4166
|
const effect = (payload) => emit("effect", payload);
|
|
2159
4167
|
const blood = (x, y, z, age, amount, died, agent) => effect({ effect: "blood", point: [x, y, z], age, amount, died, ...agent === void 0 ? {} : { agent } });
|
|
2160
|
-
const
|
|
4168
|
+
const damageWith = (i, hit, draw, deferDeath = false) => {
|
|
2161
4169
|
const C = T(i);
|
|
2162
4170
|
const A = pack.ages[Z.age[i]];
|
|
2163
4171
|
const RO = pack.outfits[Z.outfit[i]];
|
|
@@ -2184,7 +4192,7 @@ var GameCrowd = (() => {
|
|
|
2184
4192
|
} else {
|
|
2185
4193
|
Z.hp[i] -= dano * 3;
|
|
2186
4194
|
Z.headHp[i] -= dano;
|
|
2187
|
-
if (dano > N.jawDamage &&
|
|
4195
|
+
if (dano > N.jawDamage && draw() < N.jawChance) Z.missing[i] |= 512;
|
|
2188
4196
|
}
|
|
2189
4197
|
} else if (regiao === 1) {
|
|
2190
4198
|
let m = C.multipliers.torso;
|
|
@@ -2214,7 +4222,8 @@ var GameCrowd = (() => {
|
|
|
2214
4222
|
if (Z.hp[i] <= 0) morreu = true;
|
|
2215
4223
|
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);
|
|
2216
4224
|
if (morreu) {
|
|
2217
|
-
|
|
4225
|
+
if (deferDeath) deferKill(i);
|
|
4226
|
+
else kill(i, headshot, draw);
|
|
2218
4227
|
return { died: morreu, headshot };
|
|
2219
4228
|
}
|
|
2220
4229
|
const st = Z.state[i];
|
|
@@ -2231,14 +4240,15 @@ var GameCrowd = (() => {
|
|
|
2231
4240
|
}
|
|
2232
4241
|
return { died: morreu };
|
|
2233
4242
|
};
|
|
2234
|
-
|
|
4243
|
+
const damage = (i, hit) => damageWith(i, hit, R);
|
|
4244
|
+
function kill(i, headshot = false, draw = R) {
|
|
2235
4245
|
stats.kills++;
|
|
2236
4246
|
if (headshot) stats.heads++;
|
|
2237
4247
|
const tr = traits(i);
|
|
2238
4248
|
if (tr.burst) burstAt(i, Z.x[i], Z.z[i]);
|
|
2239
4249
|
const C = T(i);
|
|
2240
4250
|
emit("kill", { agent: i, type: C.id, headshot, x: Z.x[i], z: Z.z[i] });
|
|
2241
|
-
corpseAdd(i);
|
|
4251
|
+
if (Z.balance[i] !== BALANCE.THROWN) corpseAdd(i, draw);
|
|
2242
4252
|
if (target && target.state.grabbedBy === i) target.release();
|
|
2243
4253
|
remove(i);
|
|
2244
4254
|
}
|
|
@@ -2257,24 +4267,23 @@ var GameCrowd = (() => {
|
|
|
2257
4267
|
}
|
|
2258
4268
|
emitNoise(x, z, b.noise);
|
|
2259
4269
|
};
|
|
2260
|
-
const
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
};
|
|
4270
|
+
const corpseOf = (i, seed) => ({
|
|
4271
|
+
x: Z.x[i],
|
|
4272
|
+
z: Z.z[i],
|
|
4273
|
+
yaw: Z.renderYaw[i],
|
|
4274
|
+
height: Z.height[i],
|
|
4275
|
+
build: Z.build[i],
|
|
4276
|
+
named: Z.named[i],
|
|
4277
|
+
missing: Z.missing[i],
|
|
4278
|
+
color: Z.color[i],
|
|
4279
|
+
variant: Z.variant[i],
|
|
4280
|
+
skin: Z.skin[i],
|
|
4281
|
+
seed
|
|
4282
|
+
});
|
|
4283
|
+
const corpseAdd = (i, draw = R) => void corpses.add(corpseOf(i, draw() * 10));
|
|
2275
4284
|
const ability = (i, d, dx, dz, tdt, b, alive) => {
|
|
2276
4285
|
const C = T(i);
|
|
2277
|
-
const tr = C.traits ??
|
|
4286
|
+
const tr = C.traits ?? NO_TRAITS;
|
|
2278
4287
|
const st = Z.state[i];
|
|
2279
4288
|
const P = target;
|
|
2280
4289
|
if (st === STATE.DORMANT) {
|
|
@@ -2480,6 +4489,7 @@ var GameCrowd = (() => {
|
|
|
2480
4489
|
if (tr.submerged && water(Z.x[i], Z.z[i])) return tr.submerged.swim;
|
|
2481
4490
|
return Z.chaseSpeed[i];
|
|
2482
4491
|
};
|
|
4492
|
+
const physicsDraw = (i, salt) => engine.active ? () => unit(Z.identity[i], engine.ctx.step, salt) : R;
|
|
2483
4493
|
const hurtOther = (j, amount) => {
|
|
2484
4494
|
Z.hp[j] -= amount;
|
|
2485
4495
|
blood(Z.x[j], Z.height[j] * 1.2, Z.z[j], Z.age[j], 30, false, j);
|
|
@@ -2491,7 +4501,7 @@ var GameCrowd = (() => {
|
|
|
2491
4501
|
const killQFlush = () => {
|
|
2492
4502
|
if (!killQ.length) return;
|
|
2493
4503
|
killQ.sort((a, b) => b - a);
|
|
2494
|
-
for (const j of killQ) if (j < Z.n) kill(j, false);
|
|
4504
|
+
for (const j of killQ) if (j < Z.n) kill(j, false, physicsDraw(j, SALT.CORPSE));
|
|
2495
4505
|
killQ.length = 0;
|
|
2496
4506
|
};
|
|
2497
4507
|
const frame = (i, dt, d, alive) => {
|
|
@@ -2514,9 +4524,14 @@ var GameCrowd = (() => {
|
|
|
2514
4524
|
Z.hp[i] *= tr.waterWeak.keep;
|
|
2515
4525
|
effect({ effect: "cloud", point: [Z.x[i], 0, Z.z[i]], radius: 1.5, color: tr.waterWeak.cloud, amount: 3, agent: i });
|
|
2516
4526
|
}
|
|
4527
|
+
if (engine.active && (Z.balance[i] === BALANCE.FALLEN || Z.balance[i] === BALANCE.GETTING_UP)) return;
|
|
2517
4528
|
const c = cellOf(Z.x[i], Z.z[i]);
|
|
2518
4529
|
const gy = groundAt(c, Z.y[i]);
|
|
2519
4530
|
if (st === STATE.DORMANT || Z.floorLocked[i]) return;
|
|
4531
|
+
if (engine.active && st !== STATE.AIRBORNE && Z.vy[i] === 0 && Z.y[i] > gy + 0.02 && Z.y[i] - gy <= WALK_DOWN) {
|
|
4532
|
+
Z.y[i] = Math.max(gy, Z.y[i] - N.climbSpeed * dt);
|
|
4533
|
+
return;
|
|
4534
|
+
}
|
|
2520
4535
|
if (st === STATE.AIRBORNE || Z.y[i] > gy + 0.02 || Z.vy[i] > 0) {
|
|
2521
4536
|
Z.vy[i] -= N.gravity * dt;
|
|
2522
4537
|
Z.y[i] += Z.vy[i] * dt;
|
|
@@ -2524,6 +4539,9 @@ var GameCrowd = (() => {
|
|
|
2524
4539
|
const fall = -Z.vy[i];
|
|
2525
4540
|
Z.y[i] = gy;
|
|
2526
4541
|
Z.vy[i] = 0;
|
|
4542
|
+
const collapsed = st === STATE.AIRBORNE && Z.stateTimer[i] === COLLAPSE_MARK;
|
|
4543
|
+
if (collapsed) Z.stateTimer[i] = 0;
|
|
4544
|
+
if (engine.active && balanceOn && (st !== STATE.AIRBORNE || collapsed)) engine.balance.landed(i, fall * fall / (2 * N.gravity));
|
|
2527
4545
|
if (st === STATE.AIRBORNE) {
|
|
2528
4546
|
Z.state[i] = STATE.CHASE;
|
|
2529
4547
|
if (tr.pounce && alive && d < tr.pounce.landRange && target) {
|
|
@@ -2547,6 +4565,9 @@ var GameCrowd = (() => {
|
|
|
2547
4565
|
const pz = P ? P.state.z : context.flowTarget?.[1] ?? 0;
|
|
2548
4566
|
const alive = !!P && P.state.alive && !P.state.turned;
|
|
2549
4567
|
viewYaw = context.viewYaw ?? (P ? P.state.yaw - Math.PI : 0);
|
|
4568
|
+
const bandX = context.bandCentre?.[0] ?? px;
|
|
4569
|
+
const bandZ = context.bandCentre?.[1] ?? pz;
|
|
4570
|
+
engine.beginStep(dt, { x: bandX, z: bandZ });
|
|
2550
4571
|
const fx0 = context.flowTarget?.[0] ?? px;
|
|
2551
4572
|
const fz0 = context.flowTarget?.[1] ?? pz;
|
|
2552
4573
|
const pc = cellOf(fx0, fz0);
|
|
@@ -2558,14 +4579,18 @@ var GameCrowd = (() => {
|
|
|
2558
4579
|
flowStart(fx0, fz0);
|
|
2559
4580
|
flow.cell = pc;
|
|
2560
4581
|
}
|
|
2561
|
-
flowStep(N.flowBudgetMs);
|
|
2562
|
-
|
|
4582
|
+
flowStep(N.flowBudgetMs, flowCells);
|
|
4583
|
+
if (engine.active) {
|
|
4584
|
+
for (let k = 0; k < hashTouchedN; k++) hHead[hashTouched[k]] = -1;
|
|
4585
|
+
hashTouchedN = 0;
|
|
4586
|
+
} else hHead.fill(-1);
|
|
2563
4587
|
for (let i = 0; i < Z.n; i++) {
|
|
2564
|
-
if (Z.band[i] !== 0) continue;
|
|
4588
|
+
if (Z.band[i] !== 0 || engine.active && Z.balance[i] === BALANCE.THROWN) continue;
|
|
2565
4589
|
const hx = Math.floor(Z.x[i] + HALF);
|
|
2566
4590
|
const hz = Math.floor(Z.z[i] + HALF);
|
|
2567
4591
|
if (hx < 0 || hz < 0 || hx >= HN || hz >= HN) continue;
|
|
2568
4592
|
const h = hz * HN + hx;
|
|
4593
|
+
if (engine.active && hHead[h] === -1) hashTouched[hashTouchedN++] = h;
|
|
2569
4594
|
hNext[i] = hHead[h];
|
|
2570
4595
|
hHead[h] = i;
|
|
2571
4596
|
}
|
|
@@ -2577,13 +4602,16 @@ var GameCrowd = (() => {
|
|
|
2577
4602
|
const dx = Z.x[i] - px;
|
|
2578
4603
|
const dz = Z.z[i] - pz;
|
|
2579
4604
|
const d = Math.sqrt(dx * dx + dz * dz);
|
|
4605
|
+
const bandD = context.bandCentre ? Math.hypot(Z.x[i] - bandX, Z.z[i] - bandZ) : d;
|
|
2580
4606
|
let b = Z.band[i];
|
|
2581
|
-
while (b > 0 &&
|
|
2582
|
-
while (b < 3 &&
|
|
4607
|
+
while (b > 0 && bandD < BANDS[b - 1].max - HYST) b--;
|
|
4608
|
+
while (b < 3 && bandD > BANDS[b].max + HYST) b++;
|
|
2583
4609
|
Z.band[i] = b;
|
|
2584
4610
|
B[b]++;
|
|
4611
|
+
if (engine.active && Z.balance[i] === BALANCE.THROWN) continue;
|
|
4612
|
+
const grounded = engine.active && balanceOn && (Z.balance[i] === BALANCE.FALLEN || Z.balance[i] === BALANCE.GETTING_UP);
|
|
2585
4613
|
const C = T(i);
|
|
2586
|
-
const tr = C.traits ??
|
|
4614
|
+
const tr = C.traits ?? NO_TRAITS;
|
|
2587
4615
|
if (Z.state[i] === STATE.STUNNED) {
|
|
2588
4616
|
Z.stateTimer[i] -= dt;
|
|
2589
4617
|
if (Z.stateTimer[i] <= 0) Z.state[i] = STATE.CHASE;
|
|
@@ -2607,8 +4635,12 @@ var GameCrowd = (() => {
|
|
|
2607
4635
|
Z.lost[i] = 0;
|
|
2608
4636
|
}
|
|
2609
4637
|
}
|
|
2610
|
-
Z.next[i] -= dt;
|
|
2611
|
-
|
|
4638
|
+
if (!grounded) Z.next[i] -= dt;
|
|
4639
|
+
else {
|
|
4640
|
+
Z.vx[i] = Z.vz[i] = Z.dvx[i] = Z.dvz[i] = Z.aimX[i] = Z.aimZ[i] = 0;
|
|
4641
|
+
Z.steering[i] = 0;
|
|
4642
|
+
}
|
|
4643
|
+
if (Z.next[i] <= 0 && !grounded) {
|
|
2612
4644
|
const tdt = BANDS[b].dt;
|
|
2613
4645
|
Z.next[i] += tdt;
|
|
2614
4646
|
if (alive && b <= 1 && Z.state[i] <= 1) {
|
|
@@ -2655,6 +4687,7 @@ var GameCrowd = (() => {
|
|
|
2655
4687
|
Z.stateTimer[i] = tr.caller.windUp;
|
|
2656
4688
|
}
|
|
2657
4689
|
Z.steering[i] = 0;
|
|
4690
|
+
Z.aimX[i] = Z.aimZ[i] = 0;
|
|
2658
4691
|
if (!ability(i, d, dx, dz, tdt, b, alive)) {
|
|
2659
4692
|
let tx = 0;
|
|
2660
4693
|
let tz = 0;
|
|
@@ -2691,7 +4724,7 @@ var GameCrowd = (() => {
|
|
|
2691
4724
|
if (!alive) sp = Z.wanderSpeed[i];
|
|
2692
4725
|
if (d < N.attackRange && alive && Math.abs(P.state.y - Z.y[i]) < N.attackHeight) {
|
|
2693
4726
|
sp = 0;
|
|
2694
|
-
if (slots < N.attackSlots) {
|
|
4727
|
+
if (slots < N.attackSlots - (Z.aggression[i] < 1 ? RESERVED_SLOTS : 0)) {
|
|
2695
4728
|
slots++;
|
|
2696
4729
|
attack(i, d, tdt);
|
|
2697
4730
|
}
|
|
@@ -2713,6 +4746,7 @@ var GameCrowd = (() => {
|
|
|
2713
4746
|
}
|
|
2714
4747
|
if (Z.crawl[i]) sp = Math.min(sp, N.crawlSpeed);
|
|
2715
4748
|
else if (Z.limp[i]) sp *= N.limpSpeed;
|
|
4749
|
+
if (Z.balance[i] === BALANCE.STAGGERING) sp *= staggerSpeed;
|
|
2716
4750
|
let sx = 0;
|
|
2717
4751
|
let sz = 0;
|
|
2718
4752
|
if (b === 0) {
|
|
@@ -2724,7 +4758,7 @@ var GameCrowd = (() => {
|
|
|
2724
4758
|
const qz = hz + oz;
|
|
2725
4759
|
if (qx < 0 || qz < 0 || qx >= HN || qz >= HN) continue;
|
|
2726
4760
|
for (let j = hHead[qz * HN + qx]; j !== -1; j = hNext[j]) {
|
|
2727
|
-
if (j === i || Z.state[j] === STATE.PILE_STEP || Z.state[j] === STATE.DORMANT) continue;
|
|
4761
|
+
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;
|
|
2728
4762
|
const ex = Z.x[i] - Z.x[j];
|
|
2729
4763
|
const ez = Z.z[i] - Z.z[j];
|
|
2730
4764
|
const e2 = ex * ex + ez * ez;
|
|
@@ -2739,8 +4773,20 @@ var GameCrowd = (() => {
|
|
|
2739
4773
|
}
|
|
2740
4774
|
}
|
|
2741
4775
|
if (Z.state[i] <= 2) {
|
|
2742
|
-
|
|
2743
|
-
|
|
4776
|
+
if (separationMax !== Infinity) {
|
|
4777
|
+
const push = Math.hypot(sx, sz) * N.separationPush;
|
|
4778
|
+
if (push > separationMax) {
|
|
4779
|
+
sx *= separationMax / push;
|
|
4780
|
+
sz *= separationMax / push;
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
if (engine.active && sp > 0.05) {
|
|
4784
|
+
Z.aimX[i] = tx;
|
|
4785
|
+
Z.aimZ[i] = tz;
|
|
4786
|
+
}
|
|
4787
|
+
const slow = engine.densitySlowdown(i, b);
|
|
4788
|
+
Z.dvx[i] = (tx * sp + sx * N.separationPush) * slow;
|
|
4789
|
+
Z.dvz[i] = (tz * sp + sz * N.separationPush) * slow;
|
|
2744
4790
|
Z.steering[i] = 1;
|
|
2745
4791
|
if (sp > 0.05) Z.yaw[i] = Math.atan2(tx, tz);
|
|
2746
4792
|
else if (d < 3 && alive) Z.yaw[i] = Math.atan2(-dx, -dz);
|
|
@@ -2752,17 +4798,17 @@ var GameCrowd = (() => {
|
|
|
2752
4798
|
Z.vx[i] += (Z.dvx[i] - Z.vx[i]) * k;
|
|
2753
4799
|
Z.vz[i] += (Z.dvz[i] - Z.vz[i]) * k;
|
|
2754
4800
|
}
|
|
2755
|
-
const nx = Z.x[i] + Z.vx[i] * dt;
|
|
2756
|
-
const nz = Z.z[i] + Z.vz[i] * dt;
|
|
4801
|
+
const nx = Z.x[i] + (Z.vx[i] + Z.pushX[i]) * dt;
|
|
4802
|
+
const nz = Z.z[i] + (Z.vz[i] + Z.pushZ[i]) * dt;
|
|
2757
4803
|
let hitCell = -1;
|
|
2758
4804
|
if (!blockedAt(i, nx, Z.z[i])) Z.x[i] = nx;
|
|
2759
4805
|
else {
|
|
2760
|
-
if (Z.state[i] !== STATE.AIRBORNE) Z.vx[i] = 0;
|
|
4806
|
+
if (Z.state[i] !== STATE.AIRBORNE) Z.vx[i] = Z.pushX[i] = 0;
|
|
2761
4807
|
hitCell = cellOf(nx, Z.z[i]);
|
|
2762
4808
|
}
|
|
2763
4809
|
if (!blockedAt(i, Z.x[i], nz)) Z.z[i] = nz;
|
|
2764
4810
|
else {
|
|
2765
|
-
if (Z.state[i] !== STATE.AIRBORNE) Z.vz[i] = 0;
|
|
4811
|
+
if (Z.state[i] !== STATE.AIRBORNE) Z.vz[i] = Z.pushZ[i] = 0;
|
|
2766
4812
|
hitCell = cellOf(Z.x[i], nz);
|
|
2767
4813
|
}
|
|
2768
4814
|
if (hitCell >= 0 && Z.state[i] === STATE.CHASE && b === 0) {
|
|
@@ -2774,7 +4820,7 @@ var GameCrowd = (() => {
|
|
|
2774
4820
|
Z.vx[i] = Math.sin(Z.yaw[i]) * s2;
|
|
2775
4821
|
Z.vz[i] = Math.cos(Z.yaw[i]) * s2;
|
|
2776
4822
|
} else if (occ[hitCell] === 2 || occ[hitCell] === 0 && pile[hitCell] > 0) {
|
|
2777
|
-
Z.stuck[i] += dt;
|
|
4823
|
+
Z.stuck[i] += engine.active && balanceOn ? dt * (1 + engine.pressureRatio(i)) : dt;
|
|
2778
4824
|
if (Z.stuck[i] > 0.35 + 0.5 * Z.skin[i]) {
|
|
2779
4825
|
Z.stuck[i] = 0;
|
|
2780
4826
|
tryStep(i, hitCell);
|
|
@@ -2797,12 +4843,18 @@ var GameCrowd = (() => {
|
|
|
2797
4843
|
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;
|
|
2798
4844
|
Z.arms[i] += (goal - Z.arms[i]) * Math.min(1, dt * (goal > Z.arms[i] ? 3 : 5));
|
|
2799
4845
|
}
|
|
2800
|
-
Z.phase[i] += dt * (Z.renderSpeed[i] < 0.05 ? 0.15 : N.cadenceBase + N.cadencePerSpeed * Z.renderSpeed[i]);
|
|
4846
|
+
Z.phase[i] += dt * (Z.renderSpeed[i] < 0.05 ? 0.15 : N.cadenceBase + N.cadencePerSpeed * Z.renderSpeed[i]) * Z.cadenceFactor[i];
|
|
4847
|
+
if (physics.deterministic) {
|
|
4848
|
+
const attacking = Z.state[i] === STATE.CHASE && alive;
|
|
4849
|
+
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;
|
|
4850
|
+
}
|
|
2801
4851
|
if (spd > 0.35 && Z.state[i] <= 1 && !Z.attackPose[i]) Z.yaw[i] = Math.atan2(Z.vx[i], Z.vz[i]);
|
|
2802
4852
|
let dy = Z.yaw[i] - Z.renderYaw[i];
|
|
2803
4853
|
dy -= Math.round(dy / TAU_APPROX) * TAU_APPROX;
|
|
2804
4854
|
Z.renderYaw[i] += dy * Math.min(1, dt * (Z.state[i] === STATE.AIRBORNE ? 20 : 9));
|
|
2805
4855
|
}
|
|
4856
|
+
engine.endStep();
|
|
4857
|
+
if (pileColumns.length) collapsePileColumns(Z, pileColumns, FW * FW, stepRelease);
|
|
2806
4858
|
killQFlush();
|
|
2807
4859
|
noises.length = 0;
|
|
2808
4860
|
stats.slots = slots;
|
|
@@ -2866,6 +4918,31 @@ var GameCrowd = (() => {
|
|
|
2866
4918
|
if (m) emit("message", { id: "migration", seconds: 4 });
|
|
2867
4919
|
}
|
|
2868
4920
|
};
|
|
4921
|
+
const engine = createPhysics(
|
|
4922
|
+
{
|
|
4923
|
+
store: Z,
|
|
4924
|
+
grid,
|
|
4925
|
+
doors,
|
|
4926
|
+
gravity: N.gravity,
|
|
4927
|
+
attackEvery: N.attackEvery,
|
|
4928
|
+
types,
|
|
4929
|
+
moundHeight,
|
|
4930
|
+
corpsesMax: N.corpsesMax,
|
|
4931
|
+
typeOf: T,
|
|
4932
|
+
target: () => target,
|
|
4933
|
+
emit,
|
|
4934
|
+
damage: (i, hit, draw) => damageWith(i, hit, draw, true),
|
|
4935
|
+
deferKill,
|
|
4936
|
+
releasePileStep,
|
|
4937
|
+
corpseOf,
|
|
4938
|
+
corpseList: () => corpses.entries,
|
|
4939
|
+
addCorpse: (corpse) => void corpses.add(corpse),
|
|
4940
|
+
groundAt,
|
|
4941
|
+
pileBlast
|
|
4942
|
+
},
|
|
4943
|
+
physics,
|
|
4944
|
+
stats.physics
|
|
4945
|
+
);
|
|
2869
4946
|
const crowd = {
|
|
2870
4947
|
store: Z,
|
|
2871
4948
|
pack,
|
|
@@ -2875,6 +4952,9 @@ var GameCrowd = (() => {
|
|
|
2875
4952
|
numbers: N,
|
|
2876
4953
|
stats,
|
|
2877
4954
|
pile,
|
|
4955
|
+
mound: moundHeight,
|
|
4956
|
+
thrown: engine.thrown,
|
|
4957
|
+
blast: (blast) => engine.blasts.enqueue(blast),
|
|
2878
4958
|
pileCount: () => pileN,
|
|
2879
4959
|
trail,
|
|
2880
4960
|
variantsOf,
|
|
@@ -2891,6 +4971,7 @@ var GameCrowd = (() => {
|
|
|
2891
4971
|
spawn: (x, z, type = -1) => spawn(x, z, type),
|
|
2892
4972
|
remove,
|
|
2893
4973
|
update,
|
|
4974
|
+
dispose: () => engine.dispose(),
|
|
2894
4975
|
emitNoise,
|
|
2895
4976
|
damage,
|
|
2896
4977
|
kill: (i, headshot = false) => kill(i, headshot),
|