@adatechnology/game-destruction 0.6.1 → 0.7.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.
@@ -22,11 +22,16 @@ var GameDestruction = (() => {
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  ASSEMBLIES: () => ASSEMBLIES,
25
+ CONFINEMENT_DEFAULTS: () => CONFINEMENT_DEFAULTS,
25
26
  FIRE_PRIME_C: () => FIRE_PRIME_C,
27
+ FRAGMENT_HIT_DEFAULTS: () => FRAGMENT_HIT_DEFAULTS,
26
28
  IMPACT_CONTRACT: () => IMPACT_CONTRACT,
27
29
  JOINT_BY_MATERIALS: () => JOINT_BY_MATERIALS,
28
30
  JOINT_DEFAULTS: () => JOINT_DEFAULTS,
29
31
  MATERIALS: () => MATERIALS,
32
+ PIERCING_BY_CALIBER: () => PIERCING_BY_CALIBER,
33
+ PIERCING_BY_KIND: () => PIERCING_BY_KIND,
34
+ SHELTER_DEFAULTS: () => SHELTER_DEFAULTS,
30
35
  assemblyDepth: () => assemblyDepth,
31
36
  assemblyPart: () => assemblyPart,
32
37
  buildFrame: () => buildFrame,
@@ -43,6 +48,7 @@ var GameDestruction = (() => {
43
48
  partMass: () => partMass,
44
49
  partThickness: () => partThickness,
45
50
  partVolume: () => partVolume,
51
+ piercingOf: () => piercingOf,
46
52
  quat: () => quat,
47
53
  quatFromEuler: () => quatFromEuler,
48
54
  registerAssembly: () => registerAssembly,
@@ -892,7 +898,8 @@ var GameDestruction = (() => {
892
898
  0.25
893
899
  ],
894
900
  damageScale: {
895
- blunt: 1.5
901
+ blunt: 1.5,
902
+ shelter: 3
896
903
  },
897
904
  hitDebris: "shards",
898
905
  breakDebris: "shards",
@@ -943,7 +950,10 @@ var GameDestruction = (() => {
943
950
  "glass",
944
951
  "brittle"
945
952
  ],
946
- compressive: 2e5
953
+ compressive: 2e5,
954
+ damageScale: {
955
+ shelter: 3
956
+ }
947
957
  },
948
958
  {
949
959
  id: "ceramic",
@@ -1629,10 +1639,14 @@ var GameDestruction = (() => {
1629
1639
  // src/world.ts
1630
1640
  var DMG_PER_J = 0.02;
1631
1641
  var BLAST_COUPLING = 0.1;
1642
+ var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
1643
+ var FRAGMENT_HIT_DEFAULTS = { max: 24, energyFactor: 0.5, hitType: "blunt", range: 6, flightTime: 0.4, spread: 10, aim: 40 };
1644
+ var CONFINEMENT_DEFAULTS = { rays: 26, reach: 1.5, gain: 1.5, power: 2, max: 3 };
1632
1645
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1633
1646
  var BREACH_RADIUS = 0.55;
1634
1647
  var BREACH_BLAST_FACTOR = 6;
1635
1648
  var BREACH_AREA = 0.25;
1649
+ var BREACH_BLAST_REACH = Infinity;
1636
1650
  var RESIDUAL_CAPACITY = 0.15;
1637
1651
  var UNIT_FRAGMENT_FACTOR = 4;
1638
1652
  var BREACH_GRID_CAP = 4096;
@@ -1667,6 +1681,7 @@ var GameDestruction = (() => {
1667
1681
  const fireSpread = options.fireSpread ?? 0.6;
1668
1682
  const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1669
1683
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1684
+ const breachBlastReach = options.breach?.blastReach ?? BREACH_BLAST_REACH;
1670
1685
  const rng = createRng(options.seed ?? 1);
1671
1686
  const rnd = () => rng.next();
1672
1687
  let time = 0;
@@ -2022,7 +2037,7 @@ var GameDestruction = (() => {
2022
2037
  if (disc < 0) return true;
2023
2038
  return -b - Math.sqrt(disc) > maxDist;
2024
2039
  };
2025
- const raycastInternal = (origin, dir, maxDist, ignore) => {
2040
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
2026
2041
  const d = vec.normalize(dir);
2027
2042
  let best = null;
2028
2043
  for (const o of objects.values()) {
@@ -2035,7 +2050,7 @@ var GameDestruction = (() => {
2035
2050
  if (!best || r.t0 < best.distance) best = { object: o.id, part: p.spec.id, point: vec.madd(origin, d, r.t0), normal: r.n, distance: r.t0, pathLen: r.t1 - r.t0 };
2036
2051
  }
2037
2052
  }
2038
- for (const im of impls.values()) {
2053
+ for (const im of worldOnly ? [] : impls.values()) {
2039
2054
  for (const ps of im.impl.describe().parts) {
2040
2055
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
2041
2056
  const ov = im.poses.get(ps.id);
@@ -2448,7 +2463,7 @@ var GameDestruction = (() => {
2448
2463
  else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2449
2464
  return done;
2450
2465
  };
2451
- const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2466
+ const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, piercing = 1) => {
2452
2467
  const mat = p.mat;
2453
2468
  if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2454
2469
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
@@ -2461,7 +2476,9 @@ var GameDestruction = (() => {
2461
2476
  const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2462
2477
  const coats = p.layers.length > 0 ? layersEnergy(p.layers, p.rt.spalls, face, objLocal(o, entry)) + layersEnergy(p.layers, p.rt.spalls, -face, exitLocal) : 0;
2463
2478
  const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2464
- const penetrated = E > thr;
2479
+ const hard = mat.tags?.includes("metal") || mat.tags?.includes("masonry");
2480
+ const Ep = piercing === 1 || !hard ? E : E * piercing;
2481
+ const penetrated = Ep > thr;
2465
2482
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2466
2483
  const id = p.spec.id;
2467
2484
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
@@ -2473,7 +2490,7 @@ var GameDestruction = (() => {
2473
2490
  }
2474
2491
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2475
2492
  const brittle = mat.ductility < 0.3;
2476
- const ratio = E / Math.max(thr, 1e-6);
2493
+ const ratio = Ep / Math.max(thr, 1e-6);
2477
2494
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2478
2495
  let exitPoint;
2479
2496
  if (penetrated) {
@@ -2503,7 +2520,7 @@ var GameDestruction = (() => {
2503
2520
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2504
2521
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2505
2522
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2506
- return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2523
+ return { penetrated, exitEnergy: penetrated ? E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2507
2524
  };
2508
2525
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2509
2526
  const mat = p.mat;
@@ -2531,54 +2548,110 @@ var GameDestruction = (() => {
2531
2548
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2532
2549
  return done;
2533
2550
  };
2551
+ const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
2552
+ if (isLost(p)) return null;
2553
+ const { c, q } = partXf(o, p);
2554
+ const lp = toLocal(c, q, point);
2555
+ const dist = distanceToShape(p.spec.shape, lp);
2556
+ if (dist >= radius) return null;
2557
+ const sp = surfacePoint(p.spec.shape, lp);
2558
+ const wp = toWorld(c, q, sp.point);
2559
+ const reach = reachAt ? reachAt(wp, p) : 1;
2560
+ const f = (1 - dist / radius) ** 2 * reach;
2561
+ const Ein = energy * f * BLAST_COUPLING;
2562
+ const wn = rotateVec(q, sp.normal);
2563
+ if (!res.part) {
2564
+ res.part = p.spec.id;
2565
+ res.point = wp;
2566
+ }
2567
+ const mat = p.mat;
2568
+ if (Ein > 20) {
2569
+ if (mat.ductility >= 0.5) addDent(o, p, wp, wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), clamp2(4e-3 + Ein * 2e-5, 4e-3, 0.08), src);
2570
+ else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2571
+ debris(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2572
+ }
2573
+ const th = mat.thermal;
2574
+ if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2575
+ p.st.temperature += 300 * f;
2576
+ o.active = true;
2577
+ if (f >= 0.25 && !p.st.burning) {
2578
+ p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2579
+ checkIgnite(o, p, src);
2580
+ }
2581
+ }
2582
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2583
+ const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2584
+ if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2585
+ frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2586
+ return reach;
2587
+ }
2588
+ if (Ein > 20 && p.layers.length > 0) spallOuter(o, p, wp, faceOf(o, p, wn), wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), src);
2589
+ const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2590
+ res.damage += dealt;
2591
+ if (dist <= breachBlastReach * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2592
+ return reach;
2593
+ };
2534
2594
  const blastObject = (o, point, energy, radius, src) => {
2535
2595
  const start = cur.length;
2536
2596
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2537
- for (const p of o.parts) {
2538
- if (isLost(p)) continue;
2539
- const { c, q } = partXf(o, p);
2540
- const lp = toLocal(c, q, point);
2541
- const dist = distanceToShape(p.spec.shape, lp);
2542
- if (dist >= radius) continue;
2543
- const f = (1 - dist / radius) ** 2;
2544
- const Ein = energy * f * BLAST_COUPLING;
2545
- const sp = surfacePoint(p.spec.shape, lp);
2546
- const wp = toWorld(c, q, sp.point);
2547
- const wn = rotateVec(q, sp.normal);
2548
- if (!res.part) {
2549
- res.part = p.spec.id;
2550
- res.point = wp;
2551
- }
2552
- const mat = p.mat;
2553
- if (Ein > 20) {
2554
- if (mat.ductility >= 0.5) addDent(o, p, wp, wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), clamp2(4e-3 + Ein * 2e-5, 4e-3, 0.08), src);
2555
- else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2556
- debris(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2557
- }
2558
- const th = mat.thermal;
2559
- if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2560
- p.st.temperature += 300 * f;
2561
- o.active = true;
2562
- if (f >= 0.25 && !p.st.burning) {
2563
- p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2564
- checkIgnite(o, p, src);
2565
- }
2597
+ for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
2598
+ res.events = cur.slice(start);
2599
+ return res;
2600
+ };
2601
+ const blastCfg = (call) => {
2602
+ const pick = (key) => call && call[key] !== void 0 ? call[key] : options.blast?.[key];
2603
+ const merge = (v, defaults) => {
2604
+ if (!v) return null;
2605
+ const out = { ...defaults };
2606
+ if (v !== true) {
2607
+ for (const [key, value] of Object.entries(v)) if (value !== void 0) out[key] = value;
2566
2608
  }
2567
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2568
- const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2569
- if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2570
- frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2571
- continue;
2609
+ return out;
2610
+ };
2611
+ const conf = pick("confinement");
2612
+ return {
2613
+ shelter: merge(pick("shelter"), SHELTER_DEFAULTS),
2614
+ frag: merge(pick("fragmentHits"), FRAGMENT_HIT_DEFAULTS),
2615
+ conf: typeof conf === "number" ? Number.isFinite(conf) ? Math.max(0, conf) : 1 : merge(conf, CONFINEMENT_DEFAULTS)
2616
+ };
2617
+ };
2618
+ const confinementRays = /* @__PURE__ */ new Map();
2619
+ const raysOf = (n) => {
2620
+ let rays = confinementRays.get(n);
2621
+ if (rays) return rays;
2622
+ const r = createRng((options.seed ?? 1) ^ 1540483477);
2623
+ const off = r.next();
2624
+ const phi0 = r.next() * Math.PI * 2;
2625
+ rays = [];
2626
+ for (let i = 0; i < n; i++) {
2627
+ const y = 1 - 2 * (i + off) / n;
2628
+ const s = Math.sqrt(Math.max(0, 1 - y * y));
2629
+ const phi = phi0 + i * 2.399963229728653;
2630
+ rays.push([Math.cos(phi) * s, y, Math.sin(phi) * s]);
2631
+ }
2632
+ confinementRays.set(n, rays);
2633
+ return rays;
2634
+ };
2635
+ const confinementOf = (conf, point, radius) => {
2636
+ if (conf === null) return 1;
2637
+ if (typeof conf === "number") return conf;
2638
+ let hits = 0;
2639
+ let sum = 0;
2640
+ for (const d of raysOf(Math.max(1, Math.round(conf.rays)))) {
2641
+ const h = raycastInternal(point, d, radius * conf.reach, void 0, true);
2642
+ if (h) {
2643
+ hits++;
2644
+ sum += h.distance;
2572
2645
  }
2573
- if (Ein > 20 && p.layers.length > 0) spallOuter(o, p, wp, faceOf(o, p, wn), wn, clamp2(0.05 + Math.sqrt(Ein) * 2e-3, 0.05, 0.4), src);
2574
- const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2575
- res.damage += dealt;
2576
- addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2577
2646
  }
2578
- res.events = cur.slice(start);
2579
- return res;
2647
+ if (hits === 0) return 1;
2648
+ const closed = hits / Math.max(1, Math.round(conf.rays));
2649
+ const ratio = clamp2(radius / Math.max(sum / hits, 1e-3), 0, 2);
2650
+ return Math.min(Math.max(1, conf.max), 1 + conf.gain * closed ** conf.power * ratio);
2580
2651
  };
2581
- const blastWorld = (point, energy, radius, src) => {
2652
+ const FLAG_LOST = 1;
2653
+ const FLAG_OPEN = 2;
2654
+ const blastPlain = (point, energy, radius, src) => {
2582
2655
  const out = [];
2583
2656
  for (const o of [...objects.values()]) {
2584
2657
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2586,16 +2659,225 @@ var GameDestruction = (() => {
2586
2659
  if (r.part) out.push(r);
2587
2660
  }
2588
2661
  for (const im of [...impls.values()]) {
2589
- const inRange = im.impl.describe().parts.some((ps) => {
2590
- const ov = im.poses.get(ps.id);
2591
- const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2592
- const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2593
- return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2594
- });
2595
- if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2662
+ if (implInRange(im, point, radius)) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2596
2663
  }
2597
2664
  return out;
2598
2665
  };
2666
+ const implParts = (im) => im.impl.describe().parts.map((ps) => {
2667
+ const ov = im.poses.get(ps.id);
2668
+ return { ps, c: toWorld(im.position, im.rotation, ov?.pos ?? ps.position), q: quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0])) };
2669
+ });
2670
+ const implInRange = (im, point, radius) => implParts(im).some(({ ps, c, q }) => distanceToShape(ps.shape, toLocal(c, q, point)) < radius);
2671
+ const blastScene = (point, energy, radius, src, cfg, only) => {
2672
+ const mult = confinementOf(cfg.conf, point, radius);
2673
+ const E = energy * mult;
2674
+ const shelter = cfg.shelter;
2675
+ const flags = /* @__PURE__ */ new Map();
2676
+ const inRange = [];
2677
+ for (const o of objects.values()) {
2678
+ if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
2679
+ inRange.push(o);
2680
+ if (shelter) for (const p of o.parts) flags.set(p, (isLost(p) ? FLAG_LOST : 0) | (p.st.opening || p.st.stripped ? FLAG_OPEN : 0));
2681
+ }
2682
+ const shields = /* @__PURE__ */ new Map();
2683
+ const reachAt = (tp, self) => {
2684
+ if (!shelter) return 1;
2685
+ const L = vec.distance(point, tp);
2686
+ if (L < 1e-4) return 1;
2687
+ const d = vec.scale(vec.sub(tp, point), 1 / L);
2688
+ let t = 1;
2689
+ for (const o of inRange) {
2690
+ if (raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
2691
+ for (const p of o.parts) {
2692
+ if (p === self) continue;
2693
+ const was = flags.get(p) ?? 0;
2694
+ if (was & FLAG_LOST) continue;
2695
+ const r = rayPart(o, p, point, d);
2696
+ if (!r) continue;
2697
+ const t0 = Math.max(r.t0, 0);
2698
+ const path = Math.min(r.t1, L) - t0;
2699
+ if (path < shelter.minPath) continue;
2700
+ const mid = vec.madd(point, d, t0 + path / 2);
2701
+ const opening = p.st.opening && insideOpening(o, p, mid);
2702
+ if (opening && was & FLAG_OPEN) continue;
2703
+ if (p.st.stripped && was & FLAG_OPEN) continue;
2704
+ const resistance = p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1);
2705
+ let ti = Math.exp(-resistance / shelter.k);
2706
+ if (isLost(p) || opening || p.st.stripped && !(was & FLAG_OPEN)) ti += (1 - ti) * shelter.brokenTransmission;
2707
+ t *= ti;
2708
+ if (cfg.frag) {
2709
+ let list = shields.get(p);
2710
+ if (!list) shields.set(p, list = []);
2711
+ list.push(tp);
2712
+ }
2713
+ }
2714
+ }
2715
+ return t;
2716
+ };
2717
+ const items = [];
2718
+ let order = 0;
2719
+ for (const o of inRange) {
2720
+ if (only && only !== o) continue;
2721
+ for (const p of o.parts) {
2722
+ if (isLost(p)) continue;
2723
+ const dist = distToPart(o, p, point);
2724
+ if (dist < radius) items.push({ dist, order: order++, o, p });
2725
+ }
2726
+ }
2727
+ for (const im of impls.values()) {
2728
+ if (only && only !== im) continue;
2729
+ let dist = Infinity;
2730
+ for (const { ps, c, q } of implParts(im)) dist = Math.min(dist, distanceToShape(ps.shape, toLocal(c, q, point)));
2731
+ if (dist < radius) items.push({ dist, order: order++, im });
2732
+ }
2733
+ items.sort((a, b) => a.dist - b.dist || a.order - b.order);
2734
+ const results = /* @__PURE__ */ new Map();
2735
+ const reports = /* @__PURE__ */ new Map();
2736
+ const report = (key) => {
2737
+ let r = reports.get(key);
2738
+ if (!r) reports.set(key, r = { shelter: 0, confinement: mult, fragmentHits: 0 });
2739
+ return r;
2740
+ };
2741
+ const thrown = [];
2742
+ const sweep = (from) => {
2743
+ if (!cfg.frag) return;
2744
+ for (let i = from; i < cur.length; i++) {
2745
+ const e = cur[i];
2746
+ if (e.type !== "fracture" && e.type !== "stage") continue;
2747
+ for (const frag of e.fragments ?? []) thrown.push({ frag, object: e.object, part: objects.get(e.object)?.byId.get(e.part) ?? null });
2748
+ }
2749
+ };
2750
+ for (const it of items) {
2751
+ if (it.o && it.p) {
2752
+ const o = it.o;
2753
+ let res = results.get(o);
2754
+ if (!res) results.set(o, res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] });
2755
+ const start = cur.length;
2756
+ const k = blastPart(o, it.p, point, E, radius, src, res, shelter ? reachAt : void 0);
2757
+ res.events.push(...cur.slice(start));
2758
+ sweep(start);
2759
+ if (k !== null) {
2760
+ const rep = report(o);
2761
+ rep.shelter = Math.max(rep.shelter, k);
2762
+ if (shelter) (rep.parts ?? (rep.parts = {}))[it.p.spec.id] = k;
2763
+ }
2764
+ } else if (it.im) {
2765
+ const im = it.im;
2766
+ let num = 0;
2767
+ let den = 0;
2768
+ for (const { ps, c, q } of implParts(im)) {
2769
+ const lp = toLocal(c, q, point);
2770
+ const dist = distanceToShape(ps.shape, lp);
2771
+ if (dist >= radius) continue;
2772
+ const f = (1 - dist / radius) ** 2;
2773
+ const sp = surfacePoint(ps.shape, lp);
2774
+ num += f * reachAt(toWorld(c, q, sp.point), null);
2775
+ den += f;
2776
+ }
2777
+ const k = den > 0 ? num / den : 1;
2778
+ const res = implHit(im, { type: "explosive", point, energy: E * k, radius, ...shelter ? { shelter: k } : {}, ...src !== void 0 ? { source: src } : {} });
2779
+ results.set(im, res);
2780
+ report(im).shelter = k;
2781
+ }
2782
+ }
2783
+ if (cfg.frag && thrown.length > 0) {
2784
+ const g = cfg.frag;
2785
+ const energyOf = (f) => 0.5 * f.mass * (f.velocity[0] ** 2 + f.velocity[1] ** 2 + f.velocity[2] ** 2);
2786
+ const list = thrown.map((t, i) => ({ ...t, i, e: energyOf(t.frag) })).sort((a, b) => b.e - a.e || a.i - b.i).slice(0, Math.max(0, Math.floor(g.max)));
2787
+ list.forEach((t, i) => throwFragment(t.frag, t.object, t.part, g, point, i, src, shields, results, reports, mult));
2788
+ }
2789
+ const out = [];
2790
+ const reportShelter = (key, res) => {
2791
+ res.blast = reports.get(key) ?? { shelter: 1, confinement: mult, fragmentHits: 0 };
2792
+ };
2793
+ const summary = [];
2794
+ let fragmentHits = 0;
2795
+ for (const o of objects.values()) {
2796
+ const r = results.get(o);
2797
+ if (!r?.part) continue;
2798
+ reportShelter(o, r);
2799
+ out.push(r);
2800
+ for (const [part, factor] of Object.entries(r.blast?.parts ?? {})) summary.push({ object: o.id, part, factor });
2801
+ fragmentHits += r.blast?.fragmentHits ?? 0;
2802
+ }
2803
+ for (const im of impls.values()) {
2804
+ const r = results.get(im);
2805
+ if (!r) continue;
2806
+ reportShelter(im, r);
2807
+ out.push(r);
2808
+ if (shelter) summary.push({ object: im.impl.id, factor: r.blast?.shelter ?? 1 });
2809
+ fragmentHits += r.blast?.fragmentHits ?? 0;
2810
+ }
2811
+ emit({ type: "blast", object: "", time, ...src !== void 0 ? { source: src } : {}, point, energy: E, radius, confinement: mult, shelter: summary, fragmentHits });
2812
+ return out;
2813
+ };
2814
+ const seedOf = (...nums) => {
2815
+ let h = 2166136261;
2816
+ for (const n of nums) {
2817
+ h = Math.imul(h ^ (Math.round(n * 1e3) | 0), 16777619);
2818
+ h ^= h >>> 15;
2819
+ }
2820
+ return h | 0;
2821
+ };
2822
+ const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
2823
+ const v = frag.velocity;
2824
+ const speed = Math.hypot(v[0], v[1], v[2]);
2825
+ const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
2826
+ if (speed < 0.5 || energy < 1) return;
2827
+ const r = createRng(seedOf(options.seed ?? 1, centre[0], centre[1], centre[2], index, frag.center[0], frag.center[1], frag.center[2]));
2828
+ let dir = vec.scale(v, 1 / speed);
2829
+ const behind = from ? shields.get(from) : void 0;
2830
+ if (behind) {
2831
+ let best = Math.cos(g.aim * Math.PI / 180);
2832
+ for (const tp of behind) {
2833
+ const to = vec.sub(tp, frag.center);
2834
+ const len = vec.length(to);
2835
+ if (len < 1e-4) continue;
2836
+ const cos = vec.dot(dir, to) / len;
2837
+ if (cos >= best) {
2838
+ best = cos;
2839
+ dir = vec.scale(to, 1 / len);
2840
+ }
2841
+ }
2842
+ }
2843
+ const spread3 = Math.tan(g.spread * Math.PI / 180);
2844
+ dir = vec.normalize([dir[0] + (r.next() - 0.5) * 2 * spread3, dir[1] + (r.next() - 0.5) * 2 * spread3, dir[2] + (r.next() - 0.5) * 2 * spread3], dir);
2845
+ const range = Math.min(g.range, speed * g.flightTime);
2846
+ const origin = vec.madd(frag.center, dir, 0.02);
2847
+ const rh = raycastInternal(origin, dir, range, from ? /* @__PURE__ */ new Set([`${objectId}\0${from.spec.id}`]) : void 0);
2848
+ if (!rh) return;
2849
+ const hit = {
2850
+ type: g.hitType,
2851
+ point: rh.point,
2852
+ direction: dir,
2853
+ energy,
2854
+ radius: 0.05,
2855
+ part: rh.part,
2856
+ ...g.hitType === "ballistic" ? { caliber: clamp2(Math.max(...frag.size) * 1e3, 3, 30), mass: frag.mass * 1e3 } : {},
2857
+ ...src !== void 0 ? { source: src } : {}
2858
+ };
2859
+ emit({ type: "fragment_hit", object: objectId, time, ...src !== void 0 ? { source: src } : {}, from: { object: objectId, part: frag.part }, target: { object: rh.object, part: rh.part }, point: rh.point, direction: dir, energy, mass: frag.mass, speed });
2860
+ const im = impls.get(rh.object);
2861
+ const o = im ? void 0 : objects.get(rh.object);
2862
+ const key = im ?? o;
2863
+ if (!key) return;
2864
+ const res = im ? implHit(im, hit) : doHit(o, hit, o.byId.get(rh.part));
2865
+ const have = results.get(key);
2866
+ if (have) {
2867
+ have.damage += res.damage;
2868
+ have.events.push(...res.events);
2869
+ if (!have.part) have.part = res.part;
2870
+ if (res.penetrated) have.penetrated = true;
2871
+ } else results.set(key, { ...res, events: [...res.events] });
2872
+ let rep = reports.get(key);
2873
+ if (!rep) reports.set(key, rep = { shelter: 1, confinement: mult, fragmentHits: 0 });
2874
+ rep.fragmentHits++;
2875
+ };
2876
+ const blastWorld = (point, energy, radius, src, call, only) => {
2877
+ const cfg = blastCfg(call);
2878
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return blastPlain(point, energy, radius, src);
2879
+ return blastScene(point, energy, radius, src, cfg, only);
2880
+ };
2599
2881
  const heatObject = (o, point, T, radius, duration, src) => {
2600
2882
  const start = cur.length;
2601
2883
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
@@ -2632,7 +2914,7 @@ var GameDestruction = (() => {
2632
2914
  if (hit.type === "ballistic") {
2633
2915
  const loc = locate(o, point, dir, 0.05, only);
2634
2916
  if (loc) {
2635
- const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src);
2917
+ const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit.piercing ?? 1);
2636
2918
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2637
2919
  }
2638
2920
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -2654,7 +2936,13 @@ var GameDestruction = (() => {
2654
2936
  }
2655
2937
  }
2656
2938
  } else if (hit.type === "explosive") {
2657
- res = blastObject(o, point, E, hit.radius ?? 2, src);
2939
+ const call = hit.blast || hit.confinement !== void 0 ? { ...hit.blast, ...hit.confinement !== void 0 ? { confinement: hit.confinement } : {} } : void 0;
2940
+ const cfg = blastCfg(call);
2941
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) res = blastObject(o, point, E, hit.radius ?? 2, src);
2942
+ else {
2943
+ res = blastWorld(point, E, hit.radius ?? 2, src, call, o)[0] ?? res;
2944
+ res.events = cur.slice(start);
2945
+ }
2658
2946
  } else {
2659
2947
  res = heatObject(o, point, hit.temperature ?? 800, hit.radius ?? 0.25, hit.duration ?? 1, src);
2660
2948
  }
@@ -2911,6 +3199,14 @@ var GameDestruction = (() => {
2911
3199
  for (const e of r.events) if (e.type === "collapse" && !r.value.some((h) => h.events.includes(e))) last.events.push(e);
2912
3200
  return r.value;
2913
3201
  };
3202
+ const blastImpl = (im, h) => {
3203
+ const call = h.blast || h.confinement !== void 0 ? { ...h.blast, ...h.confinement !== void 0 ? { confinement: h.confinement } : {} } : void 0;
3204
+ const cfg = blastCfg(call);
3205
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return implHit(im, h);
3206
+ const world2 = h.space === "local" ? toWorld(im.position, im.rotation, h.point) : h.point;
3207
+ const r = blastWorld(world2, h.energy ?? 0, h.radius ?? 2, h.source, call, im);
3208
+ return r.find((x) => x.object === im.impl.id) ?? implHit(im, h);
3209
+ };
2914
3210
  const whole = (fn) => {
2915
3211
  const r = run(fn);
2916
3212
  r.value.events = r.events;
@@ -2998,7 +3294,7 @@ var GameDestruction = (() => {
2998
3294
  },
2999
3295
  hit(objectId, h) {
3000
3296
  const im = impls.get(objectId);
3001
- if (im) return whole(() => implHit(im, h));
3297
+ if (im) return whole(() => h.type === "explosive" ? blastImpl(im, h) : implHit(im, h));
3002
3298
  const o = getObj(objectId);
3003
3299
  return whole(() => doHit(o, h));
3004
3300
  },
@@ -3009,14 +3305,14 @@ var GameDestruction = (() => {
3009
3305
  const p = getPart(o, partId);
3010
3306
  return whole(() => doHit(o, h, p));
3011
3307
  },
3012
- explode(point, energy, radius, source) {
3013
- return withCollapses(run(() => blastWorld(point, energy, radius, source)));
3308
+ explode(point, energy, radius, source, blast) {
3309
+ return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
3014
3310
  },
3015
3311
  raycast(origin, direction, maxDistance = Infinity) {
3016
3312
  const r = raycastInternal(origin, direction, maxDistance);
3017
3313
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
3018
3314
  },
3019
- shoot(origin, direction, energy, caliber, source) {
3315
+ shoot(origin, direction, energy, caliber, source, piercing) {
3020
3316
  return withCollapses(run(() => {
3021
3317
  const results = [];
3022
3318
  const d = vec.normalize(direction);
@@ -3028,7 +3324,7 @@ var GameDestruction = (() => {
3028
3324
  if (!rh) break;
3029
3325
  const im = impls.get(rh.object);
3030
3326
  if (im) {
3031
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...source !== void 0 ? { source } : {} });
3327
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...piercing !== void 0 ? { piercing } : {}, ...source !== void 0 ? { source } : {} });
3032
3328
  r.part = rh.part;
3033
3329
  r.point = rh.point;
3034
3330
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -3042,7 +3338,7 @@ var GameDestruction = (() => {
3042
3338
  const o = getObj(rh.object);
3043
3339
  const p = getPart(o, rh.part);
3044
3340
  const start = cur.length;
3045
- const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source);
3341
+ const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, piercing ?? 1);
3046
3342
  results.push({ object: o.id, part: p.spec.id, point: rh.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy, events: cur.slice(start) });
3047
3343
  ignore.add(`${o.id}\0${p.spec.id}`);
3048
3344
  if (!out.penetrated || !out.exitPoint) break;
@@ -3230,6 +3526,21 @@ var GameDestruction = (() => {
3230
3526
  return world;
3231
3527
  };
3232
3528
 
3529
+ // src/piercing.ts
3530
+ var PIERCING_BY_KIND = {
3531
+ fmj: 1,
3532
+ hp: 0.6,
3533
+ ap: 1.6,
3534
+ buck: 0.5,
3535
+ slug: 0.25,
3536
+ birdshot: 0.3,
3537
+ rimfire: 1,
3538
+ arrow: 1,
3539
+ bolt: 1
3540
+ };
3541
+ var PIERCING_BY_CALIBER = { "5_56x45": 0.5, "5_45x39": 0.5, "22lr": 0.5 };
3542
+ var piercingOf = (caliber, kind) => PIERCING_BY_CALIBER[caliber] ?? PIERCING_BY_KIND[kind ?? "fmj"] ?? 1;
3543
+
3233
3544
  // src/data/assemblies.json
3234
3545
  var assemblies_default = [
3235
3546
  {
@@ -3562,6 +3873,8 @@ var GameDestruction = (() => {
3562
3873
  stage: "damage stage",
3563
3874
  expose: "rebar bare",
3564
3875
  body: "body",
3876
+ blast: "blast",
3877
+ fragment_hit: "fragment hit",
3565
3878
  destroyed: "destroyed"
3566
3879
  },
3567
3880
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3648,6 +3961,8 @@ var GameDestruction = (() => {
3648
3961
  stage: "est\xE1gio de dano",
3649
3962
  expose: "vergalh\xE3o \xE0 mostra",
3650
3963
  body: "corpo",
3964
+ blast: "explos\xE3o",
3965
+ fragment_hit: "estilha\xE7o atingiu",
3651
3966
  destroyed: "destru\xEDdo"
3652
3967
  },
3653
3968
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },