@adatechnology/game-characters 0.0.0-stage → 0.1.1

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,2421 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ // src/data/body.json
8
+ var body_default = {
9
+ guide: {
10
+ heightMetres: 1.8,
11
+ shoulderWidthMetres: 0.61,
12
+ shoulderWidthWomanMetres: 0.57,
13
+ shoulderHeightMetres: 1.475,
14
+ headToHeight: 0.1667
15
+ },
16
+ teamGame: {
17
+ source: "G 65336 J (shoulders +-0.24 m)",
18
+ joints: {
19
+ quadril: [
20
+ 0,
21
+ 0.95,
22
+ 0
23
+ ],
24
+ pescoco: [
25
+ 0,
26
+ 1.52,
27
+ 0
28
+ ],
29
+ ombroE: [
30
+ -0.24,
31
+ 1.44,
32
+ 0
33
+ ],
34
+ ombroD: [
35
+ 0.24,
36
+ 1.44,
37
+ 0
38
+ ],
39
+ cotoveloE: [
40
+ -0.27,
41
+ 1.13,
42
+ 0
43
+ ],
44
+ cotoveloD: [
45
+ 0.27,
46
+ 1.13,
47
+ 0
48
+ ],
49
+ coxaE: [
50
+ -0.1,
51
+ 0.92,
52
+ 0
53
+ ],
54
+ coxaD: [
55
+ 0.1,
56
+ 0.92,
57
+ 0
58
+ ],
59
+ joelhoE: [
60
+ -0.1,
61
+ 0.5,
62
+ 0
63
+ ],
64
+ joelhoD: [
65
+ 0.1,
66
+ 0.5,
67
+ 0
68
+ ]
69
+ },
70
+ heightMetres: 1.8
71
+ },
72
+ lodDistancesMetres: [
73
+ 18,
74
+ 55
75
+ ],
76
+ triangleBudget: {
77
+ lod0: 2500,
78
+ lod1: 800,
79
+ lod2: 250
80
+ }
81
+ };
82
+
83
+ // src/body/body-spec.ts
84
+ var JOINT_IDS = ["hip", "spine", "neck", "head", "jaw", "shoulderL", "shoulderR", "elbowL", "elbowR", "wristL", "wristR", "hipL", "hipR", "kneeL", "kneeR", "ankleL", "ankleR"];
85
+ var SEGMENT_RADIUS = {
86
+ head: 0.1,
87
+ jaw: 0.05,
88
+ neck: 0.06,
89
+ torso: 0.17,
90
+ pelvis: 0.16,
91
+ upperArm: 0.085,
92
+ forearm: 0.065,
93
+ hand: 0.05,
94
+ thigh: 0.1,
95
+ shin: 0.08,
96
+ foot: 0.05
97
+ };
98
+ var { guide } = body_default;
99
+ var shoulderX = guide.shoulderWidthMetres / 2;
100
+ var shoulderY = guide.shoulderHeightMetres;
101
+ var GUIDE = {
102
+ id: "guide",
103
+ heightMetres: guide.heightMetres,
104
+ joints: {
105
+ hip: [0, 0.92, 0],
106
+ spine: [0, 1.05, 0],
107
+ neck: [0, 1.56, 0],
108
+ head: [0, 1.64, 0],
109
+ jaw: [0, 1.66, 0.06],
110
+ shoulderL: [shoulderX, shoulderY, 0],
111
+ shoulderR: [-shoulderX, shoulderY, 0],
112
+ elbowL: [shoulderX, shoulderY - 0.31, 0],
113
+ elbowR: [-shoulderX, shoulderY - 0.31, 0],
114
+ wristL: [shoulderX, shoulderY - 0.61, 0],
115
+ wristR: [-shoulderX, shoulderY - 0.61, 0],
116
+ hipL: [0.105, 0.9, 0],
117
+ hipR: [-0.105, 0.9, 0],
118
+ kneeL: [0.105, 0.48, 0],
119
+ kneeR: [-0.105, 0.48, 0],
120
+ ankleL: [0.105, 0.1, 0],
121
+ ankleR: [-0.105, 0.1, 0]
122
+ },
123
+ segmentRadius: SEGMENT_RADIUS
124
+ };
125
+ var J = body_default.teamGame.joints;
126
+ var below = (point, drop) => [point[0], point[1] - drop, point[2]];
127
+ var TEAM_GAME = {
128
+ id: "team-game",
129
+ heightMetres: body_default.teamGame.heightMetres,
130
+ joints: {
131
+ hip: J.quadril,
132
+ spine: J.quadril,
133
+ neck: J.pescoco,
134
+ head: J.pescoco,
135
+ jaw: [0, J.pescoco[1] + 0.08, 0.06],
136
+ shoulderL: J.ombroE,
137
+ shoulderR: J.ombroD,
138
+ elbowL: J.cotoveloE,
139
+ elbowR: J.cotoveloD,
140
+ wristL: below(J.cotoveloE, 0.3),
141
+ wristR: below(J.cotoveloD, 0.3),
142
+ hipL: J.coxaE,
143
+ hipR: J.coxaD,
144
+ kneeL: J.joelhoE,
145
+ kneeR: J.joelhoD,
146
+ ankleL: below(J.joelhoE, 0.42),
147
+ ankleR: below(J.joelhoD, 0.42)
148
+ },
149
+ segmentRadius: SEGMENT_RADIUS
150
+ };
151
+ var createBodySpec = (patch = {}, base = GUIDE) => ({
152
+ id: patch.id ?? base.id,
153
+ heightMetres: patch.heightMetres ?? base.heightMetres,
154
+ joints: { ...base.joints, ...patch.joints },
155
+ segmentRadius: { ...base.segmentRadius, ...patch.segmentRadius }
156
+ });
157
+
158
+ // src/corpses.ts
159
+ var createCorpseLedger = (options) => {
160
+ const entries = [];
161
+ return {
162
+ entries,
163
+ get count() {
164
+ return entries.length;
165
+ },
166
+ add(data) {
167
+ if (entries.length < options.max) {
168
+ entries.push({ age: 0, data });
169
+ return void 0;
170
+ }
171
+ let oldest = 0;
172
+ for (let index = 1; index < entries.length; index++) if (entries[index].age > entries[oldest].age) oldest = index;
173
+ const replaced = entries[oldest].data;
174
+ entries[oldest] = { age: 0, data };
175
+ return replaced;
176
+ },
177
+ update(dt) {
178
+ const removed = [];
179
+ for (let index = 0; index < entries.length; index++) {
180
+ const entry = entries[index];
181
+ entry.age += dt;
182
+ if (entry.age <= options.seconds) continue;
183
+ removed.push(entry.data);
184
+ entries[index] = entries[entries.length - 1];
185
+ entries.pop();
186
+ index--;
187
+ }
188
+ return removed;
189
+ },
190
+ clear() {
191
+ entries.length = 0;
192
+ }
193
+ };
194
+ };
195
+
196
+ // src/body/body-state.ts
197
+ import { MISSING_BITS } from "@adatechnology/game-contracts";
198
+
199
+ // src/presets/team-game.json
200
+ var team_game_default = {
201
+ id: "team-game",
202
+ segments: {
203
+ head: 40,
204
+ arm: 60,
205
+ leg: 80
206
+ },
207
+ calibers: {
208
+ "7_62x51": {
209
+ gameKey: "308",
210
+ name: "7,62\xD751 (.308)",
211
+ points: 75,
212
+ penetration: 4,
213
+ effectiveRangeMetres: 600,
214
+ noiseMetres: 130,
215
+ skullRange: 1,
216
+ class: "rifle"
217
+ },
218
+ "9x19": {
219
+ gameKey: "9x19",
220
+ name: "9\xD719 mm",
221
+ points: 35,
222
+ penetration: 1.2,
223
+ effectiveRangeMetres: 35,
224
+ noiseMetres: 60,
225
+ skullRange: 1,
226
+ class: "pistol"
227
+ },
228
+ "12ga_00buck": {
229
+ gameKey: "12ga",
230
+ name: "12 ga 00",
231
+ points: 12,
232
+ pellets: 9,
233
+ penetration: 0.5,
234
+ effectiveRangeMetres: 15,
235
+ noiseMetres: 90,
236
+ skullRange: 8,
237
+ class: "pistol"
238
+ },
239
+ "7_62x39": {
240
+ gameKey: "762x39",
241
+ name: "7,62\xD739 mm",
242
+ points: 55,
243
+ penetration: 3,
244
+ effectiveRangeMetres: 250,
245
+ noiseMetres: 110,
246
+ skullRange: 1,
247
+ class: "rifle"
248
+ }
249
+ },
250
+ regionCost: {
251
+ head: 0.7,
252
+ torso: 1,
253
+ bruteTorso: 2,
254
+ limb: 0.5
255
+ },
256
+ damageKeptPerBody: 0.75,
257
+ falloff: {
258
+ fullAtRange: 1,
259
+ atTwiceRange: 0.5
260
+ },
261
+ helmetAbsorbs: "pistol",
262
+ regions: [
263
+ "head",
264
+ "torso",
265
+ "armL",
266
+ "armR",
267
+ "legL",
268
+ "legR"
269
+ ]
270
+ };
271
+
272
+ // src/body/regions.ts
273
+ var LIMB_REGIONS = ["head", "armL", "armR", "legL", "legR"];
274
+ var REGION_HIT_POINTS = {
275
+ head: team_game_default.segments.head,
276
+ armL: team_game_default.segments.arm,
277
+ armR: team_game_default.segments.arm,
278
+ legL: team_game_default.segments.leg,
279
+ legR: team_game_default.segments.leg
280
+ };
281
+ var REGION_SEGMENTS = {
282
+ head: ["head", "jaw", "neck"],
283
+ torso: ["torso", "pelvis"],
284
+ armL: ["upperArmL", "forearmL", "handL"],
285
+ armR: ["upperArmR", "forearmR", "handR"],
286
+ legL: ["thighL", "shinL", "footL"],
287
+ legR: ["thighR", "shinR", "footR"]
288
+ };
289
+ var BODY_REGIONS = ["head", "torso", "armL", "armR", "legL", "legR"];
290
+ var isBodyRegion = (value) => BODY_REGIONS.includes(value);
291
+
292
+ // src/data/segments.json
293
+ var segments_default = {
294
+ segments: [
295
+ {
296
+ id: "head",
297
+ region: "head",
298
+ hitPoints: 40,
299
+ multiplier: 3,
300
+ bleed: 40
301
+ },
302
+ {
303
+ id: "jaw",
304
+ region: "head",
305
+ hitPoints: 12,
306
+ multiplier: 2,
307
+ bleed: 8
308
+ },
309
+ {
310
+ id: "neck",
311
+ region: "head",
312
+ hitPoints: 25,
313
+ multiplier: 2,
314
+ bleed: 46
315
+ },
316
+ {
317
+ id: "torso",
318
+ region: "torso",
319
+ hitPoints: 150,
320
+ multiplier: 1,
321
+ bleed: 10
322
+ },
323
+ {
324
+ id: "pelvis",
325
+ region: "torso",
326
+ hitPoints: 90,
327
+ multiplier: 1,
328
+ bleed: 12
329
+ },
330
+ {
331
+ id: "upperArm",
332
+ region: "arm",
333
+ hitPoints: 45,
334
+ multiplier: 0.6,
335
+ bleed: 15
336
+ },
337
+ {
338
+ id: "forearm",
339
+ region: "arm",
340
+ hitPoints: 30,
341
+ multiplier: 0.6,
342
+ bleed: 8
343
+ },
344
+ {
345
+ id: "hand",
346
+ region: "arm",
347
+ hitPoints: 12,
348
+ multiplier: 0.6,
349
+ bleed: 3
350
+ },
351
+ {
352
+ id: "thigh",
353
+ region: "leg",
354
+ hitPoints: 55,
355
+ multiplier: 0.7,
356
+ bleed: 19
357
+ },
358
+ {
359
+ id: "shin",
360
+ region: "leg",
361
+ hitPoints: 40,
362
+ multiplier: 0.7,
363
+ bleed: 9
364
+ },
365
+ {
366
+ id: "foot",
367
+ region: "leg",
368
+ hitPoints: 15,
369
+ multiplier: 0.7,
370
+ bleed: 3
371
+ }
372
+ ],
373
+ regions: {
374
+ head: {
375
+ hitPoints: 40,
376
+ segmentSum: 77,
377
+ scale: 0.5195
378
+ },
379
+ arm: {
380
+ hitPoints: 60,
381
+ segmentSum: 87,
382
+ scale: 0.6897
383
+ },
384
+ leg: {
385
+ hitPoints: 80,
386
+ segmentSum: 110,
387
+ scale: 0.7273
388
+ }
389
+ },
390
+ arsenalWeapons: {
391
+ pistola: {
392
+ n: "Pistola 9\xD719",
393
+ dmg: 35,
394
+ pel: 1
395
+ },
396
+ fuzil: {
397
+ n: "Fuzil 7,62\xD739",
398
+ dmg: 55,
399
+ pel: 1
400
+ },
401
+ escopeta: {
402
+ n: "Escopeta 12 ga",
403
+ dmg: 12,
404
+ pel: 9
405
+ },
406
+ cinquenta: {
407
+ n: ".50 BMG",
408
+ dmg: 180,
409
+ pel: 1
410
+ },
411
+ granada: {
412
+ n: "Granada",
413
+ dmg: 300,
414
+ pel: 0
415
+ }
416
+ },
417
+ elenco: {
418
+ segments: {
419
+ head: {
420
+ hp: 40,
421
+ radius: 0.07
422
+ },
423
+ torso: {
424
+ hp: 120
425
+ },
426
+ pelvis: {
427
+ hp: 90
428
+ },
429
+ upperArm: {
430
+ hp: 60,
431
+ radius: 0.085
432
+ },
433
+ forearm: {
434
+ hp: 45,
435
+ radius: 0.065
436
+ },
437
+ hand: {
438
+ hp: 25,
439
+ radius: 0.05
440
+ },
441
+ thigh: {
442
+ hp: 80,
443
+ radius: 0.1
444
+ },
445
+ shin: {
446
+ hp: 55,
447
+ radius: 0.08
448
+ }
449
+ },
450
+ below: {
451
+ upperArm: [
452
+ "forearm",
453
+ "hand"
454
+ ],
455
+ forearm: [
456
+ "hand"
457
+ ],
458
+ thigh: [
459
+ "shin"
460
+ ]
461
+ },
462
+ falls: {
463
+ upperArm: "o bra\xE7o inteiro",
464
+ forearm: "o antebra\xE7o com a m\xE3o",
465
+ hand: "a m\xE3o",
466
+ thigh: "a perna inteira",
467
+ shin: "a canela com o p\xE9",
468
+ head: "a cabe\xE7a"
469
+ },
470
+ cutWeapons: [
471
+ {
472
+ id: "faca",
473
+ n: "Faca",
474
+ dmg: 22,
475
+ cut: 0.7
476
+ },
477
+ {
478
+ id: "facao",
479
+ n: "Fac\xE3o",
480
+ dmg: 55,
481
+ cut: 1
482
+ },
483
+ {
484
+ id: "machado",
485
+ n: "Machado",
486
+ dmg: 85,
487
+ cut: 1
488
+ },
489
+ {
490
+ id: "p9",
491
+ n: "Pistola 9 mm",
492
+ dmg: 30,
493
+ cut: 0.15
494
+ },
495
+ {
496
+ id: "e12",
497
+ n: "Espingarda 12",
498
+ dmg: 95,
499
+ cut: 0.75
500
+ },
501
+ {
502
+ id: "f762",
503
+ n: "Fuzil 7,62",
504
+ dmg: 65,
505
+ cut: 0.45
506
+ }
507
+ ]
508
+ },
509
+ severity: {
510
+ severAtCut: 0.5,
511
+ severAtDamageFraction: 0.9,
512
+ destroyAtDamageMultiple: 2,
513
+ limpAtHpFraction: 0.5,
514
+ splitAtDamage: 150,
515
+ headKillDamage: 20,
516
+ jawChance: 0.3,
517
+ jawDamage: 25,
518
+ headBitDamage: 60,
519
+ headBitShotgunMetres: 5
520
+ },
521
+ defaultCut: {
522
+ slash: 0.85,
523
+ blunt: 0,
524
+ explosive: 1,
525
+ ballistic: {
526
+ pistol: 0.15,
527
+ buckshot: 0.75,
528
+ rifle: 0.45
529
+ }
530
+ },
531
+ deathRule: {
532
+ default: "vitality-or-head",
533
+ options: [
534
+ "vitality-or-head",
535
+ "head-only"
536
+ ]
537
+ }
538
+ };
539
+
540
+ // src/body/segment-tree.ts
541
+ var limb = (kind, side, joint, parent, slots) => {
542
+ const isArm2 = kind === "upperArm" || kind === "forearm" || kind === "hand";
543
+ const region = isArm2 ? side === "L" ? "armL" : "armR" : side === "L" ? "legL" : "legR";
544
+ return { id: `${kind}${side}`, kind, side, joint, region, slots, ...parent ? { parent } : {} };
545
+ };
546
+ var sideOf = (side) => [
547
+ limb("upperArm", side, `shoulder${side}`, "torso", ["arms"]),
548
+ limb("forearm", side, `elbow${side}`, `upperArm${side}`, ["arms"]),
549
+ limb("hand", side, `wrist${side}`, `forearm${side}`, ["hands"]),
550
+ limb("thigh", side, `hip${side}`, "pelvis", ["legs"]),
551
+ limb("shin", side, `knee${side}`, `thigh${side}`, ["legs"]),
552
+ limb("foot", side, `ankle${side}`, `shin${side}`, ["feet"])
553
+ ];
554
+ var SEGMENT_TREE = [
555
+ { id: "pelvis", kind: "pelvis", joint: "hip", region: "torso", slots: ["torso"] },
556
+ { id: "torso", kind: "torso", joint: "spine", parent: "pelvis", region: "torso", slots: ["torso"] },
557
+ { id: "neck", kind: "neck", joint: "neck", parent: "torso", region: "head", slots: [] },
558
+ { id: "head", kind: "head", joint: "head", parent: "neck", region: "head", slots: ["head", "eyes"] },
559
+ { id: "jaw", kind: "jaw", joint: "jaw", parent: "head", region: "head", slots: [] },
560
+ ...sideOf("L"),
561
+ ...sideOf("R")
562
+ ];
563
+ var SEGMENT_IDS = SEGMENT_TREE.map((node) => node.id);
564
+ var NODES = new Map(SEGMENT_TREE.map((node) => [node.id, node]));
565
+ var segmentNode = (id) => {
566
+ const node = NODES.get(id);
567
+ if (!node) throw new Error(`game-characters: unknown segment "${id}"`);
568
+ return node;
569
+ };
570
+ var isSegmentId = (value) => NODES.has(value);
571
+ var segmentsBelow = (id) => {
572
+ const out = [];
573
+ const visit = (parent) => {
574
+ for (const node of SEGMENT_TREE) {
575
+ if (node.parent !== parent) continue;
576
+ out.push(node.id);
577
+ visit(node.id);
578
+ }
579
+ };
580
+ visit(id);
581
+ return out;
582
+ };
583
+ var SEGMENT_NUMBERS = Object.fromEntries(
584
+ segments_default.segments.map((entry) => [entry.id, { hitPoints: entry.hitPoints, multiplier: entry.multiplier, bleed: entry.bleed }])
585
+ );
586
+
587
+ // src/body/body-state.ts
588
+ var createBodyState = ({ vitality, detail, crawling, hitPointsOf }) => ({
589
+ segments: SEGMENT_TREE.map((node) => {
590
+ const max = hitPointsOf(node.kind);
591
+ return { id: node.id, kind: node.kind, region: node.region, hp: max, max, status: "ok", attached: true, ...node.side ? { side: node.side } : {} };
592
+ }),
593
+ regions: { ...REGION_HIT_POINTS },
594
+ vitality,
595
+ maxVitality: vitality,
596
+ alive: true,
597
+ missing: 0,
598
+ limping: [],
599
+ crawl: crawling ?? false,
600
+ stunSeconds: 0,
601
+ pieces: [],
602
+ consumed: /* @__PURE__ */ new Set(),
603
+ detail
604
+ });
605
+ var segmentOf = (body, id) => {
606
+ const segment = body.segments.find((candidate) => candidate.id === id);
607
+ if (!segment) throw new Error(`game-characters: unknown segment "${id}"`);
608
+ return segment;
609
+ };
610
+ var LIMB_MASK = [
611
+ { ids: ["upperArmL", "forearmL"], lost: MISSING_BITS.armL, broken: MISSING_BITS.armLBroken },
612
+ { ids: ["upperArmR", "forearmR"], lost: MISSING_BITS.armR, broken: MISSING_BITS.armRBroken },
613
+ { ids: ["thighL", "shinL"], lost: MISSING_BITS.legL, broken: MISSING_BITS.legLBroken },
614
+ { ids: ["thighR", "shinR"], lost: MISSING_BITS.legR, broken: MISSING_BITS.legRBroken }
615
+ ];
616
+ var missingFromSegments = (body) => {
617
+ let mask = 0;
618
+ if (!segmentOf(body, "head").attached) mask |= MISSING_BITS.head;
619
+ else if (!segmentOf(body, "jaw").attached) mask |= MISSING_BITS.jaw;
620
+ for (const limb2 of LIMB_MASK) {
621
+ const parts = limb2.ids.map((id) => segmentOf(body, id));
622
+ if (parts.some((part) => !part.attached)) mask |= limb2.lost;
623
+ else if (parts.some((part) => part.status === "broken")) mask |= limb2.broken;
624
+ }
625
+ return mask;
626
+ };
627
+ var regionsFromSegments = (body) => {
628
+ for (const region of LIMB_REGIONS) {
629
+ const parts = REGION_SEGMENTS[region].map((id) => segmentOf(body, id));
630
+ const left = parts.reduce((sum, part) => sum + (part.attached ? Math.max(0, part.hp) : 0), 0);
631
+ const total = parts.reduce((sum, part) => sum + part.max, 0);
632
+ body.regions[region] = REGION_HIT_POINTS[region] * left / total;
633
+ }
634
+ body.missing |= missingFromSegments(body);
635
+ };
636
+ var segmentsFromRegions = (body) => {
637
+ for (const region of LIMB_REGIONS) {
638
+ const share = Math.max(0, body.regions[region]) / REGION_HIT_POINTS[region];
639
+ for (const id of REGION_SEGMENTS[region]) {
640
+ const part = segmentOf(body, id);
641
+ if (part.attached) part.hp = Math.min(part.hp, part.max * share);
642
+ }
643
+ }
644
+ };
645
+
646
+ // src/body/describe.ts
647
+ var BODY_MATERIAL = "flesh";
648
+ var MASS_SHARE = {
649
+ head: 0.07,
650
+ jaw: 0.01,
651
+ neck: 0.02,
652
+ torso: 0.34,
653
+ pelvis: 0.14,
654
+ upperArm: 0.028,
655
+ forearm: 0.016,
656
+ hand: 6e-3,
657
+ thigh: 0.1,
658
+ shin: 0.045,
659
+ foot: 0.015
660
+ };
661
+ var ADULT_MASS_KG = 70;
662
+ var LEAF_KINDS = /* @__PURE__ */ new Set(["head", "jaw", "hand", "foot"]);
663
+ var firstChild = (node) => SEGMENT_TREE.find((candidate) => candidate.parent === node.id);
664
+ var cylinderBetween = (from, to, radius) => {
665
+ const axis = [to[0] - from[0], to[1] - from[1], to[2] - from[2]];
666
+ const length = Math.max(1e-3, Math.hypot(axis[0], axis[1], axis[2]));
667
+ const [x, y, z] = [axis[0] / length, axis[1] / length, axis[2] / length];
668
+ return {
669
+ shape: { kind: "cylinder", radius, height: length },
670
+ position: [(from[0] + to[0]) / 2, (from[1] + to[1]) / 2, (from[2] + to[2]) / 2],
671
+ rotation: [Math.atan2(z, y), 0, -Math.asin(Math.max(-1, Math.min(1, x)))]
672
+ };
673
+ };
674
+ var partOf = (node, rig, body, massKg) => {
675
+ const joint = rig.joints[node.joint];
676
+ const radius = body.segmentRadius[node.kind] * (rig.height / body.heightMetres);
677
+ const mass = massKg * MASS_SHARE[node.kind];
678
+ const child = firstChild(node);
679
+ if (LEAF_KINDS.has(node.kind) || !child) {
680
+ const lift = node.kind === "head" ? radius : node.kind === "jaw" ? 0 : -radius;
681
+ return { id: node.id, material: BODY_MATERIAL, shape: { kind: "sphere", radius }, position: [joint[0], joint[1] + lift, joint[2]], mass, tags: ["body", node.kind] };
682
+ }
683
+ const placed = cylinderBetween(joint, rig.joints[child.joint], radius);
684
+ return { id: node.id, material: BODY_MATERIAL, ...placed, mass, tags: ["body", node.kind] };
685
+ };
686
+ var describeBody = ({ rig, body, build, name }) => {
687
+ const massKg = ADULT_MASS_KG * (rig.height / body.heightMetres) ** 3 * build;
688
+ const parts = SEGMENT_TREE.map((node) => partOf(node, rig, body, massKg));
689
+ const joints = SEGMENT_TREE.filter((node) => node.parent).map((node) => ({ id: `${node.parent}~${node.id}`, a: node.parent, b: node.id, kind: "fused", tools: [] }));
690
+ return { ...name ? { name } : {}, parts, joints, tags: ["character"] };
691
+ };
692
+
693
+ // src/body/object-state.ts
694
+ var PART_STATUS = { ok: "intact", wounded: "damaged", broken: "damaged", severed: "collapsed", destroyed: "broken" };
695
+ var BODY_TEMPERATURE_C = 37;
696
+ var toObjectState = ({ id, name, position, rotation, body, spec }) => {
697
+ const groups = [body.segments.filter((segment) => segment.attached).map((segment) => segment.id)];
698
+ for (const piece of body.pieces) {
699
+ const left = piece.filter((part) => body.segments.find((segment) => segment.id === part)?.status !== "destroyed");
700
+ if (left.length > 0) groups.push(left);
701
+ }
702
+ const groupOf = (part) => groups.findIndex((group) => group.includes(part));
703
+ const parts = body.segments.map((segment) => {
704
+ const mass = spec.parts.find((part) => part.id === segment.id)?.mass ?? 0;
705
+ return {
706
+ id: segment.id,
707
+ material: "flesh",
708
+ status: PART_STATUS[segment.status],
709
+ hp: Math.max(0, segment.hp),
710
+ maxHp: segment.max,
711
+ mass,
712
+ temperature: BODY_TEMPERATURE_C,
713
+ burning: false,
714
+ fuel: 0,
715
+ char: 0,
716
+ melt: 0,
717
+ dents: [],
718
+ holes: [],
719
+ cracks: [],
720
+ group: groupOf(segment.id)
721
+ };
722
+ });
723
+ const joints = (spec.joints ?? []).map((joint) => {
724
+ const child = body.segments.find((segment) => segment.id === joint.b);
725
+ const parent = body.segments.find((segment) => segment.id === joint.a);
726
+ const open = child !== void 0 && parent !== void 0 && parent.attached !== child.attached;
727
+ return { id: joint.id ?? `${joint.a}~${joint.b}`, a: joint.a, b: joint.b, kind: joint.kind, hp: open ? 0 : 1, maxHp: 1, progress: 0, open, ...open ? { openedBy: "break" } : {} };
728
+ });
729
+ return {
730
+ id,
731
+ ...name ? { name } : {},
732
+ position: [...position],
733
+ rotation: [...rotation],
734
+ parts,
735
+ joints,
736
+ groups,
737
+ destroyed: false,
738
+ body: { alive: body.alive, vitality: body.vitality, missing: body.detail === "segments" ? body.missing | missingFromSegments(body) : body.missing, limp: body.limping.length > 0, crawl: body.crawl }
739
+ };
740
+ };
741
+
742
+ // src/body/rig.ts
743
+ var GAME_BONES = {
744
+ torso: 0,
745
+ head: 1,
746
+ upperArmL: 2,
747
+ upperArmR: 3,
748
+ thighL: 4,
749
+ thighR: 5,
750
+ eyes: 6,
751
+ forearmL: 7,
752
+ forearmR: 8,
753
+ shinL: 9,
754
+ shinR: 10,
755
+ jaw: 11
756
+ };
757
+ var JOINT_PARENTS = {
758
+ hip: void 0,
759
+ spine: "hip",
760
+ neck: "spine",
761
+ head: "neck",
762
+ jaw: "head",
763
+ shoulderL: "spine",
764
+ shoulderR: "spine",
765
+ elbowL: "shoulderL",
766
+ elbowR: "shoulderR",
767
+ wristL: "elbowL",
768
+ wristR: "elbowR",
769
+ hipL: "hip",
770
+ hipR: "hip",
771
+ kneeL: "hipL",
772
+ kneeR: "hipR",
773
+ ankleL: "kneeL",
774
+ ankleR: "kneeR"
775
+ };
776
+ var createRig = ({ body, height, build }) => {
777
+ const scale = height / body.heightMetres;
778
+ const joints = Object.fromEntries(
779
+ JOINT_IDS.map((id) => {
780
+ const [x, y, z] = body.joints[id];
781
+ return [id, [x * scale * build, y * scale, z * scale]];
782
+ })
783
+ );
784
+ return { joints, parents: JOINT_PARENTS, gameBones: GAME_BONES, height };
785
+ };
786
+
787
+ // src/character.ts
788
+ var DEFAULT_VITALITY = 100;
789
+ var DEFAULT_STUN_SECONDS = 0.45;
790
+ var NO_MULTIPLIERS = { head: 1, torso: 1, arms: 1, legs: 1 };
791
+ var resolveSpec = (spec, body, counter) => ({
792
+ ...spec,
793
+ id: spec.id ?? `${spec.kind}_${counter}`,
794
+ height: spec.height ?? body.heightMetres,
795
+ build: spec.build ?? 1,
796
+ vitality: spec.vitality ?? DEFAULT_VITALITY,
797
+ multipliers: { ...NO_MULTIPLIERS, ...spec.multipliers },
798
+ limbFactor: spec.limbFactor ?? 1,
799
+ bloodFactor: spec.bloodFactor ?? 1,
800
+ bodyCount: spec.bodyCount ?? 1,
801
+ stunSeconds: spec.stunSeconds ?? DEFAULT_STUN_SECONDS,
802
+ protection: spec.protection ?? [],
803
+ detail: spec.detail ?? "segments"
804
+ });
805
+ var createCharacterCore = ({ spec, bodySpec, hitPointsOf }) => {
806
+ const rig = createRig({ body: bodySpec, height: spec.height, build: spec.build });
807
+ const parts = describeBody({ rig, body: bodySpec, build: spec.build, name: spec.kind });
808
+ const fresh = () => createBodyState({ vitality: spec.vitality, detail: spec.detail, crawling: spec.crawling ?? false, hitPointsOf: hitPointsOf ?? ((kind) => SEGMENT_NUMBERS[kind].hitPoints) });
809
+ const body = fresh();
810
+ return { spec, rig, body, position: [0, 0, 0], rotation: [0, 0, 0, 1], describe: () => parts, resetBody: () => void Object.assign(body, fresh()) };
811
+ };
812
+ var characterView = (core) => ({
813
+ id: core.spec.id,
814
+ spec: core.spec,
815
+ rig: core.rig,
816
+ get segments() {
817
+ return core.body.segments;
818
+ },
819
+ get detail() {
820
+ return core.body.detail;
821
+ },
822
+ regionHitPoints: () => ({ ...core.body.regions }),
823
+ setDetail(detail) {
824
+ if (detail === core.body.detail) return;
825
+ if (detail === "regions") regionsFromSegments(core.body);
826
+ else segmentsFromRegions(core.body);
827
+ core.body.detail = detail;
828
+ },
829
+ setTransform(position, rotation) {
830
+ core.position = [...position];
831
+ if (rotation) core.rotation = [...rotation];
832
+ },
833
+ describe: core.describe,
834
+ state: () => toObjectState({ id: core.spec.id, name: core.spec.kind, position: core.position, rotation: core.rotation, body: core.body, spec: core.describe() }),
835
+ reset: core.resetBody
836
+ });
837
+
838
+ // src/body/transform.ts
839
+ var rotate = (q, v) => {
840
+ const [x, y, z, w] = q;
841
+ const tx = 2 * (y * v[2] - z * v[1]);
842
+ const ty = 2 * (z * v[0] - x * v[2]);
843
+ const tz = 2 * (x * v[1] - y * v[0]);
844
+ return [v[0] + w * tx + (y * tz - z * ty), v[1] + w * ty + (z * tx - x * tz), v[2] + w * tz + (x * ty - y * tx)];
845
+ };
846
+ var conjugate = (q) => [-q[0], -q[1], -q[2], q[3]];
847
+ var toWorld = ({ position, rotation }, local) => {
848
+ const turned = rotate(rotation, local);
849
+ return [turned[0] + position[0], turned[1] + position[1], turned[2] + position[2]];
850
+ };
851
+ var toLocal = ({ position, rotation }, world) => rotate(conjugate(rotation), [world[0] - position[0], world[1] - position[1], world[2] - position[2]]);
852
+ var distance = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
853
+
854
+ // src/damage/bleeding.ts
855
+ var BLOOD_LITERS_PER_DROP = 27e-5;
856
+ var bleedRateAt = (bleeder) => bleeder.age >= bleeder.duration ? 0 : bleeder.rate * (1 - bleeder.age / bleeder.duration);
857
+ var stepBleeding = ({ bleeders, dt, emit, pointOf }) => {
858
+ for (const bleeder of bleeders) {
859
+ if (bleeder.age >= bleeder.duration) continue;
860
+ bleeder.age += dt;
861
+ const second = Math.floor(bleeder.age);
862
+ if (second <= bleeder.reported) continue;
863
+ bleeder.reported = second;
864
+ const rate = bleedRateAt(bleeder);
865
+ if (rate > 0) emit({ type: "leak", part: bleeder.part, fluid: "blood", point: pointOf(bleeder.part), direction: [0, -1, 0], rate: rate * BLOOD_LITERS_PER_DROP, pressurized: bleeder.spurt });
866
+ }
867
+ };
868
+
869
+ // src/damage/body-ops.ts
870
+ var killAllowed = (rule, cause) => rule === "vitality-or-head" || cause === "head" || cause === "explosion";
871
+ var createBodyOps = ({ body, spec, rules, random, time, source, pointOf, bleeders }) => {
872
+ const events = [];
873
+ const emit = (event) => void events.push({ ...event, object: spec.id, time, ...source !== void 0 ? { source } : {} });
874
+ const change = (kind, part, extra) => emit({ type: "body", change: kind, ...part ? { part } : {}, ...extra });
875
+ const detachBelow = (segment, below2) => {
876
+ const fallen = segmentsBelow(segment.id).filter((id) => segmentOf(body, id).attached);
877
+ segment.attached = false;
878
+ for (const id of fallen) {
879
+ const part = segmentOf(body, id);
880
+ part.attached = false;
881
+ if (below2 === "zero") Object.assign(part, { hp: 0, status: "severed" });
882
+ }
883
+ return fallen;
884
+ };
885
+ const ops = {
886
+ body,
887
+ spec,
888
+ rules,
889
+ random,
890
+ events,
891
+ emit,
892
+ pointOf,
893
+ change,
894
+ damage: (segment, amount, damageType) => emit({ type: "damage", part: segment.id, amount, hp: Math.max(0, segment.hp), damageType }),
895
+ wound(segment) {
896
+ if (segment.status === "ok") segment.status = "wounded";
897
+ change("wound", segment.id);
898
+ },
899
+ breakBone(segment) {
900
+ segment.status = "broken";
901
+ change("broken", segment.id);
902
+ },
903
+ sever(segment, below2 = "detach") {
904
+ if (!segment.attached) return;
905
+ const fallen = detachBelow(segment, below2);
906
+ segment.status = "severed";
907
+ body.pieces.push([segment.id, ...fallen]);
908
+ const parent = segmentNode(segment.id).parent;
909
+ change("severed", segment.id, { below: fallen });
910
+ if (parent) emit({ type: "detach", joint: `${parent}~${segment.id}`, parts: [parent, segment.id], by: "break", groups: [[segment.id, ...fallen]] });
911
+ },
912
+ destroy(segment) {
913
+ if (!segment.attached) return;
914
+ const fallen = detachBelow(segment, "detach");
915
+ segment.status = "destroyed";
916
+ if (fallen.length > 0) body.pieces.push(fallen);
917
+ change("destroyed", segment.id, { below: fallen });
918
+ emit({ type: "fracture", part: segment.id, style: "tear", fragments: [], point: pointOf(segment.id) });
919
+ },
920
+ kill(cause) {
921
+ if (!body.alive || !killAllowed(rules.deathRule, cause)) return;
922
+ body.alive = false;
923
+ change("killed", void 0, { cause });
924
+ },
925
+ limp(side) {
926
+ if (body.limping.includes(side)) return;
927
+ body.limping.push(side);
928
+ change("limp");
929
+ },
930
+ crawl() {
931
+ if (body.crawl) return;
932
+ body.crawl = true;
933
+ change("crawl");
934
+ },
935
+ stun() {
936
+ body.stunSeconds = spec.stunSeconds;
937
+ change("stunned");
938
+ },
939
+ bleed({ segment, rate, duration, spurt, point, direction }) {
940
+ const scaled = rate * spec.bloodFactor;
941
+ bleeders.push({ part: segment.id, rate: scaled, duration, spurt, age: 0, reported: 0 });
942
+ emit({ type: "leak", part: segment.id, fluid: "blood", point: point ?? pointOf(segment.id), direction: direction ?? [0, -1, 0], rate: scaled * BLOOD_LITERS_PER_DROP, pressurized: spurt });
943
+ }
944
+ };
945
+ return ops;
946
+ };
947
+
948
+ // src/presets/physical.json
949
+ var physical_default = {
950
+ id: "physical",
951
+ tissuePerCentimetre: {
952
+ flesh: 0.25,
953
+ bone: 1
954
+ },
955
+ torsoPath: {
956
+ fleshCentimetres: 22,
957
+ boneCentimetres: 2
958
+ },
959
+ regionFactor: {
960
+ head: 0.7,
961
+ limb: 0.5,
962
+ torso: 1
963
+ },
964
+ bruteTorsoFactor: 2,
965
+ energyKept: "P / (P + R)",
966
+ points: {
967
+ coefficient: 3.5,
968
+ exponent: 0.37
969
+ },
970
+ tolerance: {
971
+ bodies: 0,
972
+ energyKept: 0.05
973
+ },
974
+ calibration: [
975
+ {
976
+ caliber: "9x19",
977
+ force: 7.67,
978
+ bodies: 2,
979
+ energyKeptFirstBody: 0.51,
980
+ expectedBodies: 2,
981
+ bestiaryEnergyKept: 0.5
982
+ },
983
+ {
984
+ caliber: "5_56x45",
985
+ force: 16.56,
986
+ bodies: 3,
987
+ energyKeptFirstBody: 0.69,
988
+ expectedBodies: 3,
989
+ bestiaryEnergyKept: 0.65
990
+ },
991
+ {
992
+ caliber: "7_62x51",
993
+ force: 26.67,
994
+ bodies: 4,
995
+ energyKeptFirstBody: 0.78,
996
+ expectedBodies: 4,
997
+ bestiaryEnergyKept: 0.75
998
+ },
999
+ {
1000
+ caliber: "7_62x39",
1001
+ force: 18.52,
1002
+ bodies: 3,
1003
+ energyKeptFirstBody: 0.71,
1004
+ expectedBodies: 3
1005
+ },
1006
+ {
1007
+ caliber: "12ga_00buck",
1008
+ force: 4.35,
1009
+ bodies: 1,
1010
+ energyKeptFirstBody: 0.37,
1011
+ expectedBodies: 1
1012
+ }
1013
+ ],
1014
+ torsoResistance: 7.5
1015
+ };
1016
+
1017
+ // src/damage/defaults.ts
1018
+ var { severity } = segments_default;
1019
+ var DEFAULT_DAMAGE_RULES = {
1020
+ deathRule: "vitality-or-head",
1021
+ segmentRule: "kit",
1022
+ severity: { ...severity, stunAtPoints: 35, limbVitalityShare: 0.3, regionLimbVitalityShare: 0.5 },
1023
+ blast: { minimumPoints: 8, headDestroyPoints: 40, bigPoints: 120 },
1024
+ defaultCut: segments_default.defaultCut,
1025
+ points: physical_default.points,
1026
+ spread: 0,
1027
+ segments: SEGMENT_NUMBERS,
1028
+ burnPointsPerSecond: 3
1029
+ };
1030
+ var ARSENAL_RULES = { ...DEFAULT_DAMAGE_RULES, segmentRule: "arsenal" };
1031
+ var ELENCO_HIT_POINTS = segments_default.elenco.segments;
1032
+ var ELENCO_RULES = {
1033
+ ...DEFAULT_DAMAGE_RULES,
1034
+ segmentRule: "elenco",
1035
+ spread: 0.15,
1036
+ segments: Object.fromEntries(
1037
+ Object.entries(SEGMENT_NUMBERS).map(([kind, numbers]) => [kind, { ...numbers, multiplier: 1, hitPoints: ELENCO_HIT_POINTS[kind]?.hp ?? numbers.hitPoints }])
1038
+ )
1039
+ };
1040
+ var DAMAGE_PRESETS = { kit: DEFAULT_DAMAGE_RULES, arsenal: ARSENAL_RULES, elenco: ELENCO_RULES };
1041
+ var PROTECTION_PRESETS = [
1042
+ { id: "vest_police", slots: ["torso"], ballistic: { resistance: 14, blunt: 0.3, pass: 0.8 } },
1043
+ { id: "vest_military", slots: ["torso"], ballistic: { resistance: 28, blunt: 0.5 } },
1044
+ { id: "helmet_combat", slots: ["head"], ballistic: { resistance: 9, blunt: 0, pass: 1, consumable: true } },
1045
+ { id: "turnout_gear", slots: ["torso", "arms", "legs"], fire: 0.25, limbs: 0.8 }
1046
+ ];
1047
+ var CLASS_OF = { pistol: "pistol", rifle: "rifle" };
1048
+ var TEAM_GAME_CALIBERS = Object.entries(team_game_default.calibers).map(([id, entry]) => ({
1049
+ id,
1050
+ points: entry.points,
1051
+ penetration: entry.penetration,
1052
+ class: CLASS_OF[entry.class] ?? "pistol",
1053
+ skullRange: entry.skullRange,
1054
+ pellets: "pellets" in entry ? entry.pellets : 1,
1055
+ effectiveRangeMetres: entry.effectiveRangeMetres
1056
+ }));
1057
+ var TEAM_GAME_CALIBER_KEYS = Object.fromEntries(Object.entries(team_game_default.calibers).map(([id, entry]) => [entry.gameKey, id]));
1058
+ var TEAM_GAME_RULES = { regionCost: team_game_default.regionCost, damageKeptPerBody: team_game_default.damageKeptPerBody, falloff: team_game_default.falloff };
1059
+ var PHYSICAL_RULES = physical_default;
1060
+
1061
+ // src/damage/penetration.ts
1062
+ var PHYSICAL_PENETRATION = {
1063
+ id: "physical",
1064
+ stops: (layer, projectile) => projectile.force <= (layer.ballistic?.resistance ?? 0),
1065
+ piercesSkull: (projectile, headResistance) => projectile.force > headResistance
1066
+ };
1067
+ var TEAM_GAME_PENETRATION = {
1068
+ id: "team-game",
1069
+ stops: (_layer, projectile) => projectile.class === "pistol",
1070
+ piercesSkull: (projectile) => projectile.skullRange === 1 || projectile.distance <= projectile.skullRange
1071
+ };
1072
+ var PENETRATION_MODELS = { physical: PHYSICAL_PENETRATION, "team-game": TEAM_GAME_PENETRATION };
1073
+ var { tissuePerCentimetre, torsoPath, regionFactor } = PHYSICAL_RULES;
1074
+ var TORSO_RESISTANCE = torsoPath.fleshCentimetres * tissuePerCentimetre.flesh + torsoPath.boneCentimetres * tissuePerCentimetre.bone;
1075
+ var HEAD_KINDS = /* @__PURE__ */ new Set(["head", "jaw", "neck"]);
1076
+ var TORSO_KINDS = /* @__PURE__ */ new Set(["torso", "pelvis"]);
1077
+ var segmentResistance = ({ kind, bodyCount, protection }) => {
1078
+ if (TORSO_KINDS.has(kind)) return TORSO_RESISTANCE * regionFactor.torso * bodyCount + protection;
1079
+ if (HEAD_KINDS.has(kind)) return TORSO_RESISTANCE * regionFactor.head + protection;
1080
+ return TORSO_RESISTANCE * regionFactor.limb + protection;
1081
+ };
1082
+ var energyKept = (force, resistance) => force > 0 ? force / (force + resistance) : 0;
1083
+ var teamGameCost = (region, bodyCount) => {
1084
+ const cost = TEAM_GAME_RULES.regionCost;
1085
+ if (region === "head") return cost.head;
1086
+ if (region === "torso") return bodyCount > 1 ? cost.bruteTorso : cost.torso;
1087
+ return cost.limb;
1088
+ };
1089
+ var teamGameFalloff = (distance2, range) => {
1090
+ const floor = TEAM_GAME_RULES.falloff.atTwiceRange;
1091
+ return distance2 <= range * TEAM_GAME_RULES.falloff.fullAtRange ? 1 : Math.max(floor, 1 - (distance2 - range) / range * (1 - floor));
1092
+ };
1093
+
1094
+ // src/damage/rules/blow.ts
1095
+ var NO_HEADSHOT = { headshot: false };
1096
+ var HEADSHOT = { headshot: true };
1097
+ var ARM_KINDS = /* @__PURE__ */ new Set(["upperArm", "forearm", "hand"]);
1098
+ var LEG_KINDS = /* @__PURE__ */ new Set(["thigh", "shin", "foot"]);
1099
+ var isArm = (kind) => ARM_KINDS.has(kind);
1100
+ var isLeg = (kind) => LEG_KINDS.has(kind);
1101
+ var isLimb = (kind) => isArm(kind) || isLeg(kind);
1102
+ var typeMultiplier = (spec, kind) => {
1103
+ if (isArm(kind)) return spec.multipliers.arms;
1104
+ if (isLeg(kind)) return spec.multipliers.legs;
1105
+ if (kind === "torso" || kind === "pelvis") return spec.multipliers.torso;
1106
+ return spec.multipliers.head;
1107
+ };
1108
+ var segmentDamage = (ops, blow, points) => {
1109
+ const { kind } = blow.segment;
1110
+ return points * ops.rules.segments[kind].multiplier * typeMultiplier(ops.spec, kind) * (isLimb(kind) ? blow.limbFactor : 1);
1111
+ };
1112
+ var bleedStump = (ops, segment) => {
1113
+ const bleed = ops.rules.segments[segment.kind].bleed;
1114
+ const parent = segmentNode(segment.id).parent;
1115
+ const stump = parent ? ops.body.segments.find((candidate) => candidate.id === parent) ?? segment : segment;
1116
+ ops.bleed({ segment: stump, rate: bleed * (segment.kind === "neck" || segment.kind === "head" ? 1.5 : 1), duration: 45, spurt: true });
1117
+ };
1118
+ var bleedPiece = (ops, segment) => ops.bleed({ segment, rate: ops.rules.segments[segment.kind].bleed * 0.35, duration: 15, spurt: true });
1119
+ var bleedWound = (ops, segment, point) => ops.bleed({ segment, rate: ops.rules.segments[segment.kind].bleed * 0.3, duration: 10, spurt: false, point });
1120
+ var severWithBleeding = (ops, segment) => {
1121
+ if (!segment.attached) return;
1122
+ bleedStump(ops, segment);
1123
+ ops.sever(segment);
1124
+ bleedPiece(ops, segment);
1125
+ };
1126
+ var destroyWithBleeding = (ops, segment) => {
1127
+ if (!segment.attached) return;
1128
+ bleedStump(ops, segment);
1129
+ ops.destroy(segment);
1130
+ };
1131
+
1132
+ // src/damage/rules/regions.ts
1133
+ import { MISSING_BITS as MISSING_BITS2 } from "@adatechnology/game-contracts";
1134
+
1135
+ // src/damage/points.ts
1136
+ var createRng = (seed) => {
1137
+ let state = seed >>> 0;
1138
+ return () => {
1139
+ state = state + 1831565813 >>> 0;
1140
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
1141
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
1142
+ return ((mixed ^ mixed >>> 14) >>> 0) / 4294967296;
1143
+ };
1144
+ };
1145
+ var pointsFromEnergy = (energy, rules) => energy > 0 ? rules.points.coefficient * energy ** rules.points.exponent : 0;
1146
+ var resolvePoints = (hit, caliber, rules) => hit.points ?? caliber?.points ?? pointsFromEnergy(hit.energy ?? 0, rules);
1147
+ var spreadPoints = (points, spread, random) => spread > 0 ? points * (1 - spread + random() * 2 * spread) : points;
1148
+ var DEFAULT_PROJECTILE_GRAMS = 8;
1149
+ var RIFLE_JOULES = 1e3;
1150
+ var penetrationForce = (energy, grams, diameterMm) => {
1151
+ const area = Math.PI * diameterMm ** 2 / 4;
1152
+ return area > 0 && energy > 0 ? Math.sqrt(energy * grams / area) : 0;
1153
+ };
1154
+ var projectileOf = (hit, caliber) => {
1155
+ const pellets = hit.pellets ?? caliber?.pellets ?? 1;
1156
+ const energy = hit.energy ?? 0;
1157
+ const fallback = energy > RIFLE_JOULES ? "rifle" : "pistol";
1158
+ return {
1159
+ force: penetrationForce(energy, hit.mass ?? DEFAULT_PROJECTILE_GRAMS, hit.caliber ?? 9),
1160
+ class: caliber?.class ?? fallback,
1161
+ skullRange: caliber?.skullRange ?? 1,
1162
+ pellets,
1163
+ distance: hit.distance ?? 0
1164
+ };
1165
+ };
1166
+ var cutOf = (hit, projectile, rules) => {
1167
+ if (hit.cut !== void 0) return hit.cut;
1168
+ const type = hit.type;
1169
+ if (type === "ballistic" && projectile) return rules.defaultCut.ballistic[projectile.pellets > 1 ? "buckshot" : projectile.class];
1170
+ if (type === "slash" || type === "blunt" || type === "explosive") return rules.defaultCut[type];
1171
+ return 0;
1172
+ };
1173
+
1174
+ // src/damage/rules/regions.ts
1175
+ var LIMB_BITS = {
1176
+ armL: { severed: MISSING_BITS2.armL, broken: MISSING_BITS2.armLBroken },
1177
+ armR: { severed: MISSING_BITS2.armR, broken: MISSING_BITS2.armRBroken },
1178
+ legL: { severed: MISSING_BITS2.legL, broken: MISSING_BITS2.legLBroken },
1179
+ legR: { severed: MISSING_BITS2.legR, broken: MISSING_BITS2.legRBroken }
1180
+ };
1181
+ var rootOf = (ops, region) => segmentOf(ops.body, REGION_SEGMENTS[region][0]);
1182
+ var hitHead = (ops, blow, points) => {
1183
+ const { body, rules } = ops;
1184
+ const rule = rules.severity;
1185
+ const multiplier = rules.segments.head.multiplier;
1186
+ const { projectile } = blow;
1187
+ const lethal = projectile ? blow.model.piercesSkull(projectile, blow.headResistance) && points >= rule.headKillDamage : points * multiplier >= body.regions.head;
1188
+ if (lethal) {
1189
+ const shotgunClose = projectile !== void 0 && projectile.pellets > 1 && projectile.distance < rule.headBitShotgunMetres;
1190
+ if (points > rule.headBitDamage || shotgunClose) {
1191
+ body.missing |= MISSING_BITS2.head;
1192
+ ops.destroy(segmentOf(body, "head"));
1193
+ }
1194
+ return HEADSHOT;
1195
+ }
1196
+ body.vitality -= points * multiplier;
1197
+ body.regions.head -= points;
1198
+ ops.damage(segmentOf(body, "head"), points * multiplier, blow.damageType);
1199
+ if (points > rule.jawDamage && ops.random() < rule.jawChance && (body.missing & MISSING_BITS2.jaw) === 0) {
1200
+ body.missing |= MISSING_BITS2.jaw;
1201
+ ops.sever(segmentOf(body, "jaw"));
1202
+ }
1203
+ return NO_HEADSHOT;
1204
+ };
1205
+ var hitLimb = (ops, blow, region, points) => {
1206
+ const { body, spec, rules } = ops;
1207
+ const leg = region === "legL" || region === "legR";
1208
+ const factor = (leg ? spec.multipliers.legs : spec.multipliers.arms) * spec.limbFactor * blow.protection.limbs;
1209
+ const amount = points * factor;
1210
+ body.regions[region] -= amount;
1211
+ body.vitality -= amount * rules.severity.regionLimbVitalityShare;
1212
+ const root = rootOf(ops, region);
1213
+ ops.damage(root, amount, blow.damageType);
1214
+ const max = REGION_HIT_POINTS[region];
1215
+ if (leg && body.regions[region] <= max * rules.severity.limpAtHpFraction) ops.limp(region === "legL" ? "L" : "R");
1216
+ if (body.regions[region] > 0) return;
1217
+ const severs = blow.cut >= rules.severity.severAtCut || amount > max * rules.severity.severAtDamageFraction;
1218
+ const bit = severs ? LIMB_BITS[region].severed : LIMB_BITS[region].broken;
1219
+ const isNew = (body.missing & bit) === 0;
1220
+ body.missing |= bit;
1221
+ if (leg) ops.crawl();
1222
+ if (!isNew) return;
1223
+ if (severs) ops.sever(root);
1224
+ else ops.breakBone(root);
1225
+ };
1226
+ var regionRule = (ops, blow) => {
1227
+ const { body, rules } = ops;
1228
+ const points = spreadPoints(blow.points, rules.spread, ops.random);
1229
+ let result = NO_HEADSHOT;
1230
+ if (blow.region === "head") result = hitHead(ops, blow, points);
1231
+ else if (blow.region === "torso") {
1232
+ const amount = points * ops.spec.multipliers.torso * blow.protection.ballistic;
1233
+ body.vitality -= amount;
1234
+ ops.damage(segmentOf(body, "torso"), amount, blow.damageType);
1235
+ } else hitLimb(ops, blow, blow.region, points);
1236
+ if (result.headshot) ops.kill("head");
1237
+ else if (body.vitality <= 0) ops.kill("vitality");
1238
+ if (!body.alive || result.headshot || body.vitality <= 0) return result;
1239
+ if (points >= rules.severity.stunAtPoints) ops.stun();
1240
+ return result;
1241
+ };
1242
+
1243
+ // src/damage/blast.ts
1244
+ var shares = (params, targets, pivot) => targets.map((target) => ({ target, points: params.points * Math.max(0, 1 - distance(params.jointOf(pivot(target)), params.center) / params.radius) })).filter((share) => share.points > params.ops.rules.blast.minimumPoints).sort((a, b) => b.points - a.points);
1245
+ var segmentBlast = (params) => {
1246
+ const { ops, rule, blow } = params;
1247
+ const { blast: blast2, segments } = ops.rules;
1248
+ let result = NO_HEADSHOT;
1249
+ for (const { target, points } of shares(params, ops.body.segments.filter((segment) => segment.attached), (segment) => segment.id)) {
1250
+ if (!target.attached) continue;
1251
+ if (target.kind === "head" && points > blast2.headDestroyPoints) {
1252
+ destroyWithBleeding(ops, target);
1253
+ ops.kill("explosion");
1254
+ result = HEADSHOT;
1255
+ continue;
1256
+ }
1257
+ rule(ops, { ...blow, segment: target, points: points / segments[target.kind].multiplier, big: points > blast2.bigPoints, explosive: true });
1258
+ }
1259
+ return result;
1260
+ };
1261
+ var regionBlast = (params) => {
1262
+ let result = NO_HEADSHOT;
1263
+ const pivot = (region) => segmentNode(REGION_SEGMENTS[region][0]).id;
1264
+ for (const { target, points } of shares(params, BODY_REGIONS, pivot)) {
1265
+ if (regionRule(params.ops, { ...params.blow, region: target, points }).headshot) result = HEADSHOT;
1266
+ }
1267
+ return result;
1268
+ };
1269
+
1270
+ // src/damage/protection.ts
1271
+ var LAYERED_PROTECTION_CAP = 0.9;
1272
+ var layeredProtection = ({ layers, attack, conditions }) => {
1273
+ const through = layers.reduce((left, layer) => left * (1 - (layer[attack] ?? 0) * (conditions?.[layer.id] ?? 1)), 1);
1274
+ return Math.min(LAYERED_PROTECTION_CAP, 1 - through);
1275
+ };
1276
+ var layersOn = (worn, slots, consumed) => worn.filter((layer) => !consumed.has(layer.id) && layer.slots.some((slot) => slots.includes(slot)));
1277
+ var protect = ({ layers, projectile, model }) => {
1278
+ let ballistic = 1;
1279
+ let resistance = 0;
1280
+ for (const layer of layers) {
1281
+ const armour = layer.ballistic;
1282
+ if (!armour || !projectile) continue;
1283
+ const stops = model.stops(layer, projectile);
1284
+ if (stops && armour.consumable) return { absorbedBy: layer.id, ballistic: 0, resistance: armour.resistance, limbs: 1, fire: 1 };
1285
+ ballistic *= stops ? armour.blunt : armour.pass ?? armour.blunt;
1286
+ resistance += armour.resistance;
1287
+ }
1288
+ const limbs = layers.reduce((factor, layer) => factor * (layer.limbs ?? 1), 1);
1289
+ const fire = layers.reduce((factor, layer) => factor * (layer.fire ?? 1), 1);
1290
+ return { ballistic, resistance, limbs, fire };
1291
+ };
1292
+
1293
+ // src/damage/resolve-part.ts
1294
+ var radiusOf = (part) => part.shape.kind === "box" ? Math.min(...part.shape.size) / 2 : part.shape.radius;
1295
+ var resolveSegment = (body, spec, part, localPoint) => {
1296
+ if (part !== void 0 && isSegmentId(part)) return part;
1297
+ if (part !== void 0 && isBodyRegion(part)) return REGION_SEGMENTS[part][0];
1298
+ let best = "torso";
1299
+ let bestGap = Infinity;
1300
+ for (const candidate of spec.parts) {
1301
+ const segment = body.segments.find((entry) => entry.id === candidate.id);
1302
+ if (!segment?.attached) continue;
1303
+ const gap = distance(candidate.position, localPoint) - radiusOf(candidate);
1304
+ if (gap < bestGap) [best, bestGap] = [segment.id, gap];
1305
+ }
1306
+ return best;
1307
+ };
1308
+ var regionOfSegment = (body, id) => body.segments.find((segment) => segment.id === id)?.region ?? "torso";
1309
+
1310
+ // src/damage/rules/arsenal.ts
1311
+ var HEAD_BLOW_POINTS = 30;
1312
+ var LIMB_VITALITY_SHARE = 0.3;
1313
+ var split = (ops) => {
1314
+ const torso = segmentOf(ops.body, "torso");
1315
+ if (!torso.attached) return;
1316
+ severWithBleeding(ops, torso);
1317
+ ops.change("split", "torso");
1318
+ ops.kill("vitality");
1319
+ };
1320
+ var loseLimb = (ops, segment, blow) => {
1321
+ if (blow.big || -segment.hp > segment.max) destroyWithBleeding(ops, segment);
1322
+ else severWithBleeding(ops, segment);
1323
+ const { body } = ops;
1324
+ if (segment.kind === "thigh" || segment.kind === "shin") {
1325
+ if (body.alive) ops.crawl();
1326
+ } else if (segment.kind === "foot" && body.alive && !body.crawl && body.limping.length === 0 && segment.side) ops.limp(segment.side);
1327
+ };
1328
+ var arsenalRule = (ops, blow) => {
1329
+ const { segment } = blow;
1330
+ if (!segment.attached) return NO_HEADSHOT;
1331
+ const amount = segmentDamage(ops, blow, blow.points);
1332
+ if (blow.point) bleedWound(ops, segment, blow.point);
1333
+ segment.hp -= amount;
1334
+ ops.damage(segment, amount, blow.damageType);
1335
+ let result = NO_HEADSHOT;
1336
+ if (segment.kind === "head") {
1337
+ if (blow.points >= HEAD_BLOW_POINTS || segment.hp <= 0) {
1338
+ destroyWithBleeding(ops, segment);
1339
+ ops.kill("head");
1340
+ result = HEADSHOT;
1341
+ } else ops.wound(segment);
1342
+ } else if (segment.kind === "jaw") {
1343
+ if (segment.hp <= 0) severWithBleeding(ops, segment);
1344
+ } else if (segment.kind === "neck") {
1345
+ if (segment.hp <= 0) {
1346
+ severWithBleeding(ops, segment);
1347
+ ops.kill("head");
1348
+ }
1349
+ } else if (segment.kind === "torso" || segment.kind === "pelvis") {
1350
+ ops.body.vitality -= amount;
1351
+ if (blow.points >= ops.rules.severity.splitAtDamage || segment.hp <= 0) split(ops);
1352
+ } else {
1353
+ ops.body.vitality -= amount * LIMB_VITALITY_SHARE;
1354
+ if (segment.hp <= 0) loseLimb(ops, segment, blow);
1355
+ }
1356
+ if (ops.body.alive && ops.body.vitality <= 0) ops.kill("vitality");
1357
+ return result;
1358
+ };
1359
+
1360
+ // src/damage/rules/elenco.ts
1361
+ var HEAD_SEVER_CUT = 0.7;
1362
+ var HEAD_SEVER_POINTS = 90;
1363
+ var JOINTED = /* @__PURE__ */ new Set(["head", "upperArm", "forearm", "hand", "thigh", "shin", "foot"]);
1364
+ var elencoRule = (ops, blow) => {
1365
+ const { segment } = blow;
1366
+ const { body, rules } = ops;
1367
+ if (!segment.attached || !body.alive && segment.kind !== "head") return NO_HEADSHOT;
1368
+ const amount = segmentDamage(ops, blow, spreadPoints(blow.points, rules.spread, ops.random));
1369
+ segment.hp = Math.max(0, segment.hp - amount);
1370
+ ops.damage(segment, amount, blow.damageType);
1371
+ if (segment.hp > 0) {
1372
+ ops.wound(segment);
1373
+ return NO_HEADSHOT;
1374
+ }
1375
+ if (segment.kind === "head") {
1376
+ if (blow.cut >= HEAD_SEVER_CUT || blow.points >= HEAD_SEVER_POINTS) ops.sever(segment, "zero");
1377
+ else ops.breakBone(segment);
1378
+ ops.kill("head");
1379
+ return HEADSHOT;
1380
+ }
1381
+ if (JOINTED.has(segment.kind)) {
1382
+ if (blow.cut >= rules.severity.severAtCut || amount >= segment.max * rules.severity.severAtDamageFraction) {
1383
+ ops.sever(segment, "zero");
1384
+ if (isLeg(segment.kind)) ops.crawl();
1385
+ return NO_HEADSHOT;
1386
+ }
1387
+ ops.breakBone(segment);
1388
+ if (isLeg(segment.kind) && segment.side) {
1389
+ ops.limp(segment.side);
1390
+ if (body.limping.length === 2) ops.crawl();
1391
+ }
1392
+ return NO_HEADSHOT;
1393
+ }
1394
+ ops.breakBone(segment);
1395
+ if (segment.kind === "torso") ops.kill("vitality");
1396
+ else if (segment.kind === "pelvis") ops.crawl();
1397
+ else if (segment.kind === "neck") ops.kill("head");
1398
+ return NO_HEADSHOT;
1399
+ };
1400
+
1401
+ // src/damage/rules/kit.ts
1402
+ var severityOf = (ops, blow, amount, max) => {
1403
+ const rule = ops.rules.severity;
1404
+ if (blow.explosive || amount >= max * rule.destroyAtDamageMultiple) return "destroyed";
1405
+ if (blow.cut >= rule.severAtCut || amount > max * rule.severAtDamageFraction) return "severed";
1406
+ return "broken";
1407
+ };
1408
+ var lose = (ops, segment, outcome) => {
1409
+ if (outcome === "destroyed") destroyWithBleeding(ops, segment);
1410
+ else if (outcome === "severed") severWithBleeding(ops, segment);
1411
+ else ops.breakBone(segment);
1412
+ };
1413
+ var hitHead2 = (ops, blow, points, amount, hpBefore) => {
1414
+ const { segment, projectile } = blow;
1415
+ const rule = ops.rules.severity;
1416
+ const lethal = projectile ? blow.model.piercesSkull(projectile, blow.headResistance) && points >= rule.headKillDamage : amount >= hpBefore;
1417
+ if (lethal) {
1418
+ const shotgunClose = projectile !== void 0 && projectile.pellets > 1 && projectile.distance < rule.headBitShotgunMetres;
1419
+ if (blow.explosive || points > rule.headBitDamage || shotgunClose) destroyWithBleeding(ops, segment);
1420
+ else ops.breakBone(segment);
1421
+ ops.kill("head");
1422
+ return HEADSHOT;
1423
+ }
1424
+ ops.body.vitality -= amount;
1425
+ ops.wound(segment);
1426
+ if (points > rule.jawDamage && ops.random() < rule.jawChance) severWithBleeding(ops, segmentOf(ops.body, "jaw"));
1427
+ return NO_HEADSHOT;
1428
+ };
1429
+ var hitTorso = (ops, points, amount, segment) => {
1430
+ ops.body.vitality -= amount;
1431
+ if (points < ops.rules.severity.splitAtDamage && segment.hp > 0) return ops.wound(segment);
1432
+ const torso = segmentOf(ops.body, "torso");
1433
+ if (!torso.attached) return;
1434
+ severWithBleeding(ops, torso);
1435
+ ops.change("split", "torso");
1436
+ if (ops.rules.deathRule === "head-only") ops.crawl();
1437
+ else ops.kill("vitality");
1438
+ };
1439
+ var hitLimb2 = (ops, blow, amount, hpBefore) => {
1440
+ const { segment } = blow;
1441
+ const rule = ops.rules.severity;
1442
+ ops.body.vitality -= amount * rule.limbVitalityShare;
1443
+ if (isLeg(segment.kind) && segment.side && segment.hp <= segment.max * rule.limpAtHpFraction) ops.limp(segment.side);
1444
+ if (segment.hp > 0 || hpBefore <= 0) return ops.wound(segment);
1445
+ const outcome = severityOf(ops, blow, amount, segment.max);
1446
+ lose(ops, segment, outcome);
1447
+ if (segment.kind === "thigh" || segment.kind === "shin") ops.crawl();
1448
+ };
1449
+ var kitRule = (ops, blow) => {
1450
+ const { segment } = blow;
1451
+ if (!segment.attached) return NO_HEADSHOT;
1452
+ const points = spreadPoints(blow.points, ops.rules.spread, ops.random);
1453
+ const amount = segmentDamage(ops, blow, points);
1454
+ if (blow.point && !blow.explosive) bleedWound(ops, segment, blow.point);
1455
+ const hpBefore = segment.hp;
1456
+ segment.hp -= amount;
1457
+ ops.damage(segment, amount, blow.damageType);
1458
+ let result = NO_HEADSHOT;
1459
+ if (segment.kind === "head") result = hitHead2(ops, blow, points, amount, hpBefore);
1460
+ else if (segment.kind === "torso" || segment.kind === "pelvis") hitTorso(ops, points, amount, segment);
1461
+ else if (segment.kind === "jaw" || segment.kind === "neck") {
1462
+ if (segment.hp <= 0 && hpBefore > 0) {
1463
+ lose(ops, segment, severityOf(ops, blow, amount, segment.max));
1464
+ if (segment.kind === "neck" && !segment.attached) ops.kill("head");
1465
+ } else ops.wound(segment);
1466
+ } else hitLimb2(ops, blow, amount, hpBefore);
1467
+ if (ops.body.vitality <= 0) ops.kill("vitality");
1468
+ if (ops.body.alive && points >= ops.rules.severity.stunAtPoints) ops.stun();
1469
+ return result;
1470
+ };
1471
+
1472
+ // src/damage/pipeline.ts
1473
+ var SEGMENT_RULES = { kit: kitRule, arsenal: arsenalRule, elenco: elencoRule };
1474
+ var HEAD_RESISTANCE = segmentResistance({ kind: "head", bodyCount: 1, protection: 0 });
1475
+ var NO_PROTECTION = { ballistic: 1, resistance: 0, limbs: 1, fire: 1 };
1476
+ var ABSORBED_SPARKS = 8;
1477
+ var METRES_PER_MM = 1e-3;
1478
+ var report = (ops, fields, result = { headshot: false }) => ({
1479
+ through: false,
1480
+ exitEnergy: 0,
1481
+ ...fields,
1482
+ killed: !ops.body.alive,
1483
+ headshot: result.headshot
1484
+ });
1485
+ var burn = ({ ops, hit, setup, worn }) => {
1486
+ const torso = segmentOf(ops.body, "torso");
1487
+ const fire = protect({ layers: layersOn(worn, segmentNode("torso").slots, ops.body.consumed), model: setup.model }).fire;
1488
+ const points = setup.rules.burnPointsPerSecond * (hit.duration ?? 0) * fire;
1489
+ ops.body.vitality -= points;
1490
+ ops.damage(torso, points, hit.type);
1491
+ if (ops.body.vitality <= 0) ops.kill("vitality");
1492
+ return report(ops, { part: "torso", points });
1493
+ };
1494
+ var blast = ({ ops, hit, setup, localPoint, jointOf }) => {
1495
+ const points = hit.points ?? pointsFromEnergy(hit.energy ?? 0, setup.rules);
1496
+ const base = { ops, center: localPoint, points, radius: hit.radius ?? 1, jointOf };
1497
+ const common = { cut: hit.cut ?? setup.rules.defaultCut.explosive, damageType: hit.type, model: setup.model, headResistance: HEAD_RESISTANCE };
1498
+ const rule = SEGMENT_RULES[setup.rules.segmentRule] ?? kitRule;
1499
+ const result = ops.body.detail === "regions" ? regionBlast({ ...base, blow: { ...common, protection: NO_PROTECTION } }) : segmentBlast({ ...base, rule, blow: { ...common, limbFactor: ops.spec.limbFactor } });
1500
+ return report(ops, { points }, result);
1501
+ };
1502
+ var DEFAULT_PATH_METRES = 0.2;
1503
+ var pathThrough = (spec, id) => {
1504
+ const shape = spec.parts.find((part) => part.id === id)?.shape;
1505
+ return shape && shape.kind !== "box" ? 2 * shape.radius : DEFAULT_PATH_METRES;
1506
+ };
1507
+ var exitOf = (hit, depth) => {
1508
+ const [x, y, z] = hit.direction ?? [0, 0, -1];
1509
+ return [hit.point[0] + x * depth, hit.point[1] + y * depth, hit.point[2] + z * depth];
1510
+ };
1511
+ var direct = ({ ops, hit, setup, worn, localPoint, describe }) => {
1512
+ const { body, spec, rules } = ops;
1513
+ const caliber = hit.caliberId === void 0 ? void 0 : setup.calibers.get(hit.caliberId);
1514
+ const projectile = hit.type === "ballistic" ? projectileOf(hit, caliber) : void 0;
1515
+ const id = resolveSegment(body, describe(), hit.part, localPoint);
1516
+ const region = regionOfSegment(body, id);
1517
+ const slots = segmentNode(body.detail === "regions" ? REGION_SEGMENTS[region][0] : id).slots;
1518
+ const protection = protect({ layers: layersOn(worn, slots, body.consumed), model: setup.model, ...projectile ? { projectile } : {} });
1519
+ if (protection.absorbedBy) {
1520
+ body.consumed.add(protection.absorbedBy);
1521
+ ops.emit({ type: "debris", part: id, kind: "sparks", point: hit.point, normal: [0, 1, 0], count: ABSORBED_SPARKS, speed: 3 });
1522
+ return report(ops, { part: id, points: 0, absorbedBy: protection.absorbedBy });
1523
+ }
1524
+ const rawPoints = resolvePoints(hit, caliber, rules);
1525
+ const points = rawPoints * protection.ballistic;
1526
+ const cut = cutOf(hit, projectile, rules);
1527
+ const segment = segmentOf(body, id);
1528
+ const resistance = segmentResistance({ kind: segment.kind, bodyCount: spec.bodyCount, protection: protection.resistance });
1529
+ const through = setup.model.id === "physical" && projectile !== void 0 && projectile.force > resistance;
1530
+ const exitEnergy = through && projectile ? (hit.energy ?? 0) * energyKept(projectile.force, resistance) : 0;
1531
+ if (projectile) {
1532
+ const depth = pathThrough(describe(), id);
1533
+ ops.emit({ type: "hole", part: id, hole: { point: localPoint, normal: [0, 0, 1], diameter: (hit.caliber ?? 9) * METRES_PER_MM, through }, worldPoint: hit.point, ...through ? { exitPoint: exitOf(hit, depth) } : {} });
1534
+ }
1535
+ const common = { points, cut, damageType: hit.type, model: setup.model, headResistance: HEAD_RESISTANCE, ...projectile ? { projectile } : {} };
1536
+ const result = body.detail === "regions" ? regionRule(ops, { ...common, points: rawPoints, region, protection }) : (SEGMENT_RULES[rules.segmentRule] ?? kitRule)(ops, {
1537
+ ...common,
1538
+ segment,
1539
+ explosive: false,
1540
+ big: points > rules.blast.bigPoints,
1541
+ limbFactor: spec.limbFactor * protection.limbs,
1542
+ ...hit.woundAtPoint === false ? {} : { point: hit.point }
1543
+ });
1544
+ return report(ops, { part: id, points, through, exitEnergy }, result);
1545
+ };
1546
+ var applyBlow = (params) => {
1547
+ if (params.hit.type === "fire" || params.hit.type === "heat") return burn(params);
1548
+ if (params.hit.type === "explosive") return blast(params);
1549
+ return direct(params);
1550
+ };
1551
+
1552
+ // src/damage/damage-character.ts
1553
+ var createDamageCharacter = (core, setup) => {
1554
+ const view = characterView(core);
1555
+ const worn = core.spec.protection.map((id) => setup.protection.get(id)).filter((layer) => layer !== void 0);
1556
+ let bleeders = [];
1557
+ let hitCounter = 0;
1558
+ let clock = 0;
1559
+ let lastHit;
1560
+ const transform = () => ({ position: core.position, rotation: core.rotation });
1561
+ const jointOf = (id) => core.rig.joints[segmentNode(id).joint];
1562
+ const pointOf = (id) => toWorld(transform(), jointOf(id));
1563
+ const opsFor = (random, source) => createBodyOps({ body: core.body, spec: core.spec, rules: setup.rules, random, time: clock, pointOf, bleeders, ...source !== void 0 ? { source } : {} });
1564
+ const character = {
1565
+ ...view,
1566
+ get segments() {
1567
+ return core.body.segments;
1568
+ },
1569
+ get detail() {
1570
+ return core.body.detail;
1571
+ },
1572
+ get alive() {
1573
+ return core.body.alive;
1574
+ },
1575
+ get limping() {
1576
+ return [...core.body.limping];
1577
+ },
1578
+ get stunSeconds() {
1579
+ return core.body.stunSeconds;
1580
+ },
1581
+ get lastHit() {
1582
+ return lastHit;
1583
+ },
1584
+ name: core.spec.kind,
1585
+ hit(raw) {
1586
+ const hit = raw;
1587
+ hitCounter += 1;
1588
+ const random = core.spec.random ?? createRng((core.spec.seed ^ hitCounter) >>> 0);
1589
+ const ops = opsFor(random, hit.source);
1590
+ lastHit = applyBlow({ ops, hit, setup, worn, localPoint: toLocal(transform(), hit.point), jointOf, describe: core.describe });
1591
+ return ops.events;
1592
+ },
1593
+ update(dt) {
1594
+ clock += dt;
1595
+ core.body.stunSeconds = Math.max(0, core.body.stunSeconds - dt);
1596
+ const ops = opsFor(() => 0);
1597
+ stepBleeding({ bleeders, dt, emit: ops.emit, pointOf });
1598
+ return ops.events;
1599
+ },
1600
+ bleeders: () => bleeders.map((bleeder) => ({ ...bleeder })),
1601
+ intactProtection: () => core.spec.protection.filter((id) => !core.body.consumed.has(id)),
1602
+ resistanceOf(part) {
1603
+ const id = resolveSegment(core.body, core.describe(), part, [0, 0, 0]);
1604
+ const layers = layersOn(worn, segmentNode(id).slots, core.body.consumed);
1605
+ const protection = layers.reduce((sum, layer) => sum + (layer.ballistic?.resistance ?? 0), 0);
1606
+ return segmentResistance({ kind: segmentNode(id).kind, bodyCount: core.spec.bodyCount, protection });
1607
+ },
1608
+ protectionAgainst: (attack, part) => layeredProtection({ layers: layersOn(worn, segmentNode(part).slots, core.body.consumed), attack }),
1609
+ reset() {
1610
+ core.resetBody();
1611
+ bleeders = [];
1612
+ hitCounter = 0;
1613
+ lastHit = void 0;
1614
+ },
1615
+ serialize() {
1616
+ const { consumed, ...rest } = core.body;
1617
+ return { body: { ...structuredClone(rest), consumed: [...consumed] }, bleeders: structuredClone(bleeders), hitCounter, clock };
1618
+ },
1619
+ load(data) {
1620
+ const saved = data;
1621
+ Object.assign(core.body, structuredClone(saved.body), { consumed: new Set(saved.body.consumed) });
1622
+ bleeders = structuredClone(saved.bleeders);
1623
+ hitCounter = saved.hitCounter;
1624
+ clock = saved.clock;
1625
+ }
1626
+ };
1627
+ return character;
1628
+ };
1629
+
1630
+ // src/damage/line.ts
1631
+ var RESTART_METRES = 0.3;
1632
+ var regionOfPart = (part) => isSegmentId(part) ? segmentNode(part).region : isBodyRegion(part) ? part : "torso";
1633
+ var lineHit = (index, target, fields) => ({
1634
+ index,
1635
+ ...fields,
1636
+ killed: !target.character.alive,
1637
+ headshot: target.character.lastHit?.headshot ?? false
1638
+ });
1639
+ var teamGameLine = ({ shot, targets, calibers }) => {
1640
+ const caliber = shot.caliberId === void 0 ? void 0 : calibers.get(shot.caliberId);
1641
+ if (caliber?.penetration === void 0 || caliber.points === void 0) throw new Error(`game-characters: the team-game line needs a calibre with points and penetration ("${shot.caliberId}")`);
1642
+ let penetration = caliber.penetration;
1643
+ let points = caliber.points;
1644
+ let origin = 0;
1645
+ const hits = [];
1646
+ for (const [index, target] of targets.entries()) {
1647
+ if (!target.character.alive || target.distance - origin <= 0) continue;
1648
+ const distance2 = target.distance - origin;
1649
+ const dealt = points * teamGameFalloff(distance2, caliber.effectiveRangeMetres ?? Infinity);
1650
+ const events = target.character.hit({ type: "ballistic", point: target.point ?? [0, 0, 0], part: target.part, points: dealt, caliberId: caliber.id, distance: distance2, ...shot.source ? { source: shot.source } : {} });
1651
+ hits.push(lineHit(index, target, { points: dealt, energy: shot.energyAt(target.distance), events }));
1652
+ penetration -= teamGameCost(regionOfPart(target.part), target.character.spec.bodyCount);
1653
+ points *= TEAM_GAME_RULES.damageKeptPerBody;
1654
+ if (penetration <= 0) break;
1655
+ if (!target.character.alive) origin = target.distance + RESTART_METRES;
1656
+ }
1657
+ return { hits, stoppedAt: hits.length === 0 ? 0 : (hits[hits.length - 1]?.index ?? -1) + 1 };
1658
+ };
1659
+ var physicalLine = ({ shot, targets }) => {
1660
+ const first = targets[0];
1661
+ let force = first ? penetrationForce(shot.energyAt(first.distance), shot.mass, shot.caliber) : 0;
1662
+ let kept = 1;
1663
+ const hits = [];
1664
+ for (const [index, target] of targets.entries()) {
1665
+ if (force <= 0) break;
1666
+ if (!target.character.alive) continue;
1667
+ const energy = shot.energyAt(target.distance) * kept;
1668
+ const events = target.character.hit({ type: "ballistic", point: target.point ?? [0, 0, 0], part: target.part, energy, mass: shot.mass, caliber: shot.caliber, distance: target.distance, ...shot.caliberId ? { caliberId: shot.caliberId } : {} });
1669
+ hits.push(lineHit(index, target, { points: target.character.lastHit?.points ?? 0, energy, events }));
1670
+ const resistance = target.character.resistanceOf(target.part);
1671
+ kept *= energyKept(force, resistance);
1672
+ force -= resistance;
1673
+ }
1674
+ return { hits, stoppedAt: hits.length === 0 ? 0 : (hits[hits.length - 1]?.index ?? -1) + 1 };
1675
+ };
1676
+ var penetrateLine = (params) => params.model === "team-game" ? teamGameLine(params) : physicalLine(params);
1677
+
1678
+ // src/registry.ts
1679
+ var mergeById = (shipped, extra) => {
1680
+ const entries = /* @__PURE__ */ new Map();
1681
+ for (const entry of [...shipped, ...extra ?? []]) entries.set(entry.id, entry);
1682
+ return entries;
1683
+ };
1684
+ var applyPatches = (entries, patches) => {
1685
+ for (const [id, patch] of Object.entries(patches ?? {})) {
1686
+ const entry = entries.get(id);
1687
+ if (entry) entries.set(id, { ...entry, ...patch, id });
1688
+ }
1689
+ };
1690
+ var removeIds = (entries, ids) => {
1691
+ for (const id of ids ?? []) entries.delete(id);
1692
+ };
1693
+
1694
+ // src/kit.ts
1695
+ var DEFAULT_CORPSES = { max: 400, seconds: 900 };
1696
+ var PERSON_PRESET = { id: "person", kind: "person", age: "adult" };
1697
+ var applyPreset = (spec, presets) => {
1698
+ const preset = presets.get(spec.preset ?? spec.kind);
1699
+ if (!preset) return spec;
1700
+ const { id: _presetId, ...defaults } = preset;
1701
+ const multipliers = { ...defaults.multipliers, ...spec.multipliers };
1702
+ return { ...defaults, ...spec, multipliers };
1703
+ };
1704
+ var resolveDamageRules = (options = {}) => {
1705
+ const { preset, severity: severity2, blast: blast2, segments, ...rest } = options;
1706
+ const base = DAMAGE_PRESETS[preset ?? "kit"];
1707
+ return { ...base, ...rest, severity: { ...base.severity, ...severity2 }, blast: { ...base.blast, ...blast2 }, segments: { ...base.segments, ...segments } };
1708
+ };
1709
+ var modelOf = (option) => typeof option === "object" ? option : PENETRATION_MODELS[option ?? "physical"];
1710
+ var createCharacters = (options = {}) => {
1711
+ const body = createBodySpec(options.body);
1712
+ const packs = options.packs ?? [];
1713
+ const presets = mergeById([PERSON_PRESET], packs.flatMap((pack) => pack.presets ?? []));
1714
+ applyPatches(presets, options.patch);
1715
+ removeIds(presets, options.remove);
1716
+ const damage = resolveDamageRules(options.damage);
1717
+ const penetration = modelOf(options.penetration);
1718
+ const protection = mergeById(PROTECTION_PRESETS, [...packs.flatMap((pack) => pack.protection ?? []), ...options.protection ?? []]);
1719
+ const shippedCalibers = penetration.id === "team-game" ? TEAM_GAME_CALIBERS : [];
1720
+ const calibers = mergeById(shippedCalibers, [...packs.flatMap((pack) => pack.calibers ?? []), ...options.calibers ?? []]);
1721
+ const setup = { rules: damage, model: penetration, protection, calibers };
1722
+ let counter = 0;
1723
+ return {
1724
+ body,
1725
+ presets,
1726
+ corpses: { ...DEFAULT_CORPSES, ...options.corpses },
1727
+ damage,
1728
+ penetration,
1729
+ protection,
1730
+ calibers,
1731
+ create(spec) {
1732
+ counter += 1;
1733
+ const resolved = resolveSpec(applyPreset(spec, presets), body, counter);
1734
+ const core = createCharacterCore({ spec: resolved, bodySpec: body, hitPointsOf: (kind) => damage.segments[kind].hitPoints });
1735
+ return createDamageCharacter(core, setup);
1736
+ },
1737
+ createCorpses: () => createCorpseLedger({ ...DEFAULT_CORPSES, ...options.corpses }),
1738
+ penetrateLine: (shot, targets) => penetrateLine({ model: penetration.id === "team-game" ? "team-game" : "physical", shot, targets, calibers })
1739
+ };
1740
+ };
1741
+
1742
+ // src/data/gait.json
1743
+ var gait_default = {
1744
+ cascade: {
1745
+ hip: 16,
1746
+ spine: 11,
1747
+ shoulder: 9,
1748
+ elbow: 7,
1749
+ leg: 14,
1750
+ knee: 11,
1751
+ head: 6
1752
+ },
1753
+ moves: {
1754
+ idle: {
1755
+ gait: 0,
1756
+ freq: 0,
1757
+ amp: 0,
1758
+ knee: 0,
1759
+ bob: 0,
1760
+ lean: 0,
1761
+ arm: 0,
1762
+ elb: 0.12,
1763
+ crouch: 0,
1764
+ aim: 0,
1765
+ turn: 0,
1766
+ grip: 0.35
1767
+ },
1768
+ walk: {
1769
+ gait: 1,
1770
+ freq: 5.6,
1771
+ amp: 0.42,
1772
+ knee: 0.85,
1773
+ bob: 0.035,
1774
+ lean: 0.05,
1775
+ arm: 0.42,
1776
+ elb: 0.3,
1777
+ crouch: 0,
1778
+ aim: 0,
1779
+ turn: 0,
1780
+ grip: 0.4
1781
+ },
1782
+ run: {
1783
+ gait: 1,
1784
+ freq: 9.2,
1785
+ amp: 0.72,
1786
+ knee: 1.5,
1787
+ bob: 0.06,
1788
+ lean: 0.3,
1789
+ arm: 0.85,
1790
+ elb: 1.35,
1791
+ crouch: 0,
1792
+ aim: 0,
1793
+ turn: 0,
1794
+ grip: 1.1
1795
+ },
1796
+ crouch: {
1797
+ gait: 1,
1798
+ freq: 3.8,
1799
+ amp: 0.3,
1800
+ knee: 0.6,
1801
+ bob: 0.02,
1802
+ lean: 0.42,
1803
+ arm: 0.15,
1804
+ elb: 0.7,
1805
+ crouch: 1,
1806
+ aim: 0,
1807
+ turn: 0,
1808
+ grip: 0.8
1809
+ },
1810
+ aim: {
1811
+ gait: 0,
1812
+ freq: 0,
1813
+ amp: 0,
1814
+ knee: 0,
1815
+ bob: 0,
1816
+ lean: 0.06,
1817
+ arm: 0,
1818
+ elb: 0.2,
1819
+ crouch: 0.25,
1820
+ aim: 1,
1821
+ turn: 0,
1822
+ grip: 1.2
1823
+ },
1824
+ turn: {
1825
+ gait: 0.5,
1826
+ freq: 4.2,
1827
+ amp: 0.14,
1828
+ knee: 0.7,
1829
+ bob: 0.012,
1830
+ lean: 0.03,
1831
+ arm: 0.12,
1832
+ elb: 0.25,
1833
+ crouch: 0,
1834
+ aim: 0,
1835
+ turn: 1,
1836
+ grip: 0.4
1837
+ }
1838
+ },
1839
+ poses: [
1840
+ {
1841
+ index: 0,
1842
+ id: "walk",
1843
+ name: {
1844
+ "pt-BR": "anda"
1845
+ }
1846
+ },
1847
+ {
1848
+ index: 1,
1849
+ id: "crawl",
1850
+ name: {
1851
+ "pt-BR": "rasteja"
1852
+ }
1853
+ },
1854
+ {
1855
+ index: 2,
1856
+ id: "fallen",
1857
+ name: {
1858
+ "pt-BR": "ca\xEDdo"
1859
+ }
1860
+ },
1861
+ {
1862
+ index: 3,
1863
+ id: "attack",
1864
+ name: {
1865
+ "pt-BR": "ataca"
1866
+ }
1867
+ },
1868
+ {
1869
+ index: 4,
1870
+ id: "scream",
1871
+ name: {
1872
+ "pt-BR": "grito"
1873
+ }
1874
+ },
1875
+ {
1876
+ index: 5,
1877
+ id: "crouch",
1878
+ name: {
1879
+ "pt-BR": "agachado"
1880
+ }
1881
+ },
1882
+ {
1883
+ index: 6,
1884
+ id: "leap",
1885
+ name: {
1886
+ "pt-BR": "salto"
1887
+ }
1888
+ },
1889
+ {
1890
+ index: 7,
1891
+ id: "eat",
1892
+ name: {
1893
+ "pt-BR": "comendo"
1894
+ }
1895
+ }
1896
+ ],
1897
+ walk: {
1898
+ cadenceBase: 0.5,
1899
+ cadencePerSpeed: 0.3,
1900
+ stopBelowSpeed: 0.05,
1901
+ maxSine: 0.9,
1902
+ strideFactor: 4,
1903
+ legLengthFactor: 0.9
1904
+ }
1905
+ };
1906
+
1907
+ // src/gait/stride.ts
1908
+ var WALK = gait_default.walk;
1909
+ var walkCadence = (speed) => WALK.cadenceBase + WALK.cadencePerSpeed * speed;
1910
+ var walkGait = (speed, height = 1) => speed < WALK.stopBelowSpeed ? 0 : 2 * Math.asin(Math.min(WALK.maxSine, speed / (WALK.strideFactor * WALK.legLengthFactor * height * walkCadence(speed))));
1911
+ var SWING_SHARE = 0.5;
1912
+ var KNEE_PER_GAIT = 0.85;
1913
+ var strideFor = ({ speed, height = 1, strideFromSpeed = true }, fallback) => {
1914
+ if (!strideFromSpeed) return fallback;
1915
+ const amplitude = walkGait(speed, height);
1916
+ return { frequency: walkCadence(speed), swing: amplitude * SWING_SHARE, kneeBend: amplitude * KNEE_PER_GAIT };
1917
+ };
1918
+
1919
+ // src/gait/profile.ts
1920
+ var emptySample = () => ({ upper: {}, head: {}, arms: [{}, {}], legs: [{}, {}] });
1921
+ var CASCADE = gait_default.cascade;
1922
+ var INSTANCE_POSES = ["walk", "crawl", "fallen", "attack", "scream", "crouch", "leap", "eat"];
1923
+ var MIN_HEIGHT = -2;
1924
+ var MAX_HEIGHT = 8;
1925
+ var HEIGHT_STEPS = 1e3;
1926
+ var POSE_SLOTS = 16;
1927
+ var encodePoseHeight = (pose, height = 0) => INSTANCE_POSES.indexOf(pose) + POSE_SLOTS * Math.round((Math.min(MAX_HEIGHT, Math.max(MIN_HEIGHT, height)) - MIN_HEIGHT) * HEIGHT_STEPS);
1928
+ var decodePoseHeight = (encoded) => {
1929
+ const height = Math.floor(encoded / POSE_SLOTS) / HEIGHT_STEPS + MIN_HEIGHT;
1930
+ return { pose: INSTANCE_POSES[encoded - Math.floor(encoded / POSE_SLOTS) * POSE_SLOTS], height };
1931
+ };
1932
+
1933
+ // src/gait/animate.ts
1934
+ var TWO_PI = Math.PI * 2;
1935
+ var LEAP_AIR_SHARE = 0.38;
1936
+ var LEAP_PHASE_DIVISOR = 6.3;
1937
+ var DEFAULT_PROFILE = { f: 1.1, a: 0.28, k: 0.35, drag: true, arm: 0.08 };
1938
+ var leap = ({ rest, time, phase, profile }) => {
1939
+ const sample = emptySample();
1940
+ const cycle = (time * profile.f + phase / LEAP_PHASE_DIVISOR) % 1;
1941
+ const air = cycle < LEAP_AIR_SHARE ? Math.sin(cycle / LEAP_AIR_SHARE * Math.PI) : 0;
1942
+ sample.hipY = rest.hipY + air * 0.95;
1943
+ sample.upper.x = rest.lean - air * 0.45;
1944
+ rest.legs.forEach((leg, index) => sample.legs[index] = { hip: leg.hip * (1 - air * 0.7), knee: leg.knee * (1 - air * 0.75) });
1945
+ rest.arms.forEach((arm, index) => sample.arms[index] = { shoulder: arm.shoulder - air * 2.3, elbow: arm.elbow * (1 - air) });
1946
+ return sample;
1947
+ };
1948
+ var animateProfile = ({ profile = DEFAULT_PROFILE, rest, time, phase, stride }) => {
1949
+ const own = { frequency: profile.f, swing: profile.a ?? 0, kneeBend: profile.k ?? 0 };
1950
+ const { frequency, swing, kneeBend } = stride ? strideFor(stride, own) : own;
1951
+ const angle = time * frequency * TWO_PI + phase;
1952
+ const sine = Math.sin(angle);
1953
+ const cosine = Math.cos(angle);
1954
+ const sample = emptySample();
1955
+ if (profile.leap) return leap({ rest, time, phase, profile });
1956
+ if (profile.listen) {
1957
+ sample.head.y = Math.sin(time * 0.7 + phase) * 0.6;
1958
+ rest.arms.forEach((arm, index) => sample.arms[index] = { shoulder: arm.shoulder + Math.sin(time * 0.9 + index) * 0.08 });
1959
+ return sample;
1960
+ }
1961
+ if (profile.eat) {
1962
+ const wave = Math.sin(time * profile.f * TWO_PI + phase);
1963
+ sample.head.x = rest.headX + Math.max(0, wave) * 0.35;
1964
+ sample.upper.x = rest.lean + Math.max(0, wave) * 0.08;
1965
+ rest.arms.forEach((arm, index) => sample.arms[index] = { elbow: arm.elbow + wave * 0.2 });
1966
+ return sample;
1967
+ }
1968
+ if (profile.crawl) {
1969
+ rest.arms.forEach((arm, index) => {
1970
+ const wave = index ? sine : -sine;
1971
+ sample.arms[index] = { shoulder: arm.shoulder + wave * 0.35, elbow: arm.elbow - Math.max(0, wave) * 0.6 };
1972
+ });
1973
+ sample.upper.z = sine * 0.06;
1974
+ return sample;
1975
+ }
1976
+ if (profile.idle) {
1977
+ const scare = Math.max(0, Math.sin(time * 0.9 + phase));
1978
+ sample.upper.x = rest.lean - scare * 0.18;
1979
+ sample.head.x = rest.headX - scare * 0.25;
1980
+ rest.arms.forEach((arm, index) => sample.arms[index] = { shoulder: arm.shoulder + scare * 0.25 });
1981
+ return sample;
1982
+ }
1983
+ rest.legs.forEach((leg, index) => {
1984
+ if (leg.lost) return;
1985
+ const wave = index ? -sine : sine;
1986
+ if (profile.drag && index === 1) return void (sample.legs[index] = { hip: leg.hip + wave * swing * 0.35, knee: leg.knee });
1987
+ sample.legs[index] = { hip: leg.hip + wave * swing, knee: leg.knee + Math.max(0, index ? cosine : -cosine) * kneeBend };
1988
+ });
1989
+ rest.arms.forEach((arm, index) => sample.arms[index] = { shoulder: arm.shoulder + (index ? sine : -sine) * (profile.arm || 0.1) });
1990
+ sample.hipY = rest.hipY - Math.abs(cosine) * (profile.bob || 0.02);
1991
+ sample.upper.y = sine * 0.06;
1992
+ sample.upper.z = (profile.sway || 0) * sine + (profile.drag ? 0.04 : 0);
1993
+ if (profile.stalk) sample.upper.x = rest.lean + sine * 0.04;
1994
+ if (profile.sniff) sample.head.y = Math.sin(time * 3.1 + phase) * 0.35;
1995
+ return sample;
1996
+ };
1997
+
1998
+ // src/gait/cascade.ts
1999
+ var stepCascade = (current, target, dt, joint, rates = CASCADE) => current + (target - current) * Math.min(1, dt * rates[joint]);
2000
+ var lerpPose = (from, to, amount) => {
2001
+ const result = {};
2002
+ for (const name of Object.keys(to)) {
2003
+ const start = from[name];
2004
+ const end = to[name];
2005
+ result[name] = start === void 0 ? end : start + (end - start) * amount;
2006
+ }
2007
+ return result;
2008
+ };
2009
+
2010
+ // src/gait/moves.ts
2011
+ var MOVES = gait_default.moves;
2012
+ var MOVE_BLEND_RATE = 4;
2013
+ var moveBlendOf = (move) => ({ ...MOVES[move] });
2014
+ var blendMoves = (current, target, dt) => {
2015
+ const goal = typeof target === "string" ? MOVES[target] : target;
2016
+ const amount = Math.min(1, dt * MOVE_BLEND_RATE);
2017
+ for (const key of Object.keys(goal)) current[key] += (goal[key] - current[key]) * amount;
2018
+ return current;
2019
+ };
2020
+ var REANIMATED_STAGE = 4;
2021
+ var TREMOR_STAGE = 3;
2022
+ var humanPose = (params) => {
2023
+ const { moves, time, phase, cycle, hunch, armRaise, stage, reduced = false, skirt = false } = params;
2024
+ const zombie = stage === REANIMATED_STAGE;
2025
+ const walking = reduced ? 0 : moves.gait;
2026
+ const phased = phase + time;
2027
+ const shift = reduced ? 0 : Math.sin(time * 0.45 + phase) * (1 - walking) * (1 - moves.aim);
2028
+ const upperX = hunch + moves.crouch * 0.15 + (stage >= TREMOR_STAGE && !reduced ? Math.sin(phased * 2.3) * 0.03 : 0);
2029
+ const upperY = walking * Math.sin(cycle - 0.35) * (0.08 + moves.arm * 0.12) - moves.aim * 0.35 + moves.turn * 0.25;
2030
+ const upperZ = -shift * 0.025 + walking * Math.sin(cycle - 0.35) * 0.02 + (zombie ? 0.05 + Math.sin(cycle * 0.5) * 0.06 * walking : 0);
2031
+ const legs = params.legSides.map((side, index) => {
2032
+ const legCycle = cycle + (index ? Math.PI : 0);
2033
+ const amplitude = moves.amp * (zombie && index === 0 ? 0.45 : 1) * (skirt ? 0.75 : 1);
2034
+ return {
2035
+ hip: -walking * Math.sin(legCycle) * amplitude - moves.crouch * 0.95 + (zombie && !walking ? index ? -0.1 : 0.08 : 0),
2036
+ knee: walking * (Math.max(0, Math.cos(legCycle)) * moves.knee * (zombie && index === 0 ? 0.3 : 1) + 0.06) + moves.crouch * 1.55 + (1 - walking) * (0.03 + Math.max(0, shift * (index ? 1 : -1)) * 0.18),
2037
+ hipZ: side * (0.02 + moves.aim * 0.1 + moves.crouch * 0.06)
2038
+ };
2039
+ });
2040
+ const arms = params.arms.map(({ side, hold = false }, index) => {
2041
+ const raise = armRaise * (hold && !zombie ? 0.2 : 1);
2042
+ const swing = cycle - 0.25;
2043
+ const shoulderX2 = (hold ? -0.2 : 0) - raise * (zombie ? 1.12 : 1.3) - side * Math.sin(swing) * moves.arm * walking * (zombie ? 0.3 : 1) + (reduced || walking ? 0 : Math.sin(phased * 1.6 + side) * 0.04) + (zombie && !reduced ? Math.sin(phased * 3 + side * 2) * 0.08 : 0);
2044
+ const aimX = index === 0 ? -1.45 : -1.32;
2045
+ return {
2046
+ shoulder: shoulderX2 * (1 - moves.aim) + aimX * moves.aim,
2047
+ shoulderZ: side * (0.07 + (zombie ? 0.1 : 0)) * (1 - moves.aim) - side * 0.32 * moves.aim,
2048
+ elbow: zombie ? -0.18 : -((moves.elb + (hold ? 0.25 : 0) + walking * Math.max(0, side * Math.sin(swing - 0.3)) * 0.3) * (1 - moves.aim) + (index === 0 ? 0.15 : 0.55) * moves.aim),
2049
+ wrist: reduced ? 0 : -walking * Math.sin(swing - 0.6) * 0.15 * side * (1 - moves.aim),
2050
+ grip: (hold ? 1.25 : moves.grip) * (zombie ? 0.5 : 1)
2051
+ };
2052
+ });
2053
+ const headY = (reduced ? 0 : Math.sin(phased * 0.5) * 0.25 * (1 - walking) * (1 - moves.aim) * (zombie ? 0.3 : 1)) - upperY * 0.75 * (1 - moves.turn) + moves.turn * 0.45 + moves.aim * 0.3;
2054
+ return {
2055
+ chestScaleY: 1 + (reduced ? 0 : Math.sin(phased * (1.6 + moves.freq * 0.12)) * (0.012 + moves.freq * 12e-4)),
2056
+ hip: { x: shift * 0.025 + walking * Math.sin(cycle) * 0.018, y: -walking * moves.bob * Math.abs(Math.sin(cycle)) - moves.crouch * 0.26, zRotation: shift * 0.02 - walking * Math.sin(cycle) * 0.022 },
2057
+ upper: { x: upperX, y: upperY, z: upperZ },
2058
+ head: { x: -hunch * 0.6 - moves.crouch * 0.12 + (zombie ? 0.15 : 0) + walking * Math.sin(cycle * 2 - 0.6) * 0.02, y: headY, z: (zombie ? 0.22 : 0) - upperZ * 0.6 },
2059
+ legs,
2060
+ arms
2061
+ };
2062
+ };
2063
+ var LIMP = { swing: 0.3, knee: 0.05, lean: 0.07 };
2064
+
2065
+ // src/gait/lying.ts
2066
+ var CRAWL_TILT_RATE = 4;
2067
+ var DEAD_TILT_RATE = 3;
2068
+ var crawlPose = (time, phase, sides) => {
2069
+ const cycle = time * 2.2 + phase;
2070
+ const wave = Math.sin(cycle);
2071
+ return {
2072
+ figTilt: 1.38,
2073
+ hipY: 0.2 + Math.abs(wave) * 0.02,
2074
+ hipZRotation: wave * 0.05,
2075
+ upper: { x: 0, y: wave * 0.12 },
2076
+ headX: -0.95,
2077
+ arms: sides.map((side) => ({ shoulder: -2.55 + side * wave * 0.45, shoulderZ: side * 0.25, elbow: -0.35 - Math.max(0, -side * wave) * 0.6 })),
2078
+ legs: [
2079
+ { hip: 0.1, knee: 0 },
2080
+ { hip: 0.12 + wave * 0.08, knee: 0.25 }
2081
+ ]
2082
+ };
2083
+ };
2084
+ var deadPose = (armSides) => ({
2085
+ figTilt: -1.45,
2086
+ hipY: 0.22,
2087
+ headY: 0.5,
2088
+ arms: armSides.map((side) => ({ shoulder: -0.2, shoulderZ: side * 1.1, elbow: -0.3 })),
2089
+ legs: [-0.15, 0.15].map((hipZ) => ({ hip: 0, hipZ, knee: 0.1 }))
2090
+ });
2091
+
2092
+ // src/condition/timeline.ts
2093
+ var phaseIndexAt = (phases, units) => {
2094
+ let found = 0;
2095
+ phases.forEach((phase, index) => {
2096
+ if (units >= phase.startsAt) found = index;
2097
+ });
2098
+ return found;
2099
+ };
2100
+ var mixEffects = (from, to, amount) => {
2101
+ const result = {};
2102
+ for (const name of /* @__PURE__ */ new Set([...Object.keys(from ?? {}), ...Object.keys(to ?? {})])) {
2103
+ const start = from?.[name] ?? 0;
2104
+ result[name] = start + ((to?.[name] ?? 0) - start) * amount;
2105
+ }
2106
+ return result;
2107
+ };
2108
+ var createConditionTimeline = (options) => {
2109
+ const { phases, unitsPerSecond = 1, blend = false } = options;
2110
+ if (phases.length === 0) throw new Error("createConditionTimeline: at least one phase is required");
2111
+ let units = 0;
2112
+ const sample = () => {
2113
+ const index = phaseIndexAt(phases, units);
2114
+ const phase = phases[index];
2115
+ const next = phases[index + 1];
2116
+ const progress = next ? Math.min(1, (units - phase.startsAt) / (next.startsAt - phase.startsAt)) : 1;
2117
+ const effects = blend && next ? mixEffects(phase.effects, next.effects, progress) : { ...phase.effects };
2118
+ return { index, phase, progress, effects, isFinal: !next };
2119
+ };
2120
+ return {
2121
+ get elapsed() {
2122
+ return units;
2123
+ },
2124
+ advance(seconds) {
2125
+ units += seconds * unitsPerSecond;
2126
+ return sample();
2127
+ },
2128
+ seek(target) {
2129
+ units = Math.max(0, target);
2130
+ return sample();
2131
+ },
2132
+ sample,
2133
+ reset() {
2134
+ units = 0;
2135
+ }
2136
+ };
2137
+ };
2138
+
2139
+ // src/data/appearance.json
2140
+ var appearance_default = {
2141
+ variance: {
2142
+ height: {
2143
+ gaussianSpread: 0.09,
2144
+ min: 0.9,
2145
+ max: 1.1
2146
+ },
2147
+ buildBands: [
2148
+ {
2149
+ below: 0.3,
2150
+ base: 0.85,
2151
+ range: 0.1
2152
+ },
2153
+ {
2154
+ below: 0.8,
2155
+ base: 0.95,
2156
+ range: 0.13
2157
+ },
2158
+ {
2159
+ base: 1.08,
2160
+ range: 0.17
2161
+ }
2162
+ ],
2163
+ buildSpeed: {
2164
+ lean: 1.05,
2165
+ heavy: 0.9
2166
+ },
2167
+ buildHealth: {
2168
+ lean: 0.9,
2169
+ heavy: 1.15
2170
+ },
2171
+ speedJitter: {
2172
+ base: 0.92,
2173
+ range: 0.16
2174
+ },
2175
+ wanderJitter: {
2176
+ base: 0.8,
2177
+ range: 0.4
2178
+ },
2179
+ runnerAgeClamp: 1,
2180
+ variantCount: {
2181
+ walker: 4,
2182
+ runner: 3,
2183
+ crawler: 2,
2184
+ brute: 2
2185
+ },
2186
+ variantSeedBase: 7e3
2187
+ },
2188
+ civilianColors: [
2189
+ "6D5A4A",
2190
+ "4D5D6B",
2191
+ "7A3B33",
2192
+ "5B6B4A",
2193
+ "8A7A5A",
2194
+ "3E4A5C",
2195
+ "6A4A6A",
2196
+ "9A8A70",
2197
+ "40403A"
2198
+ ],
2199
+ corpses: {
2200
+ max: 400,
2201
+ lifeSeconds: 900
2202
+ }
2203
+ };
2204
+
2205
+ // src/appearance/variance.ts
2206
+ var { variance } = appearance_default;
2207
+ var clamp = (value, low, high) => Math.min(high, Math.max(low, value));
2208
+ var drawBodyVariance = (random, scale = {}) => {
2209
+ const spread = (random() + random() + random() - 1.5) * variance.height.gaussianSpread;
2210
+ const height = clamp(1 + spread, variance.height.min, variance.height.max) * (scale.height ?? 1);
2211
+ const band = random();
2212
+ const bands = variance.buildBands;
2213
+ const found = bands.find((candidate) => candidate.below !== void 0 && band < candidate.below) ?? bands[bands.length - 1];
2214
+ const { base, range } = found;
2215
+ const build = (base + random() * range) * (scale.build ?? 1);
2216
+ const buildClass = band < 0.3 ? "lean" : band >= 0.8 ? "heavy" : "average";
2217
+ const speedBuild = buildClass === "lean" ? variance.buildSpeed.lean : buildClass === "heavy" ? variance.buildSpeed.heavy : 1;
2218
+ const healthBuild = buildClass === "lean" ? variance.buildHealth.lean : buildClass === "heavy" ? variance.buildHealth.heavy : 1;
2219
+ return {
2220
+ height,
2221
+ build,
2222
+ buildClass,
2223
+ speedFactor: speedBuild * (variance.speedJitter.base + random() * variance.speedJitter.range),
2224
+ wanderFactor: variance.wanderJitter.base + random() * variance.wanderJitter.range,
2225
+ healthFactor: healthBuild
2226
+ };
2227
+ };
2228
+ var variantCountFor = (typeId) => variance.variantCount[typeId] ?? 1;
2229
+ var variantSeed = (typeIndex, variant) => variance.variantSeedBase + typeIndex * 101 + variant * 13;
2230
+ var variantDecay = (variant) => variant % 2 === 0 ? 1 : 2;
2231
+
2232
+ // src/appearance/palette.ts
2233
+ var palette_exports = {};
2234
+ __export(palette_exports, {
2235
+ BAG_COLORS: () => BAG_COLORS,
2236
+ BLOOD_COLORS: () => BLOOD_COLORS,
2237
+ CAP_COLORS: () => CAP_COLORS,
2238
+ DECAY_SKIN: () => DECAY_SKIN,
2239
+ GRAY: () => GRAY,
2240
+ HAIR_COLORS: () => HAIR_COLORS,
2241
+ HAIR_STYLES_FEMALE: () => HAIR_STYLES_FEMALE,
2242
+ HAIR_STYLES_MALE: () => HAIR_STYLES_MALE,
2243
+ OLD_HAIR: () => OLD_HAIR,
2244
+ PANTS_COLORS: () => PANTS_COLORS,
2245
+ RED: () => RED,
2246
+ SHOE_COLORS: () => SHOE_COLORS,
2247
+ SKIN_DECAY_BLEND: () => SKIN_DECAY_BLEND,
2248
+ SKIN_TONES: () => SKIN_TONES,
2249
+ SLEEVES: () => SLEEVES,
2250
+ TOP_COLORS: () => TOP_COLORS,
2251
+ UNIFORM_PANTS: () => UNIFORM_PANTS,
2252
+ UNIFORM_SHIRTS: () => UNIFORM_SHIRTS,
2253
+ UNIFORM_SHOES: () => UNIFORM_SHOES
2254
+ });
2255
+ var SKIN_TONES = [15845797, 14724224, 13012066, 11039050, 9067066, 6175526];
2256
+ var TOP_COLORS = [4878230, 9321275, 6113850, 5208643, 7040888, 4153146, 5917294, 9071162, 12104354, 2894896, 10111530, 3104099, 13216074, 8007242, 14210248, 3820138];
2257
+ var PANTS_COLORS = [2896704, 2040360, 4866096, 5922662, 3820138, 6969920, 2767402];
2258
+ var SLEEVES = ["short", "short", "long", "tank"];
2259
+ var HAIR_STYLES_FEMALE = ["long", "long", "bun", "short", "afro", "bald"];
2260
+ var HAIR_STYLES_MALE = ["short", "short", "buzz", "bald", "afro", "long"];
2261
+ var CAP_COLORS = [9321275, 2894896, 3820138, 13216074];
2262
+ var BAG_COLORS = [3820090, 5909028, 2896704];
2263
+ var HAIR_COLORS = [1709072, 2760214, 3877920, 6965802, 11569744, 8006170];
2264
+ var SHOE_COLORS = [1908514, 3811866, 12104872, 4856346];
2265
+ var DECAY_SKIN = [13482920, 9740420, 8090698, 9206878];
2266
+ var UNIFORM_SHIRTS = [4878230, 9321275, 6113850, 5208643, 7040888, 4153146, 5917294, 9071162];
2267
+ var UNIFORM_PANTS = 2896704;
2268
+ var UNIFORM_SHOES = 1908514;
2269
+ var SKIN_DECAY_BLEND = [0.45, 0.65, 0.8, 0.88];
2270
+ var GRAY = 4868678;
2271
+ var RED = 8000012;
2272
+ var BLOOD_COLORS = [10361618, 7214096, 4860440, 1840658];
2273
+ var OLD_HAIR = 9078656;
2274
+
2275
+ // src/appearance/traits.ts
2276
+ var BLOOD_SEED_OFFSET = 99;
2277
+ var createBloodStream = (seed) => createRng(seed + BLOOD_SEED_OFFSET);
2278
+ var FEMALE_SHARE = 0.42;
2279
+ var BEARD_SHARE = 0.25;
2280
+ var GLASSES_SHARE = 0.12;
2281
+ var CAP_SHARE = 0.12;
2282
+ var BAG_SHARE = 0.15;
2283
+ var TORN_SHARE = 0.35;
2284
+ var APRON_SHARE = 0.05;
2285
+ var drawAppearance = (params) => {
2286
+ const { seed, outfit = {}, uniform = false, typeIndex = 0, human = false, life = 1 } = params;
2287
+ const random = createRng(seed);
2288
+ const pick = (options) => options[Math.floor(random() * options.length)];
2289
+ const female = !uniform && (params.allowFemale ?? true) && (params.female ?? random() < FEMALE_SHARE);
2290
+ const tone = params.tone ?? (uniform ? DECAY_SKIN[0] : pick(SKIN_TONES));
2291
+ const topColor = outfit.cloth ?? (uniform ? UNIFORM_SHIRTS[(params.clothIndex ?? typeIndex) % UNIFORM_SHIRTS.length] : pick(TOP_COLORS));
2292
+ const pantsColor = outfit.pants ?? (uniform ? UNIFORM_PANTS : pick(PANTS_COLORS));
2293
+ const look = uniform ? uniformLook() : drawLook({ random, pick, female, outfit, human, params });
2294
+ const hairColor = params.hairColor ?? (life === 2 ? OLD_HAIR : pick(HAIR_COLORS));
2295
+ const shoeColor = uniform ? UNIFORM_SHOES : pick(SHOE_COLORS);
2296
+ return { traits: { female, tone, topColor, pantsColor, ...look, hairColor, shoeColor }, random };
2297
+ };
2298
+ var uniformLook = () => ({
2299
+ sleeve: "short",
2300
+ hair: "short",
2301
+ headWidth: 1,
2302
+ headHeight: 1,
2303
+ jaw: 1,
2304
+ nose: 1,
2305
+ beard: false,
2306
+ glasses: false,
2307
+ cap: false,
2308
+ capColor: 0,
2309
+ bag: false,
2310
+ bagColor: 0,
2311
+ torn: false,
2312
+ apron: false
2313
+ });
2314
+ var drawLook = ({ random, pick, female, outfit, human, params }) => {
2315
+ const sleeve = outfit.cloth ? "long" : pick(SLEEVES);
2316
+ const hair = params.hairStyle ?? (params.wet ? "long" : params.charred ? "bald" : pick(female ? HAIR_STYLES_FEMALE : HAIR_STYLES_MALE));
2317
+ const headWidth = 1 + (random() - 0.5) * 0.16;
2318
+ const headHeight = 1 + (random() - 0.5) * 0.14;
2319
+ const jaw = 1 + (random() - 0.5) * 0.3;
2320
+ const nose = 1 + (random() - 0.5) * 0.5;
2321
+ const beard = !female && random() < BEARD_SHARE;
2322
+ const glasses = random() < GLASSES_SHARE;
2323
+ const cap = !outfit.hat && random() < CAP_SHARE;
2324
+ const capColor = pick(CAP_COLORS);
2325
+ const bag = random() < BAG_SHARE && !human;
2326
+ const bagColor = pick(BAG_COLORS);
2327
+ const torn = random() < TORN_SHARE && !human;
2328
+ const apron = random() < APRON_SHARE;
2329
+ return { sleeve, hair, headWidth, headHeight, jaw, nose, beard, glasses, cap, capColor, bag, bagColor, torn, apron };
2330
+ };
2331
+ export {
2332
+ ARSENAL_RULES,
2333
+ palette_exports as AppearancePalette,
2334
+ BLOOD_LITERS_PER_DROP,
2335
+ BLOOD_SEED_OFFSET,
2336
+ BODY_MATERIAL,
2337
+ BODY_REGIONS,
2338
+ CASCADE,
2339
+ CRAWL_TILT_RATE,
2340
+ DAMAGE_PRESETS,
2341
+ DEAD_TILT_RATE,
2342
+ DEFAULT_CORPSES,
2343
+ DEFAULT_DAMAGE_RULES,
2344
+ ELENCO_RULES,
2345
+ GAME_BONES,
2346
+ GUIDE,
2347
+ INSTANCE_POSES,
2348
+ JOINT_IDS,
2349
+ JOINT_PARENTS,
2350
+ LAYERED_PROTECTION_CAP,
2351
+ LIMB_REGIONS,
2352
+ LIMP,
2353
+ MOVES,
2354
+ MOVE_BLEND_RATE,
2355
+ PENETRATION_MODELS,
2356
+ PERSON_PRESET,
2357
+ PHYSICAL_PENETRATION,
2358
+ PROTECTION_PRESETS,
2359
+ REGION_HIT_POINTS,
2360
+ REGION_SEGMENTS,
2361
+ SEGMENT_IDS,
2362
+ SEGMENT_NUMBERS,
2363
+ SEGMENT_TREE,
2364
+ TEAM_GAME,
2365
+ TEAM_GAME_CALIBERS,
2366
+ TEAM_GAME_CALIBER_KEYS,
2367
+ TEAM_GAME_PENETRATION,
2368
+ TORSO_RESISTANCE,
2369
+ animateProfile,
2370
+ applyPatches,
2371
+ applyPreset,
2372
+ bleedRateAt,
2373
+ blendMoves,
2374
+ crawlPose,
2375
+ createBloodStream,
2376
+ createBodySpec,
2377
+ createCharacters,
2378
+ createConditionTimeline,
2379
+ createCorpseLedger,
2380
+ createRig,
2381
+ createRng,
2382
+ cutOf,
2383
+ deadPose,
2384
+ decodePoseHeight,
2385
+ describeBody,
2386
+ drawAppearance,
2387
+ drawBodyVariance,
2388
+ emptySample,
2389
+ encodePoseHeight,
2390
+ energyKept,
2391
+ humanPose,
2392
+ isBodyRegion,
2393
+ isSegmentId,
2394
+ layeredProtection,
2395
+ layersOn,
2396
+ lerpPose,
2397
+ mergeById,
2398
+ moveBlendOf,
2399
+ penetrateLine,
2400
+ penetrationForce,
2401
+ pointsFromEnergy,
2402
+ projectileOf,
2403
+ protect,
2404
+ removeIds,
2405
+ resolveDamageRules,
2406
+ resolvePoints,
2407
+ segmentNode,
2408
+ segmentResistance,
2409
+ segmentsBelow,
2410
+ spreadPoints,
2411
+ stepCascade,
2412
+ strideFor,
2413
+ teamGameCost,
2414
+ teamGameFalloff,
2415
+ variantCountFor,
2416
+ variantDecay,
2417
+ variantSeed,
2418
+ walkCadence,
2419
+ walkGait
2420
+ };
2421
+ //# sourceMappingURL=index.js.map