@adatechnology/game-crowd 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2308 @@
1
+ // src/crowd.ts
2
+ import { createCorpseLedger } from "@adatechnology/game-characters";
3
+
4
+ // src/data/defaults.json
5
+ var defaults_default = {
6
+ sources: {
7
+ store: "jogo.js G 66714-66734 (Z, ZMAX 6000, FIELDS)",
8
+ bands: "jogo.js G 62019-62020 (BANDS, HYST)",
9
+ flow: "jogo.js G 66735-66737, 66773-66825 (FLOW_R 192, flowStart, flowStep; budget 1 ms a frame, the first field in one go)",
10
+ update: "jogo.js G 66835-66977 hordeUpdate",
11
+ alerts: "jogo.js G 66978-66993 groan, screamAt",
12
+ attack: "jogo.js G 66996-67006 zAttack",
13
+ damage: "jogo.js G 67010-67045 zDamage",
14
+ kill: "jogo.js G 67046-67059 zKill, inchadoBurst",
15
+ corpses: "jogo.js G 62021 (CORPOS_MAX 400, CORPO_VIDA 900), 67062-67075 corpseAdd, corpsesUpdate",
16
+ population: "jogo.js G 67139-67177 spawnAway, populationUpdate (popTarget 1200, migration 120 every 90 s, the first after 60 s)",
17
+ pile: "jogo.js G 67184-67227 (PILE_STEP 0.48, PILE_TOP 3.7, PILE_MAX 160, groundAt, zBlockedAt, tryStep, stepRelease, pileBlast)",
18
+ trail: "jogo.js G 67238-67246 trailUpdate",
19
+ abilities: "jogo.js G 67266-67356 zAbility, zChaseSpeed, zHurtOther, killQ",
20
+ frame: "jogo.js G 67377-67402 zFrame",
21
+ cadence: "jogo.js G 65507 walkCad"
22
+ },
23
+ bands: [
24
+ { max: 35, dt: 0.05 },
25
+ { max: 120, dt: 0.2 },
26
+ { max: 400, dt: 1 },
27
+ { max: null, dt: 5 }
28
+ ],
29
+ numbers: {
30
+ capacity: 6e3,
31
+ hysteresis: 10,
32
+ flowRadius: 192,
33
+ flowBudgetMs: 1,
34
+ hashCell: 1,
35
+ nightSight: 0.4,
36
+ crouchSight: 0.6,
37
+ sightCone: 0.5,
38
+ sightNear: 2.5,
39
+ loseAfter: 15,
40
+ hearingBase: 40,
41
+ heardLost: 5,
42
+ alertRadius: 8,
43
+ alertDelay: 0.2,
44
+ alertDelayRandom: 0.4,
45
+ voiceDistance: 40,
46
+ voiceEvery: 0.25,
47
+ directRange: 4,
48
+ pyramidRange: 14,
49
+ screamSpeed: 1.2,
50
+ attackRange: 1.15,
51
+ attackHeight: 1.3,
52
+ attackSlots: 6,
53
+ attackEvery: 1.5,
54
+ crouchBite: 1.3,
55
+ crawlDamage: 0.6,
56
+ wanderSpan: 24,
57
+ crawlSpeed: 0.4,
58
+ limpSpeed: 0.5,
59
+ separation: 0.31,
60
+ separationPush: 2.2,
61
+ doorDamage: 10,
62
+ doorMessageDistance: 25,
63
+ skullMinDamage: 20,
64
+ skullTakeDamage: 60,
65
+ skullTakeWithin: { "12ga": 5 },
66
+ jawDamage: 25,
67
+ jawChance: 0.3,
68
+ hitStunDamage: 35,
69
+ hitStun: 0.45,
70
+ dormantWake: 3,
71
+ dormantWakeBehind: 10,
72
+ pileStep: 0.48,
73
+ pileTop: 3.7,
74
+ pileMax: 160,
75
+ pileReleaseDistance: 30,
76
+ gravity: 9.8,
77
+ fallStun: 3.2,
78
+ climbSpeed: 1.9,
79
+ cadenceBase: 0.5,
80
+ cadencePerSpeed: 0.3,
81
+ corpsesMax: 400,
82
+ corpseSeconds: 900,
83
+ trailSeconds: 120,
84
+ trailStep: 1.5,
85
+ trailMax: 200,
86
+ population: 1200,
87
+ refillMin: 70,
88
+ refillMax: 160,
89
+ migrationFirst: 60
90
+ }
91
+ };
92
+
93
+ // src/doors.ts
94
+ var DOOR_DEFAULTS = { doorHp: 300, plankHp: 150, height: 2.2, thickness: 0.1, soundChance: 0.3, soundVolume: 0.8, breakVolume: 1.5, breakNoise: 30 };
95
+ function createDoors(grid, options) {
96
+ const N = { ...DOOR_DEFAULTS, ...options.numbers };
97
+ const random = options.random;
98
+ const list = [];
99
+ const doorAt = new Int16Array(grid.n * grid.n).fill(-1);
100
+ const listeners = /* @__PURE__ */ new Map();
101
+ const emit = (event, payload) => {
102
+ for (const listener of listeners.get(event) ?? []) listener(payload);
103
+ };
104
+ const set = (index, open) => {
105
+ const d = list[index];
106
+ if (!d) return;
107
+ d.open = open;
108
+ grid.fillRect(...d.rect, open || d.broken ? 3 : 5, N.height);
109
+ };
110
+ return {
111
+ list,
112
+ at: (cell) => cell < 0 ? -1 : doorAt[cell],
113
+ add(x, z, axis, width, data) {
114
+ const i = list.length;
115
+ const t = N.thickness;
116
+ const x0 = axis === "x" ? x - width / 2 : x - t;
117
+ const x1 = axis === "x" ? x + width / 2 : x + t;
118
+ const z0 = axis === "x" ? z - t : z - width / 2;
119
+ const z1 = axis === "x" ? z + t : z + width / 2;
120
+ list.push({ x, z, axis, width, hp: N.doorHp, hpMax: N.doorHp, open: false, broken: false, planks: 0, rect: [x0, z0, x1, z1], data });
121
+ grid.fillRect(x0, z0, x1, z1, 5, N.height);
122
+ const a = Math.floor((x0 + grid.half) / grid.cell);
123
+ const b = Math.ceil((x1 + grid.half) / grid.cell);
124
+ const c = Math.floor((z0 + grid.half) / grid.cell);
125
+ const e = Math.ceil((z1 + grid.half) / grid.cell);
126
+ for (let gz = c; gz < e; gz++) for (let gx = a; gx < b; gx++) doorAt[gz * grid.n + gx] = i;
127
+ return i;
128
+ },
129
+ set,
130
+ hit(index, damage) {
131
+ const d = list[index];
132
+ if (!d || d.open || d.broken) return;
133
+ d.hp -= damage;
134
+ if (random() < N.soundChance) emit("sound", { sound: "wood", volume: N.soundVolume, position: [d.x, 1, d.z] });
135
+ if (d.planks && d.hp < d.hpMax - d.planks * N.plankHp + N.plankHp * (d.planks - 1)) d.planks = Math.max(0, d.planks - 1);
136
+ if (d.hp <= 0) {
137
+ d.broken = true;
138
+ d.planks = 0;
139
+ set(index, true);
140
+ emit("noise", { x: d.x, z: d.z, radius: N.breakNoise });
141
+ emit("sound", { sound: "wood", volume: N.breakVolume, position: [d.x, 1, d.z] });
142
+ emit("effect", { effect: "dust", point: [d.x, 1, d.z], color: 9071178 });
143
+ emit("broken", { door: index });
144
+ }
145
+ },
146
+ plank(index) {
147
+ const d = list[index];
148
+ if (!d) return;
149
+ d.planks += 1;
150
+ d.hp += N.plankHp;
151
+ d.hpMax += N.plankHp;
152
+ },
153
+ on(event, listener) {
154
+ let set_ = listeners.get(event);
155
+ if (!set_) listeners.set(event, set_ = /* @__PURE__ */ new Set());
156
+ set_.add(listener);
157
+ return () => void set_.delete(listener);
158
+ }
159
+ };
160
+ }
161
+
162
+ // src/grid.ts
163
+ var CELL = { FREE: 0, SOLID: 1, LOW_WALL: 2, OPEN_DOOR: 3, GLASS: 4, CLOSED_DOOR: 5 };
164
+ var BORDER_WIDTH = 1;
165
+ var BORDER_HEIGHT = 50;
166
+ var LOS_STEP = 0.5;
167
+ var LOS_HEIGHT = 1.5;
168
+ function createNavGrid(options = {}) {
169
+ const size = options.size ?? 768;
170
+ const half = size / 2;
171
+ const cell = options.cell ?? 0.5;
172
+ const n = size / cell;
173
+ const occ = new Uint8Array(n * n);
174
+ const occH = new Float32Array(n * n);
175
+ const cellOf = (x, z) => {
176
+ const gx = Math.floor((x + half) / cell);
177
+ const gz = Math.floor((z + half) / cell);
178
+ return gx < 0 || gz < 0 || gx >= n || gz >= n ? -1 : gz * n + gx;
179
+ };
180
+ const solidAt = (x, z, y = 0.5) => {
181
+ const c = cellOf(x, z);
182
+ return c < 0 || occ[c] !== 0 && occ[c] !== 3 && occH[c] > y;
183
+ };
184
+ const fillRect = (x0, z0, x1, z1, kind, h) => {
185
+ const a = Math.floor((x0 + half) / cell);
186
+ const b = Math.ceil((x1 + half) / cell);
187
+ const c = Math.floor((z0 + half) / cell);
188
+ const d = Math.ceil((z1 + half) / cell);
189
+ for (let gz = Math.max(0, c); gz < Math.min(n, d); gz++)
190
+ for (let gx = Math.max(0, a); gx < Math.min(n, b); gx++) {
191
+ const i = gz * n + gx;
192
+ occ[i] = kind;
193
+ occH[i] = h;
194
+ }
195
+ };
196
+ if (options.border ?? true) {
197
+ fillRect(-half, -half, half, -half + BORDER_WIDTH, 1, BORDER_HEIGHT);
198
+ fillRect(-half, half - BORDER_WIDTH, half, half, 1, BORDER_HEIGHT);
199
+ fillRect(-half, -half, -half + BORDER_WIDTH, half, 1, BORDER_HEIGHT);
200
+ fillRect(half - BORDER_WIDTH, -half, half, half, 1, BORDER_HEIGHT);
201
+ }
202
+ return {
203
+ size,
204
+ half,
205
+ cell,
206
+ n,
207
+ occ,
208
+ occH,
209
+ cellOf,
210
+ solidAt,
211
+ cellKind: (x, z) => {
212
+ const c = cellOf(x, z);
213
+ return c < 0 ? -1 : occ[c];
214
+ },
215
+ fillRect,
216
+ blocked(x, z) {
217
+ const c = cellOf(x, z);
218
+ return c < 0 || occ[c] !== 0 && occ[c] !== 3;
219
+ },
220
+ losClear(x0, z0, x1, z1) {
221
+ const d = Math.hypot(x1 - x0, z1 - z0);
222
+ const steps = Math.ceil(d / LOS_STEP);
223
+ for (let k = 1; k < steps; k++) {
224
+ const t = k / steps;
225
+ const c = cellOf(x0 + (x1 - x0) * t, z0 + (z1 - z0) * t);
226
+ if (c < 0 || occ[c] === 1 && occH[c] > LOS_HEIGHT) return false;
227
+ }
228
+ return true;
229
+ }
230
+ };
231
+ }
232
+
233
+ // src/rng.ts
234
+ function createRng(seed) {
235
+ let state = seed >>> 0;
236
+ return () => {
237
+ state = state + 1831565813 >>> 0;
238
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
239
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
240
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
241
+ };
242
+ }
243
+
244
+ // src/store.ts
245
+ var createStore = (capacity) => {
246
+ const f32 = () => new Float32Array(capacity);
247
+ const u8 = () => new Uint8Array(capacity);
248
+ return {
249
+ n: 0,
250
+ capacity,
251
+ x: f32(),
252
+ z: f32(),
253
+ vx: f32(),
254
+ vz: f32(),
255
+ yaw: f32(),
256
+ phase: f32(),
257
+ type: u8(),
258
+ age: u8(),
259
+ outfit: u8(),
260
+ height: f32(),
261
+ build: f32(),
262
+ chaseSpeed: f32(),
263
+ wanderSpeed: f32(),
264
+ hp: f32(),
265
+ hpMax: f32(),
266
+ headHp: f32(),
267
+ armHp: new Float32Array(capacity * 2),
268
+ legHp: new Float32Array(capacity * 2),
269
+ missing: new Uint16Array(capacity),
270
+ helmet: u8(),
271
+ state: u8(),
272
+ stateTimer: f32(),
273
+ lost: f32(),
274
+ attackTimer: f32(),
275
+ band: u8(),
276
+ next: f32(),
277
+ wx: f32(),
278
+ wz: f32(),
279
+ crawl: u8(),
280
+ limp: u8(),
281
+ color: new Uint32Array(capacity),
282
+ skin: f32(),
283
+ alertIn: f32(),
284
+ cooldown: f32(),
285
+ y: f32(),
286
+ floor: u8(),
287
+ floorLocked: u8(),
288
+ vy: f32(),
289
+ abilityTimer: f32(),
290
+ eaten: u8(),
291
+ stuck: f32(),
292
+ stepCell: new Int32Array(capacity),
293
+ burn: f32(),
294
+ hunts: new Int8Array(capacity),
295
+ named: new Int8Array(capacity),
296
+ dvx: f32(),
297
+ dvz: f32(),
298
+ steering: u8(),
299
+ renderYaw: f32(),
300
+ renderSpeed: f32(),
301
+ pause: f32(),
302
+ lod: u8(),
303
+ variant: u8(),
304
+ attackPose: u8(),
305
+ arms: f32()
306
+ };
307
+ };
308
+ var AGENT_FIELDS = [
309
+ "x",
310
+ "z",
311
+ "vx",
312
+ "vz",
313
+ "yaw",
314
+ "phase",
315
+ "type",
316
+ "age",
317
+ "outfit",
318
+ "height",
319
+ "build",
320
+ "chaseSpeed",
321
+ "wanderSpeed",
322
+ "hp",
323
+ "hpMax",
324
+ "headHp",
325
+ "missing",
326
+ "helmet",
327
+ "state",
328
+ "stateTimer",
329
+ "lost",
330
+ "attackTimer",
331
+ "band",
332
+ "next",
333
+ "wx",
334
+ "wz",
335
+ "crawl",
336
+ "limp",
337
+ "color",
338
+ "skin",
339
+ "alertIn",
340
+ "cooldown",
341
+ "y",
342
+ "floor",
343
+ "floorLocked",
344
+ "vy",
345
+ "abilityTimer",
346
+ "eaten",
347
+ "stuck",
348
+ "stepCell",
349
+ "burn",
350
+ "hunts",
351
+ "named",
352
+ "dvx",
353
+ "dvz",
354
+ "steering",
355
+ "renderYaw",
356
+ "renderSpeed",
357
+ "pause",
358
+ "lod",
359
+ "variant",
360
+ "attackPose",
361
+ "arms"
362
+ ];
363
+
364
+ // src/types.ts
365
+ var STATE = {
366
+ WANDER: 0,
367
+ CHASE: 1,
368
+ GRAB: 2,
369
+ STUNNED: 3,
370
+ CALLING: 4,
371
+ AIRBORNE: 5,
372
+ DORMANT: 6,
373
+ SWELLING: 7,
374
+ SWING: 8,
375
+ EATING: 9,
376
+ PILE_STEP: 10,
377
+ SEEKING_SOUND: 12
378
+ };
379
+ var MISSING = { SKULL: 1, ARM_L: 2, ARM_R: 4, LEG_L: 8, LEG_R: 16, ARM_L_BROKEN: 32, ARM_R_BROKEN: 64, LEG_L_BROKEN: 128, LEG_R_BROKEN: 256, JAW: 512 };
380
+
381
+ // src/crowd.ts
382
+ var CROWD_DEFAULTS = {
383
+ sources: defaults_default.sources,
384
+ bands: defaults_default.bands.map((band) => ({ max: band.max ?? Infinity, dt: band.dt })),
385
+ numbers: defaults_default.numbers
386
+ };
387
+ var clamp = (v, a, b) => v < a ? a : v > b ? b : v;
388
+ var TAU_APPROX = 6.2832;
389
+ var TURN = 6.28;
390
+ function createCrowd(options) {
391
+ const random = options.random ?? createRng(options.seed ?? 1);
392
+ const R = random;
393
+ const pack = options.pack;
394
+ const types = pack.types;
395
+ const N = { ...CROWD_DEFAULTS.numbers, ...options.numbers };
396
+ const BANDS = options.bands ?? CROWD_DEFAULTS.bands;
397
+ const HYST = N.hysteresis;
398
+ const grid = options.grid ?? createNavGrid();
399
+ const doors = options.doors ?? createDoors(grid, { random });
400
+ const water = options.water ?? (() => false);
401
+ const plugins = options.plugins ?? [];
402
+ const clock = options.flow?.clock ?? (() => typeof performance === "undefined" ? Date.now() : performance.now());
403
+ const FLOW_R = options.flow?.radius ?? N.flowRadius;
404
+ const Z = createStore(options.capacity ?? N.capacity);
405
+ const { occ, occH, cellOf, solidAt } = grid;
406
+ const HALF = grid.half;
407
+ const GC = grid.cell;
408
+ const FW = grid.n;
409
+ const HC = N.hashCell;
410
+ const HN = Math.floor(grid.size / HC);
411
+ const hHead = new Int32Array(HN * HN);
412
+ const hNext = new Int32Array(Z.capacity);
413
+ const pile = new Float32Array(FW * FW);
414
+ let pileN = 0;
415
+ const flow = {
416
+ dir: new Int8Array(FW * FW * 2),
417
+ back: new Int8Array(FW * FW * 2),
418
+ dist: new Float32Array(FW * FW),
419
+ q: new Int32Array((2 * FLOW_R + 1) ** 2 * 2),
420
+ phase: 0,
421
+ qh: 0,
422
+ qt: 0,
423
+ c: 0,
424
+ t: 0,
425
+ cell: -1,
426
+ ms: 0,
427
+ win: [0, 0, -1, -1],
428
+ winB: [0, 0, -1, -1]
429
+ };
430
+ const catTable = [];
431
+ types.forEach((c, i) => {
432
+ for (let k = 0; k < c.weight; k++) catTable.push(i);
433
+ });
434
+ const variantsOf = [];
435
+ {
436
+ let next = 0;
437
+ types.forEach((type) => {
438
+ const list = [];
439
+ for (let v = 0; v < (type.variants ?? 1); v++) list.push(next++);
440
+ variantsOf.push(list);
441
+ });
442
+ }
443
+ const typeIndexById = new Map(types.map((type, index) => [type.id, index]));
444
+ const noises = [];
445
+ const stats = { bands: [0, 0, 0, 0], slots: 0, kills: 0, heads: 0 };
446
+ const corpses = createCorpseLedger({ max: N.corpsesMax, seconds: N.corpseSeconds });
447
+ const trail = [];
448
+ let trailLast = { x: 1e9, z: 1e9 };
449
+ const killQ = [];
450
+ let groanBudget = 0;
451
+ let migrateT = N.migrationFirst;
452
+ let hunt = null;
453
+ let target = options.target ?? null;
454
+ let nightF = 0;
455
+ let viewYaw = 0;
456
+ const listeners = /* @__PURE__ */ new Map();
457
+ const emit = (event, payload) => {
458
+ const set = listeners.get(event);
459
+ if (set) for (const listener of set) listener(payload);
460
+ };
461
+ const emitNoise = (x, z, r) => void noises.push({ x, z, r });
462
+ doors.on("noise", (e) => emitNoise(e.x, e.z, e.radius));
463
+ doors.on("sound", (e) => emit("sound", { ...e, distance: target ? Math.hypot(e.position[0] - target.state.x, e.position[2] - target.state.z) : 0 }));
464
+ doors.on("effect", (e) => emit("effect", e));
465
+ doors.on("broken", (e) => {
466
+ const d = doors.list[e.door];
467
+ if (target && Math.hypot(d.x - target.state.x, d.z - target.state.z) < N.doorMessageDistance) emit("message", { id: "door_broken", seconds: 2 });
468
+ });
469
+ const sound = (id, volume, x, y, z, d) => emit("sound", { sound: id, volume, position: [x, y, z], distance: d });
470
+ const pick = (list) => {
471
+ const r = R();
472
+ let acc = 0;
473
+ for (const it of list) {
474
+ acc += it.p;
475
+ if (r < acc) return it;
476
+ }
477
+ return list[0];
478
+ };
479
+ const T = (i) => types[Z.type[i]];
480
+ const traits = (i) => T(i).traits ?? {};
481
+ const special = (i) => {
482
+ const tr = traits(i);
483
+ Z.y[i] = 0;
484
+ Z.floor[i] = 0;
485
+ Z.floorLocked[i] = 0;
486
+ Z.vy[i] = 0;
487
+ Z.abilityTimer[i] = 0;
488
+ Z.eaten[i] = 0;
489
+ Z.stuck[i] = 0;
490
+ Z.stepCell[i] = -1;
491
+ Z.burn[i] = 0;
492
+ Z.hunts[i] = -1;
493
+ Z.named[i] = -1;
494
+ if (tr.color !== void 0 && !tr.submerged) {
495
+ Z.color[i] = tr.color;
496
+ if (tr.skin !== void 0) Z.skin[i] = tr.skin;
497
+ }
498
+ if (tr.ambush && R() < tr.ambush.chance) {
499
+ Z.state[i] = STATE.DORMANT;
500
+ Z.yaw[i] = R() * TURN;
501
+ }
502
+ if (tr.submerged) {
503
+ Z.state[i] = STATE.DORMANT;
504
+ Z.y[i] = tr.submerged.depth;
505
+ if (tr.color !== void 0) Z.color[i] = tr.color;
506
+ }
507
+ };
508
+ const spawn = (x, z, catForce = -1) => {
509
+ if (Z.n >= Z.capacity) return -1;
510
+ const i = Z.n++;
511
+ const c = catForce >= 0 ? catForce : catTable[Math.floor(R() * catTable.length)];
512
+ const C = types[c];
513
+ const ai = pack.ages.indexOf(pick(pack.ages));
514
+ const maxAge = C.traits?.maxAge;
515
+ const A = pack.ages[maxAge !== void 0 ? Math.min(ai, maxAge) : ai];
516
+ const ri = pack.outfits.indexOf(pick(pack.outfits));
517
+ const RO = pack.outfits[ri];
518
+ Z.x[i] = x;
519
+ Z.z[i] = z;
520
+ Z.vx[i] = Z.vz[i] = 0;
521
+ Z.type[i] = c;
522
+ Z.age[i] = pack.ages.indexOf(A);
523
+ Z.outfit[i] = ri;
524
+ const g = (R() + R() + R() - 1.5) * 0.09;
525
+ Z.height[i] = clamp(1 + g, 0.9, 1.1) * (C.scale || 1);
526
+ const bb = R();
527
+ Z.build[i] = (bb < 0.3 ? 0.85 + R() * 0.1 : bb < 0.8 ? 0.95 + R() * 0.13 : 1.08 + R() * 0.17) * (C.build || (C.scale ? 1.3 : 1));
528
+ const heavy = bb >= 0.8;
529
+ const thin = bb < 0.3;
530
+ Z.chaseSpeed[i] = C.speed.chase * A.speed * (thin ? 1.05 : heavy ? 0.9 : 1) * (0.92 + R() * 0.16);
531
+ Z.wanderSpeed[i] = C.speed.wander * A.speed * (0.8 + R() * 0.4);
532
+ Z.hpMax[i] = Z.hp[i] = C.health * A.health * (thin ? 0.9 : heavy ? 1.15 : 1);
533
+ Z.headHp[i] = pack.segments.head;
534
+ Z.armHp[i * 2] = Z.armHp[i * 2 + 1] = pack.segments.arm;
535
+ Z.legHp[i * 2] = Z.legHp[i * 2 + 1] = pack.segments.leg;
536
+ Z.missing[i] = 0;
537
+ Z.helmet[i] = RO.helmet ? 1 : 0;
538
+ Z.crawl[i] = C.crawls ? 1 : 0;
539
+ Z.limp[i] = 0;
540
+ Z.state[i] = 0;
541
+ Z.stateTimer[i] = 0;
542
+ Z.lost[i] = 0;
543
+ Z.attackTimer[i] = R();
544
+ Z.alertIn[i] = 0;
545
+ Z.cooldown[i] = 0;
546
+ {
547
+ const vs = variantsOf[c];
548
+ Z.variant[i] = vs[Math.floor(R() * vs.length)];
549
+ }
550
+ Z.yaw[i] = Z.renderYaw[i] = R() * TURN;
551
+ Z.renderSpeed[i] = 0;
552
+ Z.steering[i] = 0;
553
+ Z.pause[i] = 0;
554
+ Z.attackPose[i] = 0;
555
+ Z.arms[i] = 0;
556
+ Z.phase[i] = R() * 10;
557
+ Z.band[i] = 3;
558
+ Z.next[i] = R() * 0.05;
559
+ Z.wx[i] = x;
560
+ Z.wz[i] = z;
561
+ Z.color[i] = RO.color || pack.palette[Math.floor(R() * pack.palette.length)];
562
+ Z.skin[i] = R();
563
+ special(i);
564
+ emit("spawn", { agent: i, type: C.id });
565
+ return i;
566
+ };
567
+ const remove = (i) => {
568
+ if (Z.state[i] === STATE.PILE_STEP) stepRelease(i);
569
+ const j = --Z.n;
570
+ if (i === j) {
571
+ emit("remove", { agent: i, moved: -1 });
572
+ return;
573
+ }
574
+ for (const k of AGENT_FIELDS) Z[k][i] = Z[k][j];
575
+ Z.armHp[i * 2] = Z.armHp[j * 2];
576
+ Z.armHp[i * 2 + 1] = Z.armHp[j * 2 + 1];
577
+ Z.legHp[i * 2] = Z.legHp[j * 2];
578
+ Z.legHp[i * 2 + 1] = Z.legHp[j * 2 + 1];
579
+ if (target) {
580
+ if (target.state.grabbedBy === j) target.state.grabbedBy = i;
581
+ else if (target.state.grabbedBy === i) target.state.grabbedBy = -1;
582
+ }
583
+ emit("remove", { agent: i, moved: j });
584
+ };
585
+ const flowStart = (x, z) => {
586
+ const gx = clamp(Math.floor((x + HALF) / GC), 0, FW - 1);
587
+ const gz = clamp(Math.floor((z + HALF) / GC), 0, FW - 1);
588
+ const W = flow.winB;
589
+ W[0] = Math.max(0, gx - FLOW_R);
590
+ W[1] = Math.max(0, gz - FLOW_R);
591
+ W[2] = Math.min(FW - 1, gx + FLOW_R);
592
+ W[3] = Math.min(FW - 1, gz + FLOW_R);
593
+ for (let r = W[1]; r <= W[3]; r++) flow.dist.fill(1e9, r * FW + W[0], r * FW + W[2] + 1);
594
+ const s = gz * FW + gx;
595
+ flow.dist[s] = 0;
596
+ flow.q[0] = s;
597
+ flow.qh = 0;
598
+ flow.qt = 1;
599
+ flow.phase = 1;
600
+ flow.c = 0;
601
+ flow.t = 0;
602
+ };
603
+ const flowStep = (budget) => {
604
+ if (!flow.phase) return;
605
+ const t0 = clock();
606
+ const D = flow.dist;
607
+ const Q = flow.q;
608
+ const F = flow.back;
609
+ const W = flow.winB;
610
+ let n = 0;
611
+ while (flow.phase === 1) {
612
+ if (flow.qh >= flow.qt) {
613
+ flow.phase = 2;
614
+ break;
615
+ }
616
+ const c = Q[flow.qh++];
617
+ const cx = c % FW;
618
+ const cz = c / FW | 0;
619
+ const dc = D[c];
620
+ for (let dz = -1; dz <= 1; dz++)
621
+ for (let dx = -1; dx <= 1; dx++) {
622
+ if (!dx && !dz) continue;
623
+ const nx = cx + dx;
624
+ const nz = cz + dz;
625
+ if (nx < W[0] || nz < W[1] || nx > W[2] || nz > W[3]) continue;
626
+ const k = nz * FW + nx;
627
+ if (occ[k] && occ[k] < 3) continue;
628
+ if (dx && dz && (occ[cz * FW + nx] && occ[cz * FW + nx] < 3 || occ[nz * FW + cx] && occ[nz * FW + cx] < 3)) continue;
629
+ const nd = dc + (dx && dz ? 1.414 : 1);
630
+ if (nd < D[k] - 1e-3) {
631
+ D[k] = nd;
632
+ if (flow.qt < Q.length) Q[flow.qt++] = k;
633
+ }
634
+ }
635
+ if ((++n & 2047) === 0 && clock() - t0 > budget) {
636
+ flow.t += clock() - t0;
637
+ return;
638
+ }
639
+ }
640
+ const ww = W[2] - W[0] + 1;
641
+ while (flow.phase === 2) {
642
+ const j = flow.c++;
643
+ if (j >= ww * (W[3] - W[1] + 1)) {
644
+ const tmp = flow.dir;
645
+ flow.dir = F;
646
+ flow.back = tmp;
647
+ const tw = flow.win;
648
+ flow.win = W;
649
+ flow.winB = tw;
650
+ flow.phase = 0;
651
+ flow.ms = flow.t + clock() - t0;
652
+ return;
653
+ }
654
+ const cx = W[0] + j % ww;
655
+ const cz = W[1] + (j / ww | 0);
656
+ const c = cz * FW + cx;
657
+ if (occ[c] && occ[c] < 3) {
658
+ F[c * 2] = F[c * 2 + 1] = 0;
659
+ continue;
660
+ }
661
+ let best = D[c];
662
+ let bx = 0;
663
+ let bz = 0;
664
+ for (let dz = -1; dz <= 1; dz++)
665
+ for (let dx = -1; dx <= 1; dx++) {
666
+ const nx = cx + dx;
667
+ const nz = cz + dz;
668
+ if (nx < W[0] || nz < W[1] || nx > W[2] || nz > W[3]) continue;
669
+ const d = D[nz * FW + nx];
670
+ if (d < best) {
671
+ best = d;
672
+ bx = dx;
673
+ bz = dz;
674
+ }
675
+ }
676
+ F[c * 2] = bx;
677
+ F[c * 2 + 1] = bz;
678
+ if ((j & 8191) === 0 && clock() - t0 > budget) {
679
+ flow.t += clock() - t0;
680
+ return;
681
+ }
682
+ }
683
+ };
684
+ const losClear = grid.losClear;
685
+ const groundAt = (c, _y) => {
686
+ if (c < 0) return 0;
687
+ const k = occ[c];
688
+ if (k === 2 || k === 1 && occH[c] <= _y + 0.05) return occH[c];
689
+ return pile[c];
690
+ };
691
+ const blockedAt = (i, x, z) => {
692
+ const c = cellOf(x, z);
693
+ if (c < 0) return true;
694
+ const k = occ[c];
695
+ const y = Z.y[i];
696
+ if (k === 0 || k === 3) return pile[c] > y + 0.5 && Z.state[i] !== STATE.AIRBORNE;
697
+ if (k === 2 && y + 0.5 >= occH[c]) return false;
698
+ if (Z.state[i] === STATE.AIRBORNE && occH[c] <= y) return false;
699
+ return true;
700
+ };
701
+ const tryStep = (i, hitCell) => {
702
+ if (pileN >= N.pileMax) return false;
703
+ const c = cellOf(Z.x[i], Z.z[i]);
704
+ if (c < 0 || occ[c] !== 0) return false;
705
+ const dir = hitCell - c;
706
+ const w = c + dir;
707
+ const back = c - dir;
708
+ const h = pile[c] + N.pileStep;
709
+ if (h > N.pileTop) return false;
710
+ const cap = occ[w] === 2 ? (back >= 0 ? pile[back] : 0) + 1 : pile[w];
711
+ if (h > cap + 1e-3) return false;
712
+ pile[c] = h;
713
+ pileN++;
714
+ Z.state[i] = STATE.PILE_STEP;
715
+ Z.stepCell[i] = c;
716
+ Z.vx[i] = Z.vz[i] = 0;
717
+ Z.y[i] = h - N.pileStep;
718
+ Z.stateTimer[i] = 6 + R() * 6;
719
+ return true;
720
+ };
721
+ function stepRelease(i) {
722
+ const c = Z.stepCell[i];
723
+ if (c >= 0) {
724
+ pile[c] = Math.max(0, pile[c] - N.pileStep);
725
+ pileN = Math.max(0, pileN - 1);
726
+ }
727
+ Z.stepCell[i] = -1;
728
+ Z.state[i] = STATE.CHASE;
729
+ Z.stuck[i] = 0;
730
+ }
731
+ const pileBlast = (x, z, r) => {
732
+ for (let i = 0; i < Z.n; i++)
733
+ if (Z.state[i] === STATE.PILE_STEP && (Z.x[i] - x) ** 2 + (Z.z[i] - z) ** 2 < r * r) {
734
+ stepRelease(i);
735
+ Z.state[i] = STATE.STUNNED;
736
+ Z.stateTimer[i] = 1.2;
737
+ }
738
+ };
739
+ const voice = (i) => traits(i).voice ?? "groan";
740
+ const groan = (i, d) => {
741
+ const r2 = N.alertRadius * N.alertRadius;
742
+ for (let j = 0; j < Z.n; j++) {
743
+ if (Z.state[j] !== 0 || Z.alertIn[j] > 0) continue;
744
+ const ex = Z.x[j] - Z.x[i];
745
+ const ez = Z.z[j] - Z.z[i];
746
+ if (ex * ex + ez * ez < r2) Z.alertIn[j] = N.alertDelay + R() * N.alertDelayRandom;
747
+ }
748
+ emit("alert", { agent: i });
749
+ if (d < N.voiceDistance && groanBudget <= 0) {
750
+ sound(voice(i), 1, Z.x[i], Z.height[i] * 1.6, Z.z[i], d);
751
+ groanBudget = N.voiceEvery;
752
+ }
753
+ };
754
+ const screamAt = (i, x, z) => {
755
+ const tr = traits(i).caller;
756
+ sound("scream_hoarse", 1, x, 1.6, z, target ? Math.hypot(x - target.state.x, z - target.state.z) : 0);
757
+ for (let j = 0; j < Z.n; j++) {
758
+ const ex = Z.x[j] - x;
759
+ const ez = Z.z[j] - z;
760
+ if (ex * ex + ez * ez < tr.radius * tr.radius) {
761
+ Z.state[j] = STATE.CHASE;
762
+ Z.lost[j] = 0;
763
+ }
764
+ }
765
+ if (target) target.state.screamBoost = tr.boost;
766
+ emit("message", { id: "called", seconds: 3, type: T(i).id });
767
+ emit("scream", { agent: i, x, z });
768
+ };
769
+ const attack = (i, d, tdt) => {
770
+ Z.attackTimer[i] -= tdt;
771
+ if (Z.attackTimer[i] > 0) return;
772
+ Z.attackTimer[i] = N.attackEvery;
773
+ const C = T(i);
774
+ const tr = C.traits ?? {};
775
+ if (tr.heavySwing && R() < tr.heavySwing.chance) {
776
+ Z.state[i] = STATE.SWING;
777
+ Z.stateTimer[i] = tr.heavySwing.windUp;
778
+ return;
779
+ }
780
+ const noJaw = Z.missing[i] & 512;
781
+ const noArms = (Z.missing[i] & 6) === 6;
782
+ const P = target;
783
+ if (P.state.grabbedBy < 0 && !noArms && !Z.crawl[i] && R() < C.bite * (P.state.crouch ? N.crouchBite : 1)) {
784
+ P.grab(i, !!noJaw, !!tr.drownGrab && water(P.state.x, P.state.z));
785
+ Z.state[i] = STATE.GRAB;
786
+ } else {
787
+ P.hurt(C.damage * (Z.crawl[i] ? N.crawlDamage : 1), i);
788
+ if (tr.burns && Z.abilityTimer[i] >= 0) P.state.burnTimer = tr.burns.seconds;
789
+ }
790
+ void d;
791
+ };
792
+ const effect = (payload) => emit("effect", payload);
793
+ const blood = (x, y, z, age, amount, died, agent) => effect({ effect: "blood", point: [x, y, z], age, amount, died, ...agent === void 0 ? {} : { agent } });
794
+ const damage = (i, hit) => {
795
+ const C = T(i);
796
+ const A = pack.ages[Z.age[i]];
797
+ const RO = pack.outfits[Z.outfit[i]];
798
+ const cal = hit.caliber ?? null;
799
+ const proj = !!cal;
800
+ const dano = hit.amount;
801
+ const dist = hit.distance;
802
+ const corte = hit.cut ?? 0;
803
+ const regiao = hit.region;
804
+ let morreu = false;
805
+ let headshot = false;
806
+ if (regiao === 0) {
807
+ if (proj && Z.helmet[i] && cal.class === "pistol") {
808
+ Z.helmet[i] = 0;
809
+ effect({ effect: "spark", point: [Z.x[i], Z.height[i] * 1.75, Z.z[i]], agent: i });
810
+ sound("impact", 1, Z.x[i], Z.height[i] * 1.75, Z.z[i], dist);
811
+ return { died: false };
812
+ }
813
+ const mata = proj ? (cal.skull === 1 || dist <= cal.skull) && dano >= N.skullMinDamage : dano * 3 >= Z.headHp[i];
814
+ if (mata) {
815
+ morreu = true;
816
+ headshot = true;
817
+ if (dano > N.skullTakeDamage || cal && N.skullTakeWithin[cal.id] !== void 0 && dist < N.skullTakeWithin[cal.id]) Z.missing[i] |= 1;
818
+ } else {
819
+ Z.hp[i] -= dano * 3;
820
+ Z.headHp[i] -= dano;
821
+ if (dano > N.jawDamage && R() < N.jawChance) Z.missing[i] |= 512;
822
+ }
823
+ } else if (regiao === 1) {
824
+ let m = C.multipliers.torso;
825
+ if (RO.vest && proj) m *= cal.class === "pistol" ? RO.vest.pistol : RO.vest.rifle;
826
+ Z.hp[i] -= dano * m;
827
+ } else {
828
+ const perna = regiao >= 4;
829
+ const lado = regiao & 1;
830
+ const mm = (perna ? C.multipliers.legs : C.multipliers.arms) * A.limb * (RO.limb || 1);
831
+ const segArr = perna ? Z.legHp : Z.armHp;
832
+ const k = i * 2 + (perna ? lado : regiao === 3 ? 1 : 0);
833
+ segArr[k] -= dano * mm;
834
+ Z.hp[i] -= dano * mm * 0.5;
835
+ const max = perna ? pack.segments.leg : pack.segments.arm;
836
+ if (perna && segArr[k] <= max * 0.5) Z.limp[i] = 1;
837
+ if (segArr[k] <= 0) {
838
+ const arranca = corte >= 0.5 || dano * mm > max * 0.9;
839
+ if (perna) {
840
+ Z.crawl[i] = 1;
841
+ if (arranca) Z.missing[i] |= lado ? 16 : 8;
842
+ else Z.missing[i] |= lado ? 256 : 128;
843
+ } else if (arranca) Z.missing[i] |= regiao === 3 ? 4 : 2;
844
+ else Z.missing[i] |= regiao === 3 ? 64 : 32;
845
+ effect({ effect: "limb", point: [Z.x[i], Z.height[i] * (perna ? 0.5 : 1.2), Z.z[i]], agent: i, region: regiao, severed: arranca });
846
+ }
847
+ }
848
+ if (Z.hp[i] <= 0) morreu = true;
849
+ blood(Z.x[i], regiao === 0 ? Z.height[i] * 1.65 : regiao === 1 ? Z.height[i] * 1.2 : Z.height[i] * 0.7, Z.z[i], Z.age[i], regiao === 0 ? 120 : 30, morreu, i);
850
+ if (morreu) {
851
+ kill(i, headshot);
852
+ return { died: morreu, headshot };
853
+ }
854
+ const st = Z.state[i];
855
+ if (dano >= N.hitStunDamage && st !== STATE.GRAB && st !== STATE.PILE_STEP && st !== STATE.AIRBORNE) {
856
+ Z.state[i] = STATE.STUNNED;
857
+ Z.stateTimer[i] = C.traits?.hitStun ?? N.hitStun;
858
+ }
859
+ const st2 = Z.state[i];
860
+ if (st2 === 0 || st2 === STATE.DORMANT || st2 === STATE.EATING || st2 === STATE.SEEKING_SOUND) {
861
+ Z.state[i] = STATE.CHASE;
862
+ Z.lost[i] = 0;
863
+ const sub = C.traits?.submerged;
864
+ if (sub) Z.y[i] = Math.max(Z.y[i], sub.surface);
865
+ }
866
+ return { died: morreu };
867
+ };
868
+ function kill(i, headshot = false) {
869
+ stats.kills++;
870
+ if (headshot) stats.heads++;
871
+ const tr = traits(i);
872
+ if (tr.burst) burstAt(i, Z.x[i], Z.z[i]);
873
+ const C = T(i);
874
+ emit("kill", { agent: i, type: C.id, headshot, x: Z.x[i], z: Z.z[i] });
875
+ corpseAdd(i);
876
+ if (target && target.state.grabbedBy === i) target.release();
877
+ remove(i);
878
+ }
879
+ const burstAt = (i, x, z) => {
880
+ const b = traits(i).burst;
881
+ effect({ effect: "cloud", point: [x, 0, z], radius: 4, color: b.cloudColor, amount: 8 });
882
+ effect({ effect: "blood", point: [x, 1, z], amount: 1500, died: true, color: b.bloodColor });
883
+ if (target) {
884
+ const d = Math.hypot(x - target.state.x, z - target.state.z);
885
+ if (d < b.radius) {
886
+ target.state.blind = b.blind;
887
+ target.state.scentTimer = b.scent;
888
+ emit("message", { id: "burst_hit", seconds: 2.5, type: T(i).id });
889
+ emitNoise(target.state.x, target.state.z, b.noiseNear);
890
+ }
891
+ }
892
+ emitNoise(x, z, b.noise);
893
+ };
894
+ const corpseAdd = (i) => {
895
+ corpses.add({
896
+ x: Z.x[i],
897
+ z: Z.z[i],
898
+ yaw: Z.renderYaw[i],
899
+ height: Z.height[i],
900
+ build: Z.build[i],
901
+ named: Z.named[i],
902
+ missing: Z.missing[i],
903
+ color: Z.color[i],
904
+ variant: Z.variant[i],
905
+ skin: Z.skin[i],
906
+ seed: R() * 10
907
+ });
908
+ };
909
+ const ability = (i, d, dx, dz, tdt, b, alive) => {
910
+ const C = T(i);
911
+ const tr = C.traits ?? {};
912
+ const st = Z.state[i];
913
+ const P = target;
914
+ if (st === STATE.DORMANT) {
915
+ Z.vx[i] = Z.vz[i] = 0;
916
+ if (!alive || b > 1) return true;
917
+ if (tr.submerged) {
918
+ if (d < tr.submerged.wake) {
919
+ Z.state[i] = STATE.CHASE;
920
+ groan(i, d);
921
+ sound("step", 2, Z.x[i], 0, Z.z[i], d);
922
+ }
923
+ return true;
924
+ }
925
+ const am = tr.ambush;
926
+ const wake = am?.wake ?? N.dormantWake;
927
+ const behind = am?.wakeBehind ?? N.dormantWakeBehind;
928
+ const fx = -Math.sin(viewYaw);
929
+ const fz = -Math.cos(viewYaw);
930
+ const costas = (dx * fx + dz * fz) / (d || 1) < (am?.behindCos ?? -0.3);
931
+ if (d < wake || costas && d < behind) {
932
+ Z.state[i] = STATE.CHASE;
933
+ Z.abilityTimer[i] = am?.sprintSeconds ?? 3;
934
+ Z.lost[i] = 0;
935
+ sound("runner_groan", 1, Z.x[i], 1.6, Z.z[i], d);
936
+ }
937
+ return true;
938
+ }
939
+ if (st === STATE.AIRBORNE || st === STATE.PILE_STEP) return true;
940
+ if (st === STATE.SWELLING) {
941
+ Z.vx[i] = Z.vz[i] = 0;
942
+ return true;
943
+ }
944
+ if (st === STATE.SWING) {
945
+ Z.vx[i] = Z.vz[i] = 0;
946
+ Z.stateTimer[i] -= tdt;
947
+ if (Z.stateTimer[i] <= 0) {
948
+ const hs = tr.heavySwing;
949
+ Z.state[i] = STATE.CHASE;
950
+ Z.attackTimer[i] = 2;
951
+ sound("impact", 1.4, Z.x[i], 1, Z.z[i], d);
952
+ if (d < hs.range && alive && P) {
953
+ P.hurt(C.damage, i);
954
+ P.knock(hs.knock);
955
+ emit("message", { id: "swing_knock", seconds: 1.5, type: C.id });
956
+ }
957
+ }
958
+ return true;
959
+ }
960
+ if (st === STATE.EATING) {
961
+ const ec2 = tr.eatsCorpses;
962
+ Z.vx[i] = Z.vz[i] = 0;
963
+ Z.stateTimer[i] -= tdt;
964
+ if (alive && d < ec2.defend) {
965
+ Z.state[i] = STATE.CHASE;
966
+ Z.lost[i] = 0;
967
+ groan(i, d);
968
+ return false;
969
+ }
970
+ if (Z.stateTimer[i] <= 0) {
971
+ let best = -1;
972
+ let bd = ec2.grabRange;
973
+ const list = corpses.entries;
974
+ for (let k = 0; k < list.length; k++) {
975
+ const e = Math.hypot(list[k].data.x - Z.x[i], list[k].data.z - Z.z[i]);
976
+ if (e < bd) {
977
+ bd = e;
978
+ best = k;
979
+ }
980
+ }
981
+ if (best >= 0) {
982
+ corpses.remove(best);
983
+ Z.hp[i] += ec2.health;
984
+ Z.hpMax[i] += ec2.health;
985
+ Z.height[i] *= ec2.grow;
986
+ Z.build[i] *= ec2.grow;
987
+ Z.eaten[i]++;
988
+ }
989
+ Z.state[i] = 0;
990
+ Z.abilityTimer[i] = ec2.rest;
991
+ }
992
+ return true;
993
+ }
994
+ if (st === STATE.SEEKING_SOUND) {
995
+ const ss = tr.soundSeeker ?? { arrive: 1, loseChase: 3, hitRadius2: 1.1 };
996
+ if (alive && d < 2.5) {
997
+ Z.state[i] = STATE.CHASE;
998
+ return false;
999
+ }
1000
+ const wx = Z.wx[i] - Z.x[i];
1001
+ const wz = Z.wz[i] - Z.z[i];
1002
+ const wl = Math.hypot(wx, wz);
1003
+ if (wl < ss.arrive) {
1004
+ Z.state[i] = 0;
1005
+ return false;
1006
+ }
1007
+ Z.vx[i] = wx / wl * Z.chaseSpeed[i];
1008
+ Z.vz[i] = wz / wl * Z.chaseSpeed[i];
1009
+ Z.yaw[i] = Math.atan2(wx, wz);
1010
+ if (b === 0) {
1011
+ Z.attackTimer[i] -= tdt;
1012
+ if (Z.attackTimer[i] <= 0) {
1013
+ const hx = Math.floor(Z.x[i] + HALF);
1014
+ const hz = Math.floor(Z.z[i] + HALF);
1015
+ if (hx >= 0 && hz >= 0 && hx < HN && hz < HN)
1016
+ for (let j = hHead[hz * HN + hx]; j !== -1; j = hNext[j]) {
1017
+ if (j !== i && (Z.x[j] - Z.x[i]) ** 2 + (Z.z[j] - Z.z[i]) ** 2 < ss.hitRadius2) {
1018
+ Z.attackTimer[i] = N.attackEvery;
1019
+ hurtOther(j, C.damage);
1020
+ break;
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ return true;
1026
+ }
1027
+ for (const plugin of plugins) if (plugin.ability?.(crowd, i, { d, dx, dz, tdt, band: b, alive })) return true;
1028
+ if (!alive) return false;
1029
+ if (Z.hunts[i] >= 0 && st <= 1 && hunt) return hunt(i, tdt);
1030
+ if (tr.soundSeeker && st === STATE.CHASE && d > tr.soundSeeker.loseChase) {
1031
+ Z.state[i] = STATE.SEEKING_SOUND;
1032
+ Z.wx[i] = P.state.x;
1033
+ Z.wz[i] = P.state.z;
1034
+ return false;
1035
+ }
1036
+ const po = tr.pounce;
1037
+ if (po && st === STATE.CHASE && Z.cooldown[i] <= 0 && d > po.min && d < po.max && b === 0 && losClear(Z.x[i], Z.z[i], P.state.x, P.state.z)) {
1038
+ const sp = Math.min(po.speedCap, d / po.time);
1039
+ Z.state[i] = STATE.AIRBORNE;
1040
+ Z.vy[i] = P.state.y > po.highAbove ? po.liftHigh : po.lift;
1041
+ Z.vx[i] = -dx / d * sp;
1042
+ Z.vz[i] = -dz / d * sp;
1043
+ Z.yaw[i] = Math.atan2(-dx, -dz);
1044
+ Z.cooldown[i] = po.cooldown;
1045
+ Z.abilityTimer[i] = 1;
1046
+ sound("runner_groan", 1, Z.x[i], 1.6, Z.z[i], d);
1047
+ return true;
1048
+ }
1049
+ if (tr.burst && st === STATE.CHASE && d < tr.burst.range) {
1050
+ Z.state[i] = STATE.SWELLING;
1051
+ Z.stateTimer[i] = tr.burst.swell;
1052
+ sound("flesh", 1.2, Z.x[i], 1, Z.z[i], d);
1053
+ return true;
1054
+ }
1055
+ const ec = tr.eatsCorpses;
1056
+ if (ec && st === 0 && Z.eaten[i] < ec.max && Z.abilityTimer[i] <= 0 && corpses.count) {
1057
+ let best = -1;
1058
+ let bd = ec.search;
1059
+ const list = corpses.entries;
1060
+ for (let k = 0; k < list.length; k++) {
1061
+ const e = Math.hypot(list[k].data.x - Z.x[i], list[k].data.z - Z.z[i]);
1062
+ if (e < bd) {
1063
+ bd = e;
1064
+ best = k;
1065
+ }
1066
+ }
1067
+ if (best >= 0) {
1068
+ if (bd < ec.reach) {
1069
+ Z.state[i] = STATE.EATING;
1070
+ Z.stateTimer[i] = ec.seconds;
1071
+ Z.vx[i] = Z.vz[i] = 0;
1072
+ return true;
1073
+ }
1074
+ const cp = list[best].data;
1075
+ const ex = cp.x - Z.x[i];
1076
+ const ez = cp.z - Z.z[i];
1077
+ const sp = Z.wanderSpeed[i] * ec.approach;
1078
+ Z.vx[i] = ex / bd * sp;
1079
+ Z.vz[i] = ez / bd * sp;
1080
+ Z.yaw[i] = Math.atan2(ex, ez);
1081
+ Z.wx[i] = cp.x;
1082
+ Z.wz[i] = cp.z;
1083
+ return true;
1084
+ } else Z.abilityTimer[i] = ec.retry;
1085
+ }
1086
+ const tk = tr.tracker;
1087
+ if (tk && st === 0 && trail.length) {
1088
+ let best = null;
1089
+ for (let k = trail.length - 1; k >= 0; k--) {
1090
+ const p = trail[k];
1091
+ if ((p.x - Z.x[i]) ** 2 + (p.z - Z.z[i]) ** 2 < tk.range * tk.range) {
1092
+ best = p;
1093
+ break;
1094
+ }
1095
+ }
1096
+ if (best) {
1097
+ Z.wx[i] = best.x;
1098
+ Z.wz[i] = best.z;
1099
+ let led = 0;
1100
+ const leads = typeIndexById.get(tk.leads) ?? -1;
1101
+ for (let j = 0; j < Z.n && led < tk.max; j++)
1102
+ if (Z.type[j] === leads && Z.state[j] === 0 && (Z.x[j] - Z.x[i]) ** 2 + (Z.z[j] - Z.z[i]) ** 2 < tk.leadRadius * tk.leadRadius) {
1103
+ Z.wx[j] = Z.x[i];
1104
+ Z.wz[j] = Z.z[i];
1105
+ led++;
1106
+ }
1107
+ }
1108
+ }
1109
+ return false;
1110
+ };
1111
+ const chaseSpeed = (i) => {
1112
+ const tr = traits(i);
1113
+ if (tr.ambush) return Z.abilityTimer[i] > 0 ? tr.ambush.sprint : tr.ambush.slow;
1114
+ if (tr.submerged && water(Z.x[i], Z.z[i])) return tr.submerged.swim;
1115
+ return Z.chaseSpeed[i];
1116
+ };
1117
+ const hurtOther = (j, amount) => {
1118
+ Z.hp[j] -= amount;
1119
+ blood(Z.x[j], Z.height[j] * 1.2, Z.z[j], Z.age[j], 30, false, j);
1120
+ if (Z.hp[j] <= 0) deferKill(j);
1121
+ };
1122
+ const deferKill = (j) => {
1123
+ if (!killQ.includes(j)) killQ.push(j);
1124
+ };
1125
+ const killQFlush = () => {
1126
+ if (!killQ.length) return;
1127
+ killQ.sort((a, b) => b - a);
1128
+ for (const j of killQ) if (j < Z.n) kill(j, false);
1129
+ killQ.length = 0;
1130
+ };
1131
+ const frame = (i, dt, d, alive) => {
1132
+ const st = Z.state[i];
1133
+ const tr = traits(i);
1134
+ if (st === STATE.PILE_STEP) {
1135
+ Z.stateTimer[i] -= dt;
1136
+ if (!alive || d > N.pileReleaseDistance || Z.stateTimer[i] <= 0) stepRelease(i);
1137
+ return;
1138
+ }
1139
+ if (st === STATE.SWELLING) {
1140
+ Z.stateTimer[i] -= dt;
1141
+ if (Z.stateTimer[i] <= 0) deferKill(i);
1142
+ return;
1143
+ }
1144
+ if (tr.ambush && Z.abilityTimer[i] > 0) Z.abilityTimer[i] -= dt;
1145
+ if (tr.eatsCorpses && Z.abilityTimer[i] > 0 && st === 0) Z.abilityTimer[i] -= dt;
1146
+ if (tr.waterWeak && Z.abilityTimer[i] === 0 && water(Z.x[i], Z.z[i])) {
1147
+ Z.abilityTimer[i] = -1;
1148
+ Z.hp[i] *= tr.waterWeak.keep;
1149
+ effect({ effect: "cloud", point: [Z.x[i], 0, Z.z[i]], radius: 1.5, color: tr.waterWeak.cloud, amount: 3, agent: i });
1150
+ }
1151
+ const c = cellOf(Z.x[i], Z.z[i]);
1152
+ const gy = groundAt(c, Z.y[i]);
1153
+ if (st === STATE.DORMANT || Z.floorLocked[i]) return;
1154
+ if (st === STATE.AIRBORNE || Z.y[i] > gy + 0.02 || Z.vy[i] > 0) {
1155
+ Z.vy[i] -= N.gravity * dt;
1156
+ Z.y[i] += Z.vy[i] * dt;
1157
+ if (Z.y[i] <= gy) {
1158
+ const fall = -Z.vy[i];
1159
+ Z.y[i] = gy;
1160
+ Z.vy[i] = 0;
1161
+ if (st === STATE.AIRBORNE) {
1162
+ Z.state[i] = STATE.CHASE;
1163
+ if (tr.pounce && alive && d < tr.pounce.landRange && target) {
1164
+ target.knock(tr.pounce.knock);
1165
+ target.hurt(T(i).damage, i);
1166
+ emit("message", { id: "pounce_knock", seconds: 1.5, type: T(i).id });
1167
+ }
1168
+ } else if (fall > N.fallStun && Z.state[i] <= 1) {
1169
+ Z.state[i] = STATE.STUNNED;
1170
+ Z.stateTimer[i] = 0.9;
1171
+ }
1172
+ }
1173
+ } else if (gy > Z.y[i]) Z.y[i] = Math.min(gy, Z.y[i] + N.climbSpeed * dt);
1174
+ };
1175
+ const update = (dt, context = {}) => {
1176
+ if (context.target !== void 0) target = context.target;
1177
+ groanBudget -= dt;
1178
+ nightF = context.nightFactor ?? 0;
1179
+ const P = target;
1180
+ const px = P ? P.state.x : context.flowTarget?.[0] ?? 0;
1181
+ const pz = P ? P.state.z : context.flowTarget?.[1] ?? 0;
1182
+ const alive = !!P && P.state.alive && !P.state.turned;
1183
+ viewYaw = context.viewYaw ?? (P ? P.state.yaw - Math.PI : 0);
1184
+ const fx0 = context.flowTarget?.[0] ?? px;
1185
+ const fz0 = context.flowTarget?.[1] ?? pz;
1186
+ const pc = cellOf(fx0, fz0);
1187
+ if (flow.cell < 0) {
1188
+ flowStart(fx0, fz0);
1189
+ flowStep(1e9);
1190
+ flow.cell = pc;
1191
+ } else if (pc !== flow.cell && !flow.phase) {
1192
+ flowStart(fx0, fz0);
1193
+ flow.cell = pc;
1194
+ }
1195
+ flowStep(N.flowBudgetMs);
1196
+ hHead.fill(-1);
1197
+ for (let i = 0; i < Z.n; i++) {
1198
+ if (Z.band[i] !== 0) continue;
1199
+ const hx = Math.floor(Z.x[i] + HALF);
1200
+ const hz = Math.floor(Z.z[i] + HALF);
1201
+ if (hx < 0 || hz < 0 || hx >= HN || hz >= HN) continue;
1202
+ const h = hz * HN + hx;
1203
+ hNext[i] = hHead[h];
1204
+ hHead[h] = i;
1205
+ }
1206
+ const B = stats.bands;
1207
+ B[0] = B[1] = B[2] = B[3] = 0;
1208
+ let slots = 0;
1209
+ const F = flow.dir;
1210
+ for (let i = 0; i < Z.n; i++) {
1211
+ const dx = Z.x[i] - px;
1212
+ const dz = Z.z[i] - pz;
1213
+ const d = Math.sqrt(dx * dx + dz * dz);
1214
+ let b = Z.band[i];
1215
+ while (b > 0 && d < BANDS[b - 1].max - HYST) b--;
1216
+ while (b < 3 && d > BANDS[b].max + HYST) b++;
1217
+ Z.band[i] = b;
1218
+ B[b]++;
1219
+ const C = T(i);
1220
+ const tr = C.traits ?? {};
1221
+ if (Z.state[i] === STATE.STUNNED) {
1222
+ Z.stateTimer[i] -= dt;
1223
+ if (Z.stateTimer[i] <= 0) Z.state[i] = STATE.CHASE;
1224
+ Z.vx[i] *= 0.9;
1225
+ Z.vz[i] *= 0.9;
1226
+ }
1227
+ if (Z.state[i] === STATE.CALLING) {
1228
+ Z.stateTimer[i] -= dt;
1229
+ Z.vx[i] = Z.vz[i] = 0;
1230
+ if (Z.stateTimer[i] <= 0) {
1231
+ Z.state[i] = STATE.CHASE;
1232
+ screamAt(i, Z.x[i], Z.z[i]);
1233
+ Z.cooldown[i] = tr.caller.cooldown;
1234
+ }
1235
+ }
1236
+ Z.cooldown[i] -= dt;
1237
+ if (Z.alertIn[i] > 0) {
1238
+ Z.alertIn[i] -= dt;
1239
+ if (Z.alertIn[i] <= 0 && Z.state[i] === 0 && !tr.soundSeeker) {
1240
+ Z.state[i] = STATE.CHASE;
1241
+ Z.lost[i] = 0;
1242
+ }
1243
+ }
1244
+ Z.next[i] -= dt;
1245
+ if (Z.next[i] <= 0) {
1246
+ const tdt = BANDS[b].dt;
1247
+ Z.next[i] += tdt;
1248
+ if (alive && b <= 1 && Z.state[i] <= 1) {
1249
+ let sees = false;
1250
+ const A = pack.ages[Z.age[i]];
1251
+ const vis = C.perception.sight * (1 - N.nightSight * nightF) * (P.state.crouch ? N.crouchSight : 1) * A.sight;
1252
+ if (d < vis) {
1253
+ const fx = Math.sin(Z.yaw[i]);
1254
+ const fz = Math.cos(Z.yaw[i]);
1255
+ const cone = (-dx * fx - dz * fz) / (d || 1);
1256
+ if ((cone > N.sightCone || d < N.sightNear) && (Z.floor[i] === P.state.floor || d < 5) && (Z.floor[i] || P.state.floor || losClear(Z.x[i], Z.z[i], px, pz))) sees = true;
1257
+ }
1258
+ if (C.perception.smell && d < C.perception.smell * (P.state.bleeding || P.state.scentTimer > 0 ? 2 : 1)) sees = true;
1259
+ if (sees) {
1260
+ if (Z.state[i] === 0) groan(i, d);
1261
+ Z.state[i] = STATE.CHASE;
1262
+ Z.lost[i] = 0;
1263
+ } else if (Z.state[i] === STATE.CHASE) {
1264
+ Z.lost[i] += tdt;
1265
+ if (Z.lost[i] > N.loseAfter) {
1266
+ Z.state[i] = 0;
1267
+ Z.wx[i] = px;
1268
+ Z.wz[i] = pz;
1269
+ }
1270
+ }
1271
+ }
1272
+ for (let k = 0; k < noises.length && (Z.state[i] === 0 || Z.state[i] === STATE.SEEKING_SOUND); k++) {
1273
+ const Nz = noises[k];
1274
+ const hear = Nz.r * (C.perception.hearing / N.hearingBase) * pack.ages[Z.age[i]].hearing;
1275
+ if ((Z.x[i] - Nz.x) ** 2 + (Z.z[i] - Nz.z) ** 2 < hear * hear) {
1276
+ if (tr.soundSeeker) {
1277
+ Z.state[i] = STATE.SEEKING_SOUND;
1278
+ Z.wx[i] = Nz.x;
1279
+ Z.wz[i] = Nz.z;
1280
+ break;
1281
+ }
1282
+ Z.state[i] = STATE.CHASE;
1283
+ Z.lost[i] = N.heardLost;
1284
+ groan(i, d);
1285
+ }
1286
+ }
1287
+ if (tr.caller && Z.state[i] === STATE.CHASE && d < tr.caller.range && Z.cooldown[i] <= 0) {
1288
+ Z.state[i] = STATE.CALLING;
1289
+ Z.stateTimer[i] = tr.caller.windUp;
1290
+ }
1291
+ Z.steering[i] = 0;
1292
+ if (!ability(i, d, dx, dz, tdt, b, alive)) {
1293
+ let tx = 0;
1294
+ let tz = 0;
1295
+ let sp = 0;
1296
+ if (Z.state[i] === STATE.CHASE || Z.state[i] === STATE.GRAB) {
1297
+ const c = cellOf(Z.x[i], Z.z[i]);
1298
+ const sameL = !P || Z.floor[i] === P.state.floor;
1299
+ if (d < N.directRange && alive && sameL) {
1300
+ tx = -dx / (d || 1);
1301
+ tz = -dz / (d || 1);
1302
+ } else if (c >= 0 && b === 0 && d < N.pyramidRange && sameL && flow.dist[c] < 1e8 && flow.dist[c] * GC > d * 2 + 6 && losClear(Z.x[i], Z.z[i], px, pz)) {
1303
+ tx = -dx / d;
1304
+ tz = -dz / d;
1305
+ } else if (c >= 0) {
1306
+ const W = flow.win;
1307
+ const cx = c % FW;
1308
+ const cz = c / FW | 0;
1309
+ if (cx >= W[0] && cx <= W[2] && cz >= W[1] && cz <= W[3]) {
1310
+ tx = F[c * 2];
1311
+ tz = F[c * 2 + 1];
1312
+ } else {
1313
+ tx = -dx;
1314
+ tz = -dz;
1315
+ }
1316
+ const l = Math.hypot(tx, tz) || 1;
1317
+ tx /= l;
1318
+ tz /= l;
1319
+ }
1320
+ if (tr.caller && Z.cooldown[i] > tr.caller.fleeAbove && d < tr.caller.fleeRange && alive) {
1321
+ tx = dx / (d || 1);
1322
+ tz = dz / (d || 1);
1323
+ }
1324
+ sp = chaseSpeed(i) * (P && P.state.screamBoost > 0 ? N.screamSpeed : 1);
1325
+ if (!alive) sp = Z.wanderSpeed[i];
1326
+ if (d < N.attackRange && alive && Math.abs(P.state.y - Z.y[i]) < N.attackHeight) {
1327
+ sp = 0;
1328
+ if (slots < N.attackSlots) {
1329
+ slots++;
1330
+ attack(i, d, tdt);
1331
+ }
1332
+ }
1333
+ } else if (Z.state[i] === 0) {
1334
+ const wx = Z.wx[i] - Z.x[i];
1335
+ const wz = Z.wz[i] - Z.z[i];
1336
+ const wl = Math.hypot(wx, wz);
1337
+ if (wl < 1 || R() < 0.01 * tdt * 60) {
1338
+ Z.wx[i] = clamp(Z.x[i] + (R() - 0.5) * N.wanderSpan, -HALF + 2, HALF - 2);
1339
+ Z.wz[i] = clamp(Z.z[i] + (R() - 0.5) * N.wanderSpan, -HALF + 2, HALF - 2);
1340
+ } else {
1341
+ tx = wx / wl;
1342
+ tz = wz / wl;
1343
+ Z.pause[i] -= tdt;
1344
+ if (Z.pause[i] < -4 - R() * 6) Z.pause[i] = 1 + R() * 2.5;
1345
+ sp = Z.pause[i] > 0 ? 0 : Z.wanderSpeed[i];
1346
+ }
1347
+ }
1348
+ if (Z.crawl[i]) sp = Math.min(sp, N.crawlSpeed);
1349
+ else if (Z.limp[i]) sp *= N.limpSpeed;
1350
+ let sx = 0;
1351
+ let sz = 0;
1352
+ if (b === 0) {
1353
+ const hx = Math.floor(Z.x[i] + HALF);
1354
+ const hz = Math.floor(Z.z[i] + HALF);
1355
+ for (let oz = -1; oz <= 1; oz++)
1356
+ for (let ox = -1; ox <= 1; ox++) {
1357
+ const qx = hx + ox;
1358
+ const qz = hz + oz;
1359
+ if (qx < 0 || qz < 0 || qx >= HN || qz >= HN) continue;
1360
+ for (let j = hHead[qz * HN + qx]; j !== -1; j = hNext[j]) {
1361
+ if (j === i || Z.state[j] === STATE.PILE_STEP || Z.state[j] === STATE.DORMANT) continue;
1362
+ const ex = Z.x[i] - Z.x[j];
1363
+ const ez = Z.z[i] - Z.z[j];
1364
+ const e2 = ex * ex + ez * ez;
1365
+ const rr = N.separation * (Z.build[i] + Z.build[j]);
1366
+ if (e2 < rr * rr * 1.6 && e2 > 1e-6) {
1367
+ const e = Math.sqrt(e2);
1368
+ const w = (rr * 1.26 - e) / e;
1369
+ sx += ex * w;
1370
+ sz += ez * w;
1371
+ }
1372
+ }
1373
+ }
1374
+ }
1375
+ if (Z.state[i] <= 2) {
1376
+ Z.dvx[i] = tx * sp + sx * N.separationPush;
1377
+ Z.dvz[i] = tz * sp + sz * N.separationPush;
1378
+ Z.steering[i] = 1;
1379
+ if (sp > 0.05) Z.yaw[i] = Math.atan2(tx, tz);
1380
+ else if (d < 3 && alive) Z.yaw[i] = Math.atan2(-dx, -dz);
1381
+ }
1382
+ }
1383
+ }
1384
+ if (Z.steering[i] && Z.state[i] <= 2) {
1385
+ const k = 1 - Math.exp(-dt * (b === 0 ? 9 : 5));
1386
+ Z.vx[i] += (Z.dvx[i] - Z.vx[i]) * k;
1387
+ Z.vz[i] += (Z.dvz[i] - Z.vz[i]) * k;
1388
+ }
1389
+ const nx = Z.x[i] + Z.vx[i] * dt;
1390
+ const nz = Z.z[i] + Z.vz[i] * dt;
1391
+ let hitCell = -1;
1392
+ if (!blockedAt(i, nx, Z.z[i])) Z.x[i] = nx;
1393
+ else {
1394
+ if (Z.state[i] !== STATE.AIRBORNE) Z.vx[i] = 0;
1395
+ hitCell = cellOf(nx, Z.z[i]);
1396
+ }
1397
+ if (!blockedAt(i, Z.x[i], nz)) Z.z[i] = nz;
1398
+ else {
1399
+ if (Z.state[i] !== STATE.AIRBORNE) Z.vz[i] = 0;
1400
+ hitCell = cellOf(Z.x[i], nz);
1401
+ }
1402
+ if (hitCell >= 0 && Z.state[i] === STATE.CHASE && b === 0) {
1403
+ const vl = tr.vaultsLow;
1404
+ if (vl && occ[hitCell] !== 5 && occH[hitCell] <= vl.maxHeight && occH[hitCell] > Z.y[i] + vl.minRise) {
1405
+ Z.state[i] = STATE.AIRBORNE;
1406
+ Z.vy[i] = Math.sqrt(2 * N.gravity * (occH[hitCell] - Z.y[i] + vl.clearance));
1407
+ const s2 = Z.chaseSpeed[i] * vl.speedFactor;
1408
+ Z.vx[i] = Math.sin(Z.yaw[i]) * s2;
1409
+ Z.vz[i] = Math.cos(Z.yaw[i]) * s2;
1410
+ } else if (occ[hitCell] === 2 || occ[hitCell] === 0 && pile[hitCell] > 0) {
1411
+ Z.stuck[i] += dt;
1412
+ if (Z.stuck[i] > 0.35 + 0.5 * Z.skin[i]) {
1413
+ Z.stuck[i] = 0;
1414
+ tryStep(i, hitCell);
1415
+ }
1416
+ }
1417
+ } else if (Z.stuck[i] > 0) Z.stuck[i] -= dt;
1418
+ if (hitCell >= 0 && Z.state[i] === STATE.CHASE && doors.at(hitCell) >= 0) {
1419
+ Z.attackTimer[i] -= dt;
1420
+ if (Z.attackTimer[i] <= 0) {
1421
+ Z.attackTimer[i] = N.attackEvery;
1422
+ doors.hit(doors.at(hitCell), N.doorDamage * (tr.doorDamage ?? 1));
1423
+ }
1424
+ }
1425
+ frame(i, dt, d, alive);
1426
+ const spd = Math.hypot(Z.vx[i], Z.vz[i]);
1427
+ Z.renderSpeed[i] += (spd - Z.renderSpeed[i]) * Math.min(1, dt * 6);
1428
+ if (Z.band[i] <= 1) {
1429
+ const s = Z.state[i];
1430
+ let goal = s === STATE.CHASE || s === STATE.GRAB || s === STATE.PILE_STEP ? 1 : 0;
1431
+ if (goal && solidAt(Z.x[i] + Math.sin(Z.renderYaw[i]) * 0.62, Z.z[i] + Math.cos(Z.renderYaw[i]) * 0.62, 1)) goal = 0.1;
1432
+ Z.arms[i] += (goal - Z.arms[i]) * Math.min(1, dt * (goal > Z.arms[i] ? 3 : 5));
1433
+ }
1434
+ Z.phase[i] += dt * (Z.renderSpeed[i] < 0.05 ? 0.15 : N.cadenceBase + N.cadencePerSpeed * Z.renderSpeed[i]);
1435
+ if (spd > 0.35 && Z.state[i] <= 1 && !Z.attackPose[i]) Z.yaw[i] = Math.atan2(Z.vx[i], Z.vz[i]);
1436
+ let dy = Z.yaw[i] - Z.renderYaw[i];
1437
+ dy -= Math.round(dy / TAU_APPROX) * TAU_APPROX;
1438
+ Z.renderYaw[i] += dy * Math.min(1, dt * (Z.state[i] === STATE.AIRBORNE ? 20 : 9));
1439
+ }
1440
+ killQFlush();
1441
+ noises.length = 0;
1442
+ stats.slots = slots;
1443
+ };
1444
+ const updateTrail = (dt, who = target) => {
1445
+ for (const p of trail) p.t += dt;
1446
+ while (trail.length && trail[0].t > N.trailSeconds) trail.shift();
1447
+ if (!who) return;
1448
+ const s = who.state;
1449
+ if ((s.bleeding || s.scentTimer > 0) && s.alive && Math.hypot(s.x - trailLast.x, s.z - trailLast.z) > N.trailStep) {
1450
+ trail.push({ x: s.x, z: s.z, t: 0 });
1451
+ trailLast = { x: s.x, z: s.z };
1452
+ if (trail.length > N.trailMax) trail.shift();
1453
+ }
1454
+ };
1455
+ const spawnAway = (minD, maxD, view) => {
1456
+ const ox = target ? target.state.x : view.x;
1457
+ const oz = target ? target.state.z : view.z;
1458
+ for (let t = 0; t < 30; t++) {
1459
+ const a = R() * TURN;
1460
+ const r = minD + Math.sqrt(R()) * (maxD - minD);
1461
+ const x = ox + Math.cos(a) * r;
1462
+ const z = oz + Math.sin(a) * r;
1463
+ const vx = x - view.x;
1464
+ const vz = z - view.z;
1465
+ const vd = Math.hypot(vx, vz);
1466
+ if (vd < view.far && vx * -Math.sin(view.yaw) + vz * -Math.cos(view.yaw) > vd * 0.5) continue;
1467
+ if (Math.abs(x) < HALF - 3 && Math.abs(z) < HALF - 3 && !grid.blocked(x, z)) return spawn(x, z);
1468
+ }
1469
+ return -1;
1470
+ };
1471
+ const updatePopulation = (dt, view, opts = {}) => {
1472
+ const popTarget = opts.target ?? N.population;
1473
+ let k = 0;
1474
+ while (Z.n < popTarget && k++ < 20) spawnAway(N.refillMin, N.refillMax, view);
1475
+ if (Z.n > popTarget + 120)
1476
+ for (let t = 0, rm = 0; t < 40 && rm < 4; t++) {
1477
+ const i = Math.floor(R() * Z.n);
1478
+ if (Z.band[i] < 2 || Z.named[i] >= 0 || target && i === target.state.grabbedBy) continue;
1479
+ const vx = Z.x[i] - view.x;
1480
+ const vz = Z.z[i] - view.z;
1481
+ const vd = Math.hypot(vx, vz);
1482
+ if (vd < view.far && vx * -Math.sin(view.yaw) + vz * -Math.cos(view.yaw) > -vd * 0.35) continue;
1483
+ remove(i);
1484
+ rm++;
1485
+ }
1486
+ const mg = opts.migration === void 0 ? null : opts.migration;
1487
+ if (!mg) return;
1488
+ migrateT -= dt;
1489
+ if (migrateT <= 0) {
1490
+ migrateT = mg.every;
1491
+ let m = 0;
1492
+ for (let q = 0; q < mg.count && Z.n < Z.capacity; q++) {
1493
+ const i = spawn(mg.x + (R() - 0.5) * mg.width, mg.z + R() * mg.depth);
1494
+ if (i >= 0) {
1495
+ Z.wx[i] = mg.goal[0];
1496
+ Z.wz[i] = mg.goal[1];
1497
+ m++;
1498
+ }
1499
+ }
1500
+ if (m) emit("message", { id: "migration", seconds: 4 });
1501
+ }
1502
+ };
1503
+ const crowd = {
1504
+ store: Z,
1505
+ pack,
1506
+ grid,
1507
+ doors,
1508
+ corpses,
1509
+ numbers: N,
1510
+ stats,
1511
+ pile,
1512
+ pileCount: () => pileN,
1513
+ trail,
1514
+ variantsOf,
1515
+ get target() {
1516
+ return target;
1517
+ },
1518
+ set target(next) {
1519
+ target = next;
1520
+ },
1521
+ random,
1522
+ sound,
1523
+ effect,
1524
+ typeIndex: (id) => typeIndexById.get(id) ?? -1,
1525
+ spawn: (x, z, type = -1) => spawn(x, z, type),
1526
+ remove,
1527
+ update,
1528
+ emitNoise,
1529
+ damage,
1530
+ kill: (i, headshot = false) => kill(i, headshot),
1531
+ updateTrail,
1532
+ updateCorpses: (dt) => void corpses.update(dt),
1533
+ updatePopulation,
1534
+ spawnAway,
1535
+ pileBlast,
1536
+ setHunt(next) {
1537
+ hunt = next;
1538
+ },
1539
+ chaseSpeed,
1540
+ asPlayerCrowd: () => ({
1541
+ count: () => Z.n,
1542
+ active: (i) => Z.band[i] === 0,
1543
+ x: (i) => Z.x[i],
1544
+ z: (i) => Z.z[i],
1545
+ y: (i) => Z.y[i],
1546
+ floor: (i) => Z.floor[i],
1547
+ chasing: (i) => Z.state[i] === STATE.CHASE,
1548
+ down: (i) => Z.crawl[i] !== 0 || Z.state[i] === STATE.STUNNED,
1549
+ panicWeight: (i) => traits(i).panicWeight ?? 1,
1550
+ hold(i, x, z, k, lookX, lookZ) {
1551
+ Z.x[i] += (x - Z.x[i]) * k;
1552
+ Z.z[i] += (z - Z.z[i]) * k;
1553
+ Z.vx[i] = Z.vz[i] = Z.dvx[i] = Z.dvz[i] = 0;
1554
+ Z.yaw[i] = Math.atan2(lookX - Z.x[i], lookZ - Z.z[i]);
1555
+ },
1556
+ stun(i, seconds, vx, vz) {
1557
+ Z.state[i] = STATE.STUNNED;
1558
+ Z.stateTimer[i] = seconds;
1559
+ Z.vx[i] = vx;
1560
+ Z.vz[i] = vz;
1561
+ },
1562
+ chase(i, cooldown) {
1563
+ Z.state[i] = STATE.CHASE;
1564
+ Z.attackTimer[i] = cooldown;
1565
+ },
1566
+ damage: (i, hit) => void damage(i, { region: hit.region, amount: hit.amount, distance: hit.distance, cut: hit.cut, caliber: null })
1567
+ }),
1568
+ on(event, listener) {
1569
+ let set = listeners.get(event);
1570
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
1571
+ set.add(listener);
1572
+ return () => void set.delete(listener);
1573
+ }
1574
+ };
1575
+ return crowd;
1576
+ }
1577
+
1578
+ // src/behaviours/civilians.ts
1579
+ var CIVILIAN_DEFAULTS = {
1580
+ hpAdult: 100,
1581
+ hpChild: 40,
1582
+ childOffset: 0.6,
1583
+ sightSquared: 100,
1584
+ leaveDistance: 18,
1585
+ attackRange: 1,
1586
+ attackEvery: 1.5,
1587
+ biteFactor: 1.5,
1588
+ biteSeconds: 120,
1589
+ fearAfterHit: 10,
1590
+ nearMessage: 30,
1591
+ fearRange: 6,
1592
+ fearSpeed: 3.4,
1593
+ walkSpeed: 1.3,
1594
+ childSpeed: 0.85,
1595
+ bittenSpeed: 0.8,
1596
+ groupRunBeyond: 6,
1597
+ groupRunSpeed: 3.6,
1598
+ fleeSpeed: 2.6,
1599
+ childFleeFactor: 1.1,
1600
+ stuckSeconds: 8,
1601
+ arrive: 0.25,
1602
+ probe: 0.45,
1603
+ whiskers: [0, 0.7, -0.7, 1.4, -1.4],
1604
+ doorProbe: 0.6,
1605
+ steer: 8,
1606
+ bodyHeight: 0.4,
1607
+ patrolChance: 0.2,
1608
+ childHeight: 0.65,
1609
+ childBuild: 0.7,
1610
+ cadenceBase: 0.5,
1611
+ cadencePerSpeed: 0.3,
1612
+ inside: 0.8,
1613
+ doorInside: 0.8,
1614
+ doorOutside: 0.9,
1615
+ gate: 7.6,
1616
+ sidewalk: 6,
1617
+ houseOffset: 0.8
1618
+ };
1619
+ function createCivilians(crowd, options) {
1620
+ const N = { ...CIVILIAN_DEFAULTS, ...options.numbers };
1621
+ const Z = crowd.store;
1622
+ const grid = crowd.grid;
1623
+ const doors = crowd.doors;
1624
+ const hourNow = options.hour ?? (() => 12);
1625
+ const turnInto = options.turnInto ?? 0;
1626
+ const listeners = /* @__PURE__ */ new Map();
1627
+ const emit = (event, payload) => {
1628
+ for (const listener of listeners.get(event) ?? []) listener(payload);
1629
+ };
1630
+ const random = crowd.random;
1631
+ const R = random;
1632
+ const lerp = (a, b, t) => a + (b - a) * t;
1633
+ const casaDe = (name) => options.buildings[name];
1634
+ const pontos = options.waypoints ?? ((b) => {
1635
+ const casa = !!b.house;
1636
+ const side = b.front;
1637
+ const front = side < 0 ? b.z + b.d : b.z;
1638
+ const back = side < 0 ? b.z : b.z + b.d;
1639
+ const cx = b.x + b.w / 2;
1640
+ const inw = side < 0 ? -1 : 1;
1641
+ const calc = side < 0 ? -N.sidewalk : N.sidewalk;
1642
+ const pts = [
1643
+ [cx + (casa ? N.houseOffset : 0), (front + (front + back) / 2) / 2],
1644
+ [cx, front + inw * N.doorInside],
1645
+ [cx, front - inw * N.doorOutside]
1646
+ ];
1647
+ if (casa) pts.push([cx, side < 0 ? -N.gate : N.gate]);
1648
+ pts.push([cx, calc]);
1649
+ return pts;
1650
+ });
1651
+ const rota = (bA, bB) => {
1652
+ const a = pontos(bA);
1653
+ const b = pontos(bB).reverse();
1654
+ const out = a.slice();
1655
+ if (Math.sign(a[a.length - 1][1]) !== Math.sign(b[0][1])) out.push([a[a.length - 1][0], b[0][1]]);
1656
+ return out.concat(b);
1657
+ };
1658
+ const list = options.people.map((spec, k) => {
1659
+ const b = casaDe(spec.home);
1660
+ const p = pontos(b)[0];
1661
+ return {
1662
+ id: spec.id,
1663
+ spec,
1664
+ k,
1665
+ x: p[0] + (spec.child ? N.childOffset : 0),
1666
+ z: p[1],
1667
+ yaw: 0,
1668
+ phase: 0,
1669
+ vx: 0,
1670
+ vz: 0,
1671
+ hp: spec.child ? N.hpChild : N.hpAdult,
1672
+ state: "home",
1673
+ path: [],
1674
+ pathIndex: 0,
1675
+ biteTimer: 0,
1676
+ attackTimer: 0,
1677
+ fear: 0,
1678
+ goal: spec.home,
1679
+ stuckTimer: 0,
1680
+ agent: -1
1681
+ };
1682
+ });
1683
+ const byId = new Map(list.map((c) => [c.id, c]));
1684
+ const exposed = (n) => n.state !== "dead" && n.state !== "undead" && n.state !== "gone";
1685
+ const target = () => crowd.target?.state ?? null;
1686
+ const distToTarget = (x, z) => {
1687
+ const t = target();
1688
+ return t ? Math.hypot(x - t.x, z - t.z) : 0;
1689
+ };
1690
+ const turn = (n) => {
1691
+ n.state = "undead";
1692
+ const i = crowd.spawn(n.x, n.z, turnInto);
1693
+ if (i >= 0) {
1694
+ Z.color[i] = n.spec.color ?? Z.color[i];
1695
+ Z.named[i] = n.k;
1696
+ Z.height[i] = n.spec.child ? N.childHeight : Z.height[i];
1697
+ Z.build[i] = n.spec.child ? N.childBuild : Z.build[i];
1698
+ Z.age[i] = 0;
1699
+ Z.state[i] = STATE.CHASE;
1700
+ n.agent = i;
1701
+ }
1702
+ emit("turned", { id: n.id, agent: i });
1703
+ };
1704
+ const die = (n) => {
1705
+ if (n.biteTimer > 0) return turn(n);
1706
+ n.state = "dead";
1707
+ n.vx = n.vz = 0;
1708
+ crowd.corpses.add({
1709
+ x: n.x,
1710
+ z: n.z,
1711
+ yaw: n.yaw,
1712
+ height: n.spec.child ? N.childHeight : 1,
1713
+ build: n.spec.child ? N.childBuild : 1,
1714
+ missing: 0,
1715
+ color: n.spec.color ?? 0,
1716
+ variant: -1,
1717
+ skin: 0.4,
1718
+ seed: R() * 10,
1719
+ named: n.k
1720
+ });
1721
+ crowd.effect({ effect: "blood", point: [n.x, 1, n.z], age: 0, amount: 300, died: true });
1722
+ emit("died", { id: n.id, cause: "attacked" });
1723
+ };
1724
+ const agentsAgainst = (dt) => {
1725
+ const t = target();
1726
+ for (let i = 0; i < Z.n; i++) {
1727
+ if (Z.band[i] > 1) continue;
1728
+ const k = Z.hunts[i];
1729
+ if (k >= 0) {
1730
+ const n = list[k];
1731
+ if (!exposed(n)) {
1732
+ Z.hunts[i] = -1;
1733
+ continue;
1734
+ }
1735
+ const dx = n.x - Z.x[i];
1736
+ const dz = n.z - Z.z[i];
1737
+ const d = Math.hypot(dx, dz);
1738
+ if (d > N.leaveDistance) {
1739
+ Z.hunts[i] = -1;
1740
+ continue;
1741
+ }
1742
+ if (d < N.attackRange) {
1743
+ Z.attackTimer[i] -= dt;
1744
+ if (Z.attackTimer[i] <= 0) {
1745
+ Z.attackTimer[i] = N.attackEvery;
1746
+ const C = crowd.pack.types[Z.type[i]];
1747
+ n.hp -= C.damage;
1748
+ n.fear = N.fearAfterHit;
1749
+ crowd.sound("flesh", 0.7, n.x, 1, n.z, distToTarget(n.x, n.z));
1750
+ crowd.effect({ effect: "blood", point: [n.x, 1.2, n.z], age: 0, amount: 30, died: false });
1751
+ if (n.biteTimer <= 0 && R() < C.bite * N.biteFactor) {
1752
+ n.biteTimer = N.biteSeconds;
1753
+ emit("bitten", { id: n.id, near: distToTarget(n.x, n.z) < N.nearMessage });
1754
+ }
1755
+ if (n.hp <= 0) die(n);
1756
+ }
1757
+ }
1758
+ } else if (Z.state[i] <= 1) {
1759
+ for (const n of list) {
1760
+ if (!exposed(n) || n.state === "home") continue;
1761
+ const dx = n.x - Z.x[i];
1762
+ const dz = n.z - Z.z[i];
1763
+ const d2 = dx * dx + dz * dz;
1764
+ const dp = t ? (t.x - Z.x[i]) ** 2 + (t.z - Z.z[i]) ** 2 : Infinity;
1765
+ if (d2 < N.sightSquared && d2 < dp && grid.losClear(Z.x[i], Z.z[i], n.x, n.z)) {
1766
+ Z.hunts[i] = n.k;
1767
+ Z.state[i] = STATE.CHASE;
1768
+ break;
1769
+ }
1770
+ }
1771
+ }
1772
+ }
1773
+ };
1774
+ crowd.setHunt((i) => {
1775
+ const n = list[Z.hunts[i]];
1776
+ if (!n) return false;
1777
+ const dx = n.x - Z.x[i];
1778
+ const dz = n.z - Z.z[i];
1779
+ const d = Math.hypot(dx, dz) || 1;
1780
+ const sp = d < 1 ? 0 : crowd.chaseSpeed(i);
1781
+ Z.vx[i] = dx / d * sp;
1782
+ Z.vz[i] = dz / d * sp;
1783
+ Z.yaw[i] = Math.atan2(dx, dz);
1784
+ return true;
1785
+ });
1786
+ const move = (n, dt, tx, tz, sp) => {
1787
+ const dx = tx - n.x;
1788
+ const dz = tz - n.z;
1789
+ const d = Math.hypot(dx, dz);
1790
+ if (d < N.arrive) {
1791
+ n.vx = n.vz = 0;
1792
+ return true;
1793
+ }
1794
+ let ax = dx / d;
1795
+ let az = dz / d;
1796
+ for (const a of N.whiskers) {
1797
+ const c2 = Math.cos(a);
1798
+ const s = Math.sin(a);
1799
+ const bx = ax * c2 - az * s;
1800
+ const bz = ax * s + az * c2;
1801
+ if (!grid.solidAt(n.x + bx * N.probe, n.z + bz * N.probe, N.bodyHeight)) {
1802
+ ax = bx;
1803
+ az = bz;
1804
+ break;
1805
+ }
1806
+ }
1807
+ const c = grid.cellOf(n.x + ax * N.doorProbe, n.z + az * N.doorProbe);
1808
+ const di = doors.at(c);
1809
+ if (c >= 0 && di >= 0) {
1810
+ const D = doors.list[di];
1811
+ if (!D.open && !D.broken && !D.planks) {
1812
+ doors.set(di, true);
1813
+ crowd.sound("wood", 0.3, D.x, 1, D.z, distToTarget(D.x, D.z));
1814
+ }
1815
+ }
1816
+ n.vx = lerp(n.vx, ax * sp, Math.min(1, dt * N.steer));
1817
+ n.vz = lerp(n.vz, az * sp, Math.min(1, dt * N.steer));
1818
+ const nx = n.x + n.vx * dt;
1819
+ const nz = n.z + n.vz * dt;
1820
+ if (!grid.solidAt(nx, n.z, N.bodyHeight)) n.x = nx;
1821
+ if (!grid.solidAt(n.x, nz, N.bodyHeight)) n.z = nz;
1822
+ n.yaw = Math.atan2(n.vx, n.vz);
1823
+ return false;
1824
+ };
1825
+ const update = (dt) => {
1826
+ agentsAgainst(dt);
1827
+ const h = hourNow();
1828
+ const t = target();
1829
+ for (const n of list) {
1830
+ if (n.state === "dead" || n.state === "undead" || n.state === "gone") continue;
1831
+ if (n.biteTimer > 0) {
1832
+ n.biteTimer -= dt;
1833
+ if (n.biteTimer <= 0) {
1834
+ turn(n);
1835
+ continue;
1836
+ }
1837
+ }
1838
+ let perto = 99;
1839
+ for (let i = 0; i < Z.n; i++) {
1840
+ if (Z.band[i]) continue;
1841
+ const d = Math.hypot(Z.x[i] - n.x, Z.z[i] - n.z);
1842
+ if (d < perto) perto = d;
1843
+ }
1844
+ const sp = (perto < N.fearRange ? N.fearSpeed : N.walkSpeed) * (n.spec.child ? N.childSpeed : 1) * (n.biteTimer > 0 ? N.bittenSpeed : 1);
1845
+ if (n.state === "group") {
1846
+ const leaderCivilian = n.spec.leader ? byId.get(n.spec.leader) : void 0;
1847
+ const lider = leaderCivilian && leaderCivilian.state === "group" ? leaderCivilian : t;
1848
+ if (lider) {
1849
+ const dd = Math.hypot(lider.x - n.x, lider.z - n.z);
1850
+ if (dd > (n.spec.followDistance ?? 2.2)) move(n, dt, lider.x, lider.z, dd > N.groupRunBeyond ? N.groupRunSpeed : sp);
1851
+ else {
1852
+ n.vx *= 0.8;
1853
+ n.vz *= 0.8;
1854
+ }
1855
+ }
1856
+ } else if (n.state === "fleeing") {
1857
+ const p = n.path[n.pathIndex];
1858
+ if (!p) {
1859
+ n.state = "gone";
1860
+ emit("gone", { id: n.id });
1861
+ continue;
1862
+ }
1863
+ n.stuckTimer += dt;
1864
+ if (move(n, dt, p[0], p[1], N.fleeSpeed * (n.spec.child ? N.childFleeFactor : 1)) || n.stuckTimer > N.stuckSeconds) {
1865
+ n.pathIndex++;
1866
+ n.stuckTimer = 0;
1867
+ }
1868
+ } else if (n.spec.routine) {
1869
+ let alvo = n.spec.home;
1870
+ for (const [hh, onde] of n.spec.routine) if (h >= hh && h < hh + 1) alvo = onde;
1871
+ if (perto < N.fearRange) alvo = n.spec.home;
1872
+ if (alvo !== n.goal) {
1873
+ const home = casaDe(n.spec.home);
1874
+ n.path = rota(casaDe(n.goal) ?? home, casaDe(alvo) ?? home);
1875
+ n.pathIndex = 0;
1876
+ n.goal = alvo;
1877
+ n.state = "walking";
1878
+ }
1879
+ if (n.state === "walking") {
1880
+ const p = n.path[n.pathIndex];
1881
+ n.stuckTimer += dt;
1882
+ if (!p) n.state = "home";
1883
+ else if (move(n, dt, p[0], p[1], sp) || n.stuckTimer > N.stuckSeconds) {
1884
+ n.pathIndex++;
1885
+ n.stuckTimer = 0;
1886
+ }
1887
+ } else {
1888
+ n.vx *= 0.8;
1889
+ n.vz *= 0.8;
1890
+ }
1891
+ } else {
1892
+ n.vx *= 0.8;
1893
+ n.vz *= 0.8;
1894
+ }
1895
+ {
1896
+ const v = Math.hypot(n.vx, n.vz);
1897
+ n.phase += dt * (v < 0.05 ? 0 : N.cadenceBase + N.cadencePerSpeed * v);
1898
+ }
1899
+ }
1900
+ for (let i = 0; i < Z.n; i++)
1901
+ if (Z.named[i] >= 0 && Z.state[i] === STATE.WANDER && R() < dt * N.patrolChance) {
1902
+ const n = list[Z.named[i]];
1903
+ if (!n) continue;
1904
+ const p = pontos(casaDe(n.spec.home));
1905
+ const q = p[Math.floor(R() * p.length)];
1906
+ Z.wx[i] = q[0];
1907
+ Z.wz[i] = q[1];
1908
+ }
1909
+ };
1910
+ return {
1911
+ list,
1912
+ get: (id) => byId.get(id),
1913
+ update,
1914
+ join(id) {
1915
+ const n = byId.get(id);
1916
+ if (n && exposed(n)) n.state = "group";
1917
+ },
1918
+ flee(id, path) {
1919
+ const n = byId.get(id);
1920
+ if (!n || !exposed(n)) return;
1921
+ n.state = "fleeing";
1922
+ n.path = path.map(([x, z]) => [x, z]);
1923
+ n.pathIndex = 0;
1924
+ },
1925
+ route(from, to) {
1926
+ return rota(casaDe(from), casaDe(to));
1927
+ },
1928
+ on(event, listener) {
1929
+ let set = listeners.get(event);
1930
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
1931
+ set.add(listener);
1932
+ return () => void set.delete(listener);
1933
+ }
1934
+ };
1935
+ }
1936
+
1937
+ // src/behaviours/herd.ts
1938
+ var HERD_DEFAULTS = {
1939
+ range: 8,
1940
+ fear: 5,
1941
+ walkFactor: 1.05,
1942
+ walkBase: 0.25,
1943
+ runFactor: 2.2,
1944
+ runBase: 0.8,
1945
+ activeDistance: 70,
1946
+ companionBehind: 2.2,
1947
+ companionRun: 6,
1948
+ companionStop: 0.8,
1949
+ retargetTries: 6,
1950
+ restBase: 4,
1951
+ restRandom: 7,
1952
+ lookEvery: 7,
1953
+ lookShare: 0.25,
1954
+ lookRadius: 40,
1955
+ fearAgentFactor: 1.5,
1956
+ fearAgentBase: 2,
1957
+ fleeStep: 6,
1958
+ arrive: 0.3,
1959
+ bodyHeight: 0.3,
1960
+ turn: 6,
1961
+ freeHeight: 0.6,
1962
+ worldMargin: 4
1963
+ };
1964
+ var HERD_ROLES = {
1965
+ dog: { range: 6, fear: 0, guard: { bark: 12, barkNoise: 60, barkCooldown: 3, growl: 40, growlCooldown: 10, sound: "bark" } },
1966
+ cat: { range: 6, fear: 5 },
1967
+ chicken: { range: 5, fear: 4, fleeCall: { sound: "cluck", noise: 30, cooldown: 6 } },
1968
+ southern_lapwing: { range: 5, fear: 0, alarm: { range: 12, sound: "lapwing", noise: 40, cooldown: 8, fly: 4 } },
1969
+ cow: { range: 10, fear: 6, call: { chance: 0.02, sound: "moo", noise: 80, cooldown: 30 } },
1970
+ horse: { range: 10, fear: 7 },
1971
+ pig: { range: 5, fear: 3 },
1972
+ capybara: { range: 8, fear: 15 },
1973
+ vulture: { sky: true }
1974
+ };
1975
+ function createHerd(crowd, options = {}) {
1976
+ const N = { ...HERD_DEFAULTS, ...options.numbers };
1977
+ const R = crowd.random;
1978
+ const Z = crowd.store;
1979
+ const grid = crowd.grid;
1980
+ const list = [];
1981
+ let time = 0;
1982
+ const listeners = /* @__PURE__ */ new Map();
1983
+ const emit = (event, payload) => {
1984
+ for (const listener of listeners.get(event) ?? []) listener(payload);
1985
+ };
1986
+ const view = options.view ?? (() => crowd.target?.state ?? { x: 0, z: 0 });
1987
+ const free = (x0, z0, r0, r1, tries = 40) => {
1988
+ for (let k = 0; k < tries; k++) {
1989
+ const a = R() * 6.28;
1990
+ const r = r0 + R() * (r1 - r0);
1991
+ const x = x0 + Math.cos(a) * r;
1992
+ const z = z0 + Math.sin(a) * r;
1993
+ if (Math.abs(x) < grid.half - N.worldMargin && Math.abs(z) < grid.half - N.worldMargin && !grid.solidAt(x, z, N.freeHeight) && !options.blocked?.(x, z)) return [x, z];
1994
+ }
1995
+ return null;
1996
+ };
1997
+ const nearestAgent = (x, z, r) => {
1998
+ let best = -1;
1999
+ let bd = r * r;
2000
+ for (let i = 0; i < Z.n; i++) {
2001
+ if (Z.state[i] === STATE.DORMANT) continue;
2002
+ const dx = Z.x[i] - x;
2003
+ const dz = Z.z[i] - z;
2004
+ const d = dx * dx + dz * dz;
2005
+ if (d < bd) {
2006
+ bd = d;
2007
+ best = i;
2008
+ }
2009
+ }
2010
+ return best < 0 ? null : [best, Math.sqrt(bd)];
2011
+ };
2012
+ const call = (E, sound, noise, dc) => {
2013
+ crowd.emitNoise(E.x, E.z, noise);
2014
+ emit("noise", { x: E.x, z: E.z, radius: noise, animal: E.index });
2015
+ emit("sound", { sound, volume: 1, position: [E.x, 0.5, E.z], distance: dc, animal: E.index });
2016
+ };
2017
+ const update = (dt) => {
2018
+ time += dt;
2019
+ const t = crowd.target?.state;
2020
+ const px = t?.x ?? 0;
2021
+ const pz = t?.z ?? 0;
2022
+ const v = view();
2023
+ for (const E of list) {
2024
+ if (E.spec.sky) continue;
2025
+ const dc = Math.hypot(E.x - v.x, E.z - v.z);
2026
+ if (dc >= N.activeDistance || E.dead) continue;
2027
+ E.timer -= dt;
2028
+ E.cooldown -= dt;
2029
+ let alvoX = E.targetX;
2030
+ let alvoZ = E.targetZ;
2031
+ let corre = false;
2032
+ const dp = t ? Math.hypot(E.x - px, E.z - pz) : Infinity;
2033
+ if (E.companion && t) {
2034
+ const bx = px - Math.sin(t.yaw) * N.companionBehind;
2035
+ const bz = pz - Math.cos(t.yaw) * N.companionBehind;
2036
+ alvoX = bx;
2037
+ alvoZ = bz;
2038
+ corre = dp > N.companionRun;
2039
+ if (Math.hypot(bx - E.x, bz - E.z) < N.companionStop) {
2040
+ alvoX = E.x;
2041
+ alvoZ = E.z;
2042
+ }
2043
+ } else if (E.timer <= 0) {
2044
+ const q = free(E.homeX, E.homeZ, 0, E.range, N.retargetTries);
2045
+ if (q) {
2046
+ E.targetX = q[0];
2047
+ E.targetZ = q[1];
2048
+ }
2049
+ E.timer = N.restBase + R() * N.restRandom;
2050
+ alvoX = E.targetX;
2051
+ alvoZ = E.targetZ;
2052
+ }
2053
+ const zp = (time * N.lookEvery + E.x) % 1 < N.lookShare || E.near === void 0 ? E.near = nearestAgent(E.x, E.z, N.lookRadius) : E.near;
2054
+ let fx = 0;
2055
+ let fz = 0;
2056
+ if (E.fear && !E.companion && dp < E.fear) {
2057
+ fx += E.x - px;
2058
+ fz += E.z - pz;
2059
+ }
2060
+ if (E.fear && zp && zp[1] < E.fear * N.fearAgentFactor + N.fearAgentBase) {
2061
+ fx += E.x - Z.x[zp[0]];
2062
+ fz += E.z - Z.z[zp[0]];
2063
+ }
2064
+ if (fx || fz) {
2065
+ const l = Math.hypot(fx, fz) || 1;
2066
+ alvoX = E.x + fx / l * N.fleeStep;
2067
+ alvoZ = E.z + fz / l * N.fleeStep;
2068
+ corre = true;
2069
+ const fc = E.spec.fleeCall;
2070
+ if (fc && E.cooldown <= 0) {
2071
+ E.cooldown = fc.cooldown;
2072
+ call(E, fc.sound, fc.noise, dc);
2073
+ }
2074
+ }
2075
+ const al = E.spec.alarm;
2076
+ if (al && dp < al.range && E.cooldown <= 0) {
2077
+ E.cooldown = al.cooldown;
2078
+ E.fly = al.fly;
2079
+ call(E, al.sound, al.noise, dc);
2080
+ emit("message", { id: "alarm", seconds: 2.5, animal: E.index });
2081
+ }
2082
+ const cl = E.spec.call;
2083
+ if (cl && E.cooldown <= 0 && R() < dt * cl.chance) {
2084
+ E.cooldown = cl.cooldown;
2085
+ call(E, cl.sound, cl.noise, dc);
2086
+ }
2087
+ const g = E.spec.guard;
2088
+ if (g && zp && E.cooldown <= 0) {
2089
+ if (zp[1] < g.bark) {
2090
+ E.cooldown = g.barkCooldown;
2091
+ call(E, g.sound, g.barkNoise, dc);
2092
+ if (E.companion) emit("message", { id: "bark", seconds: 1.5, animal: E.index, distance: Math.round(zp[1]) });
2093
+ } else if (E.companion && zp[1] < g.growl) {
2094
+ E.cooldown = g.growlCooldown;
2095
+ emit("message", { id: "growl", seconds: 2.5, animal: E.index, distance: Math.round(zp[1]) });
2096
+ }
2097
+ }
2098
+ const ax = alvoX - E.x;
2099
+ const az = alvoZ - E.z;
2100
+ const ad = Math.hypot(ax, az);
2101
+ let sp = 0;
2102
+ if (E.fly > 0) {
2103
+ E.fly -= dt;
2104
+ E.clip = "run";
2105
+ } else if (ad > N.arrive) {
2106
+ sp = corre ? E.runSpeed : E.walkSpeed;
2107
+ E.clip = corre ? "run" : "walk";
2108
+ } else E.clip = "idle";
2109
+ if (sp) {
2110
+ const k = Math.min(1, sp * dt / ad);
2111
+ const nx = E.x + ax * k;
2112
+ const nz = E.z + az * k;
2113
+ if (!grid.solidAt(nx, nz, N.bodyHeight)) {
2114
+ E.x = nx;
2115
+ E.z = nz;
2116
+ } else E.timer = 0;
2117
+ let dy = Math.atan2(ax, az) - E.yaw;
2118
+ dy -= Math.round(dy / 6.2832) * 6.2832;
2119
+ E.yaw += dy * Math.min(1, dt * N.turn);
2120
+ }
2121
+ }
2122
+ };
2123
+ return {
2124
+ list,
2125
+ add(spec) {
2126
+ const E = {
2127
+ index: list.length,
2128
+ spec,
2129
+ x: spec.x,
2130
+ z: spec.z,
2131
+ yaw: R() * 6.28,
2132
+ homeX: spec.x,
2133
+ homeZ: spec.z,
2134
+ range: spec.range ?? N.range,
2135
+ clip: "idle",
2136
+ timer: R() * 4,
2137
+ targetX: spec.x,
2138
+ targetZ: spec.z,
2139
+ fear: spec.fear ?? N.fear,
2140
+ fly: 0,
2141
+ cooldown: 0,
2142
+ walkSpeed: spec.walkHz * spec.size * N.walkFactor + N.walkBase,
2143
+ runSpeed: spec.runHz * spec.size * N.runFactor + N.runBase,
2144
+ companion: !!spec.companion,
2145
+ dead: !!spec.dead,
2146
+ near: void 0
2147
+ };
2148
+ list.push(E);
2149
+ return E;
2150
+ },
2151
+ free,
2152
+ update,
2153
+ get time() {
2154
+ return time;
2155
+ },
2156
+ setCompanion(index, on) {
2157
+ const E = list[index];
2158
+ if (!E) return;
2159
+ E.companion = on;
2160
+ E.homeX = E.x;
2161
+ E.homeZ = E.z;
2162
+ },
2163
+ on(event, listener) {
2164
+ let set = listeners.get(event);
2165
+ if (!set) listeners.set(event, set = /* @__PURE__ */ new Set());
2166
+ set.add(listener);
2167
+ return () => void set.delete(listener);
2168
+ }
2169
+ };
2170
+ }
2171
+
2172
+ // src/behaviours/flock.ts
2173
+ var carrionCenter = (corpses, fallback) => {
2174
+ let ux = 0;
2175
+ let uz = 0;
2176
+ let un = 0;
2177
+ for (const c of corpses) {
2178
+ ux += c.x;
2179
+ uz += c.z;
2180
+ un++;
2181
+ }
2182
+ if (un >= 3) return [ux / un, uz / un];
2183
+ return fallback ? [fallback[0], fallback[1]] : [ux, uz];
2184
+ };
2185
+ function createFlock(options) {
2186
+ const birds = Array.from({ length: options.count }, () => ({ x: 0, y: 0, z: 0, yaw: 0, roll: 0, wing: 0, visible: true }));
2187
+ const within = options.visibleWithin ?? (options.mode === "carrion" ? 260 : Infinity);
2188
+ return {
2189
+ birds,
2190
+ update(t, dt, [cx, cz], view) {
2191
+ birds.forEach((U, i) => {
2192
+ if (options.mode === "carrion") {
2193
+ const a = t * (0.18 + i * 0.04) + i * 2.1;
2194
+ const r = 7 + i * 2;
2195
+ U.x = cx + Math.cos(a) * r;
2196
+ U.y = 16 + i * 2.5;
2197
+ U.z = cz + Math.sin(a) * r;
2198
+ U.yaw = -a;
2199
+ U.roll = 0.35;
2200
+ U.visible = !view || Math.hypot(cx - view[0], cz - view[1]) < within;
2201
+ if (U.visible) U.wing += dt * 1.2 * 2 * Math.PI;
2202
+ } else {
2203
+ const a = t * 0.3 + i * 0.4;
2204
+ const r = 20 + i % 7 * 4;
2205
+ U.x = cx + Math.cos(a) * r;
2206
+ U.y = 25 + Math.sin(t + i) * 2;
2207
+ U.z = cz + Math.sin(a) * r;
2208
+ U.yaw = -a;
2209
+ U.roll = 0;
2210
+ U.visible = !view || Math.hypot(cx - view[0], cz - view[1]) < within;
2211
+ }
2212
+ });
2213
+ }
2214
+ };
2215
+ }
2216
+
2217
+ // src/behaviours/infection.ts
2218
+ function bindInfection(crowd, model, options) {
2219
+ const tracked = /* @__PURE__ */ new Map();
2220
+ const band = options.giveBackBand ?? 3;
2221
+ const offKill = crowd.on("kill", ({ agent }) => {
2222
+ const entry = tracked.get(agent);
2223
+ if (!entry) return;
2224
+ model.giveBack(entry.region, [{ state: "removed", seed: entry.seed }]);
2225
+ tracked.delete(agent);
2226
+ });
2227
+ const offRemove = crowd.on("remove", ({ agent, moved }) => {
2228
+ tracked.delete(agent);
2229
+ if (moved < 0) return;
2230
+ const entry = tracked.get(moved);
2231
+ if (!entry) return;
2232
+ tracked.delete(moved);
2233
+ tracked.set(agent, entry);
2234
+ });
2235
+ const offBite = options.civilians?.on("bitten", ({ id }) => {
2236
+ const n = options.civilians?.get(id);
2237
+ const region = n && options.regionOf?.(n.x, n.z);
2238
+ if (region) model.infect(region, 1, 0);
2239
+ });
2240
+ return {
2241
+ materialize(region, n) {
2242
+ const batch = model.materialize(region, n);
2243
+ const born = [];
2244
+ const back = [];
2245
+ batch.forEach((agent, k) => {
2246
+ const at = options.place(region, agent.seed, k);
2247
+ const i = at ? crowd.spawn(at[0], at[1], options.typeFor?.(agent, region) ?? -1) : -1;
2248
+ if (i < 0) {
2249
+ back.push(agent);
2250
+ return;
2251
+ }
2252
+ tracked.set(i, { region, state: agent.state, seed: agent.seed });
2253
+ born.push(i);
2254
+ });
2255
+ if (back.length) model.giveBack(region, back);
2256
+ return born;
2257
+ },
2258
+ giveBack(region, opts = {}) {
2259
+ const byRegion = /* @__PURE__ */ new Map();
2260
+ const indices = [...tracked.keys()].sort((a, b) => b - a);
2261
+ for (const i of indices) {
2262
+ const entry = tracked.get(i);
2263
+ if (!entry || region !== void 0 && entry.region !== region) continue;
2264
+ if (!opts.all && crowd.store.band[i] < band) continue;
2265
+ let list = byRegion.get(entry.region);
2266
+ if (!list) byRegion.set(entry.region, list = []);
2267
+ list.push({ state: entry.state, seed: entry.seed });
2268
+ tracked.delete(i);
2269
+ crowd.remove(i);
2270
+ }
2271
+ let count = 0;
2272
+ for (const [r, list] of byRegion) {
2273
+ model.giveBack(r, list);
2274
+ count += list.length;
2275
+ }
2276
+ return count;
2277
+ },
2278
+ regionOf: (agent) => tracked.get(agent)?.region ?? null,
2279
+ unbind() {
2280
+ offKill();
2281
+ offRemove();
2282
+ offBite?.();
2283
+ tracked.clear();
2284
+ }
2285
+ };
2286
+ }
2287
+ export {
2288
+ AGENT_FIELDS,
2289
+ CELL,
2290
+ CIVILIAN_DEFAULTS,
2291
+ CROWD_DEFAULTS,
2292
+ DOOR_DEFAULTS,
2293
+ HERD_DEFAULTS,
2294
+ HERD_ROLES,
2295
+ MISSING,
2296
+ STATE,
2297
+ bindInfection,
2298
+ carrionCenter,
2299
+ createCivilians,
2300
+ createCrowd,
2301
+ createDoors,
2302
+ createFlock,
2303
+ createHerd,
2304
+ createNavGrid,
2305
+ createRng,
2306
+ createStore
2307
+ };
2308
+ //# sourceMappingURL=index.js.map