@adatechnology/game-destruction 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -836,7 +836,8 @@ var materials_default = [
836
836
  0.25
837
837
  ],
838
838
  damageScale: {
839
- blunt: 1.5
839
+ blunt: 1.5,
840
+ shelter: 3
840
841
  },
841
842
  hitDebris: "shards",
842
843
  breakDebris: "shards",
@@ -887,7 +888,10 @@ var materials_default = [
887
888
  "glass",
888
889
  "brittle"
889
890
  ],
890
- compressive: 2e5
891
+ compressive: 2e5,
892
+ damageScale: {
893
+ shelter: 3
894
+ }
891
895
  },
892
896
  {
893
897
  id: "ceramic",
@@ -1573,10 +1577,14 @@ var fragility = (ductility, thickness) => {
1573
1577
  // src/world.ts
1574
1578
  var DMG_PER_J = 0.02;
1575
1579
  var BLAST_COUPLING = 0.1;
1580
+ var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
1581
+ var FRAGMENT_HIT_DEFAULTS = { max: 24, energyFactor: 0.5, hitType: "blunt", range: 6, flightTime: 0.4, spread: 10, aim: 40 };
1582
+ var CONFINEMENT_DEFAULTS = { rays: 26, reach: 1.5, gain: 1.5, power: 2, max: 3 };
1576
1583
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
1577
1584
  var BREACH_RADIUS = 0.55;
1578
1585
  var BREACH_BLAST_FACTOR = 6;
1579
1586
  var BREACH_AREA = 0.25;
1587
+ var BREACH_BLAST_REACH = Infinity;
1580
1588
  var RESIDUAL_CAPACITY = 0.15;
1581
1589
  var UNIT_FRAGMENT_FACTOR = 4;
1582
1590
  var BREACH_GRID_CAP = 4096;
@@ -1611,6 +1619,7 @@ var createDestruction = (options = {}) => {
1611
1619
  const fireSpread = options.fireSpread ?? 0.6;
1612
1620
  const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
1613
1621
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1622
+ const breachBlastReach = options.breach?.blastReach ?? BREACH_BLAST_REACH;
1614
1623
  const rng = createRng(options.seed ?? 1);
1615
1624
  const rnd = () => rng.next();
1616
1625
  let time = 0;
@@ -1966,7 +1975,7 @@ var createDestruction = (options = {}) => {
1966
1975
  if (disc < 0) return true;
1967
1976
  return -b - Math.sqrt(disc) > maxDist;
1968
1977
  };
1969
- const raycastInternal = (origin, dir, maxDist, ignore) => {
1978
+ const raycastInternal = (origin, dir, maxDist, ignore, worldOnly = false) => {
1970
1979
  const d = vec.normalize(dir);
1971
1980
  let best = null;
1972
1981
  for (const o of objects.values()) {
@@ -1979,7 +1988,7 @@ var createDestruction = (options = {}) => {
1979
1988
  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 };
1980
1989
  }
1981
1990
  }
1982
- for (const im of impls.values()) {
1991
+ for (const im of worldOnly ? [] : impls.values()) {
1983
1992
  for (const ps of im.impl.describe().parts) {
1984
1993
  if (ignore?.has(`${im.impl.id}\0${ps.id}`)) continue;
1985
1994
  const ov = im.poses.get(ps.id);
@@ -2392,7 +2401,7 @@ var createDestruction = (options = {}) => {
2392
2401
  else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2393
2402
  return done;
2394
2403
  };
2395
- const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src) => {
2404
+ const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, piercing = 1) => {
2396
2405
  const mat = p.mat;
2397
2406
  if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2398
2407
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
@@ -2405,7 +2414,9 @@ var createDestruction = (options = {}) => {
2405
2414
  const exitLocal = objLocal(o, vec.madd(entry, d, Math.min(pathLen, effM)));
2406
2415
  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;
2407
2416
  const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2408
- const penetrated = E > thr;
2417
+ const hard = mat.tags?.includes("metal") || mat.tags?.includes("masonry");
2418
+ const Ep = piercing === 1 || !hard ? E : E * piercing;
2419
+ const penetrated = Ep > thr;
2409
2420
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2410
2421
  const id = p.spec.id;
2411
2422
  if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
@@ -2417,7 +2428,7 @@ var createDestruction = (options = {}) => {
2417
2428
  }
2418
2429
  if (p.layers.length > 0) spallOuter(o, p, entry, face, n, Math.max(0.03, caliber / 1e3 * 4), src);
2419
2430
  const brittle = mat.ductility < 0.3;
2420
- const ratio = E / Math.max(thr, 1e-6);
2431
+ const ratio = Ep / Math.max(thr, 1e-6);
2421
2432
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2422
2433
  let exitPoint;
2423
2434
  if (penetrated) {
@@ -2447,7 +2458,7 @@ var createDestruction = (options = {}) => {
2447
2458
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2448
2459
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2449
2460
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2450
- return { penetrated, exitEnergy: penetrated ? E - thr : 0, exitPoint, damage: dmg };
2461
+ return { penetrated, exitEnergy: penetrated ? E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2451
2462
  };
2452
2463
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2453
2464
  const mat = p.mat;
@@ -2475,54 +2486,110 @@ var createDestruction = (options = {}) => {
2475
2486
  addBreach(o, p, point, breachAreaOf(p, done, false), E, d, src);
2476
2487
  return done;
2477
2488
  };
2489
+ const blastPart = (o, p, point, energy, radius, src, res, reachAt) => {
2490
+ if (isLost(p)) return null;
2491
+ const { c, q } = partXf(o, p);
2492
+ const lp = toLocal(c, q, point);
2493
+ const dist = distanceToShape(p.spec.shape, lp);
2494
+ if (dist >= radius) return null;
2495
+ const sp = surfacePoint(p.spec.shape, lp);
2496
+ const wp = toWorld(c, q, sp.point);
2497
+ const reach = reachAt ? reachAt(wp, p) : 1;
2498
+ const f = (1 - dist / radius) ** 2 * reach;
2499
+ const Ein = energy * f * BLAST_COUPLING;
2500
+ const wn = rotateVec(q, sp.normal);
2501
+ if (!res.part) {
2502
+ res.part = p.spec.id;
2503
+ res.point = wp;
2504
+ }
2505
+ const mat = p.mat;
2506
+ if (Ein > 20) {
2507
+ 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);
2508
+ else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2509
+ debris(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2510
+ }
2511
+ const th = mat.thermal;
2512
+ if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2513
+ p.st.temperature += 300 * f;
2514
+ o.active = true;
2515
+ if (f >= 0.25 && !p.st.burning) {
2516
+ p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2517
+ checkIgnite(o, p, src);
2518
+ }
2519
+ }
2520
+ wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2521
+ const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2522
+ if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2523
+ frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2524
+ return reach;
2525
+ }
2526
+ 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);
2527
+ const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2528
+ res.damage += dealt;
2529
+ if (dist <= breachBlastReach * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2530
+ return reach;
2531
+ };
2478
2532
  const blastObject = (o, point, energy, radius, src) => {
2479
2533
  const start = cur.length;
2480
2534
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
2481
- for (const p of o.parts) {
2482
- if (isLost(p)) continue;
2483
- const { c, q } = partXf(o, p);
2484
- const lp = toLocal(c, q, point);
2485
- const dist = distanceToShape(p.spec.shape, lp);
2486
- if (dist >= radius) continue;
2487
- const f = (1 - dist / radius) ** 2;
2488
- const Ein = energy * f * BLAST_COUPLING;
2489
- const sp = surfacePoint(p.spec.shape, lp);
2490
- const wp = toWorld(c, q, sp.point);
2491
- const wn = rotateVec(q, sp.normal);
2492
- if (!res.part) {
2493
- res.part = p.spec.id;
2494
- res.point = wp;
2495
- }
2496
- const mat = p.mat;
2497
- if (Ein > 20) {
2498
- 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);
2499
- else if (mat.ductility < 0.3 && Ein > 60) addCrack(o, p, wp, wn, clamp2(0.05 + Ein * 5e-4, 0.05, 1), src);
2500
- debris(o, p, mat.hitDebris, wp, wn, clamp2(Math.round(Ein / 80) + 1, 1, 12), clamp2(Math.sqrt(Ein) / 4, 2, 15), src);
2501
- }
2502
- const th = mat.thermal;
2503
- if (th.ignitionC !== void 0 && p.st.fuel > 0) {
2504
- p.st.temperature += 300 * f;
2505
- o.active = true;
2506
- if (f >= 0.25 && !p.st.burning) {
2507
- p.st.temperature = Math.max(p.st.temperature, th.ignitionC + 1);
2508
- checkIgnite(o, p, src);
2509
- }
2535
+ for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
2536
+ res.events = cur.slice(start);
2537
+ return res;
2538
+ };
2539
+ const blastCfg = (call) => {
2540
+ const pick = (key) => call && call[key] !== void 0 ? call[key] : options.blast?.[key];
2541
+ const merge = (v, defaults) => {
2542
+ if (!v) return null;
2543
+ const out = { ...defaults };
2544
+ if (v !== true) {
2545
+ for (const [key, value] of Object.entries(v)) if (value !== void 0) out[key] = value;
2510
2546
  }
2511
- wearJoints(o, p, Ein * 0.5, void 0, 0, src);
2512
- const amount = Ein * DMG_PER_J * (mat.damageScale?.explosive ?? 1);
2513
- if (blastFrontSpeed > 0 && dist > 0 && mat.tags?.includes("glass")) {
2514
- frontQueue.push({ o, p, due: time + dist / blastFrontSpeed, amount, fragEnergy: Ein * 0.3, point, ...src !== void 0 ? { src } : {} });
2515
- continue;
2547
+ return out;
2548
+ };
2549
+ const conf = pick("confinement");
2550
+ return {
2551
+ shelter: merge(pick("shelter"), SHELTER_DEFAULTS),
2552
+ frag: merge(pick("fragmentHits"), FRAGMENT_HIT_DEFAULTS),
2553
+ conf: typeof conf === "number" ? Number.isFinite(conf) ? Math.max(0, conf) : 1 : merge(conf, CONFINEMENT_DEFAULTS)
2554
+ };
2555
+ };
2556
+ const confinementRays = /* @__PURE__ */ new Map();
2557
+ const raysOf = (n) => {
2558
+ let rays = confinementRays.get(n);
2559
+ if (rays) return rays;
2560
+ const r = createRng((options.seed ?? 1) ^ 1540483477);
2561
+ const off = r.next();
2562
+ const phi0 = r.next() * Math.PI * 2;
2563
+ rays = [];
2564
+ for (let i = 0; i < n; i++) {
2565
+ const y = 1 - 2 * (i + off) / n;
2566
+ const s = Math.sqrt(Math.max(0, 1 - y * y));
2567
+ const phi = phi0 + i * 2.399963229728653;
2568
+ rays.push([Math.cos(phi) * s, y, Math.sin(phi) * s]);
2569
+ }
2570
+ confinementRays.set(n, rays);
2571
+ return rays;
2572
+ };
2573
+ const confinementOf = (conf, point, radius) => {
2574
+ if (conf === null) return 1;
2575
+ if (typeof conf === "number") return conf;
2576
+ let hits = 0;
2577
+ let sum = 0;
2578
+ for (const d of raysOf(Math.max(1, Math.round(conf.rays)))) {
2579
+ const h = raycastInternal(point, d, radius * conf.reach, void 0, true);
2580
+ if (h) {
2581
+ hits++;
2582
+ sum += h.distance;
2516
2583
  }
2517
- 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);
2518
- const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2519
- res.damage += dealt;
2520
- addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2521
2584
  }
2522
- res.events = cur.slice(start);
2523
- return res;
2585
+ if (hits === 0) return 1;
2586
+ const closed = hits / Math.max(1, Math.round(conf.rays));
2587
+ const ratio = clamp2(radius / Math.max(sum / hits, 1e-3), 0, 2);
2588
+ return Math.min(Math.max(1, conf.max), 1 + conf.gain * closed ** conf.power * ratio);
2524
2589
  };
2525
- const blastWorld = (point, energy, radius, src) => {
2590
+ const FLAG_LOST = 1;
2591
+ const FLAG_OPEN = 2;
2592
+ const blastPlain = (point, energy, radius, src) => {
2526
2593
  const out = [];
2527
2594
  for (const o of [...objects.values()]) {
2528
2595
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2530,16 +2597,225 @@ var createDestruction = (options = {}) => {
2530
2597
  if (r.part) out.push(r);
2531
2598
  }
2532
2599
  for (const im of [...impls.values()]) {
2533
- const inRange = im.impl.describe().parts.some((ps) => {
2534
- const ov = im.poses.get(ps.id);
2535
- const c = toWorld(im.position, im.rotation, ov?.pos ?? ps.position);
2536
- const q = quat.mul(im.rotation, ov?.q ?? quatFromEuler(...ps.rotation ?? [0, 0, 0]));
2537
- return distanceToShape(ps.shape, toLocal(c, q, point)) < radius;
2538
- });
2539
- if (inRange) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2600
+ if (implInRange(im, point, radius)) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
2540
2601
  }
2541
2602
  return out;
2542
2603
  };
2604
+ const implParts = (im) => im.impl.describe().parts.map((ps) => {
2605
+ const ov = im.poses.get(ps.id);
2606
+ 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])) };
2607
+ });
2608
+ const implInRange = (im, point, radius) => implParts(im).some(({ ps, c, q }) => distanceToShape(ps.shape, toLocal(c, q, point)) < radius);
2609
+ const blastScene = (point, energy, radius, src, cfg, only) => {
2610
+ const mult = confinementOf(cfg.conf, point, radius);
2611
+ const E = energy * mult;
2612
+ const shelter = cfg.shelter;
2613
+ const flags = /* @__PURE__ */ new Map();
2614
+ const inRange = [];
2615
+ for (const o of objects.values()) {
2616
+ if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
2617
+ inRange.push(o);
2618
+ 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));
2619
+ }
2620
+ const shields = /* @__PURE__ */ new Map();
2621
+ const reachAt = (tp, self) => {
2622
+ if (!shelter) return 1;
2623
+ const L = vec.distance(point, tp);
2624
+ if (L < 1e-4) return 1;
2625
+ const d = vec.scale(vec.sub(tp, point), 1 / L);
2626
+ let t = 1;
2627
+ for (const o of inRange) {
2628
+ if (raySphereMiss(point, d, toWorld(o.st.position, o.st.rotation, o.bc), o.br, L)) continue;
2629
+ for (const p of o.parts) {
2630
+ if (p === self) continue;
2631
+ const was = flags.get(p) ?? 0;
2632
+ if (was & FLAG_LOST) continue;
2633
+ const r = rayPart(o, p, point, d);
2634
+ if (!r) continue;
2635
+ const t0 = Math.max(r.t0, 0);
2636
+ const path = Math.min(r.t1, L) - t0;
2637
+ if (path < shelter.minPath) continue;
2638
+ const mid = vec.madd(point, d, t0 + path / 2);
2639
+ const opening = p.st.opening && insideOpening(o, p, mid);
2640
+ if (opening && was & FLAG_OPEN) continue;
2641
+ if (p.st.stripped && was & FLAG_OPEN) continue;
2642
+ const resistance = p.mat.hpPerKg * p.mat.density * path * (p.mat.damageScale?.shelter ?? 1);
2643
+ let ti = Math.exp(-resistance / shelter.k);
2644
+ if (isLost(p) || opening || p.st.stripped && !(was & FLAG_OPEN)) ti += (1 - ti) * shelter.brokenTransmission;
2645
+ t *= ti;
2646
+ if (cfg.frag) {
2647
+ let list = shields.get(p);
2648
+ if (!list) shields.set(p, list = []);
2649
+ list.push(tp);
2650
+ }
2651
+ }
2652
+ }
2653
+ return t;
2654
+ };
2655
+ const items = [];
2656
+ let order = 0;
2657
+ for (const o of inRange) {
2658
+ if (only && only !== o) continue;
2659
+ for (const p of o.parts) {
2660
+ if (isLost(p)) continue;
2661
+ const dist = distToPart(o, p, point);
2662
+ if (dist < radius) items.push({ dist, order: order++, o, p });
2663
+ }
2664
+ }
2665
+ for (const im of impls.values()) {
2666
+ if (only && only !== im) continue;
2667
+ let dist = Infinity;
2668
+ for (const { ps, c, q } of implParts(im)) dist = Math.min(dist, distanceToShape(ps.shape, toLocal(c, q, point)));
2669
+ if (dist < radius) items.push({ dist, order: order++, im });
2670
+ }
2671
+ items.sort((a, b) => a.dist - b.dist || a.order - b.order);
2672
+ const results = /* @__PURE__ */ new Map();
2673
+ const reports = /* @__PURE__ */ new Map();
2674
+ const report = (key) => {
2675
+ let r = reports.get(key);
2676
+ if (!r) reports.set(key, r = { shelter: 0, confinement: mult, fragmentHits: 0 });
2677
+ return r;
2678
+ };
2679
+ const thrown = [];
2680
+ const sweep = (from) => {
2681
+ if (!cfg.frag) return;
2682
+ for (let i = from; i < cur.length; i++) {
2683
+ const e = cur[i];
2684
+ if (e.type !== "fracture" && e.type !== "stage") continue;
2685
+ for (const frag of e.fragments ?? []) thrown.push({ frag, object: e.object, part: objects.get(e.object)?.byId.get(e.part) ?? null });
2686
+ }
2687
+ };
2688
+ for (const it of items) {
2689
+ if (it.o && it.p) {
2690
+ const o = it.o;
2691
+ let res = results.get(o);
2692
+ if (!res) results.set(o, res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] });
2693
+ const start = cur.length;
2694
+ const k = blastPart(o, it.p, point, E, radius, src, res, shelter ? reachAt : void 0);
2695
+ res.events.push(...cur.slice(start));
2696
+ sweep(start);
2697
+ if (k !== null) {
2698
+ const rep = report(o);
2699
+ rep.shelter = Math.max(rep.shelter, k);
2700
+ if (shelter) (rep.parts ?? (rep.parts = {}))[it.p.spec.id] = k;
2701
+ }
2702
+ } else if (it.im) {
2703
+ const im = it.im;
2704
+ let num = 0;
2705
+ let den = 0;
2706
+ for (const { ps, c, q } of implParts(im)) {
2707
+ const lp = toLocal(c, q, point);
2708
+ const dist = distanceToShape(ps.shape, lp);
2709
+ if (dist >= radius) continue;
2710
+ const f = (1 - dist / radius) ** 2;
2711
+ const sp = surfacePoint(ps.shape, lp);
2712
+ num += f * reachAt(toWorld(c, q, sp.point), null);
2713
+ den += f;
2714
+ }
2715
+ const k = den > 0 ? num / den : 1;
2716
+ const res = implHit(im, { type: "explosive", point, energy: E * k, radius, ...shelter ? { shelter: k } : {}, ...src !== void 0 ? { source: src } : {} });
2717
+ results.set(im, res);
2718
+ report(im).shelter = k;
2719
+ }
2720
+ }
2721
+ if (cfg.frag && thrown.length > 0) {
2722
+ const g = cfg.frag;
2723
+ const energyOf = (f) => 0.5 * f.mass * (f.velocity[0] ** 2 + f.velocity[1] ** 2 + f.velocity[2] ** 2);
2724
+ 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)));
2725
+ list.forEach((t, i) => throwFragment(t.frag, t.object, t.part, g, point, i, src, shields, results, reports, mult));
2726
+ }
2727
+ const out = [];
2728
+ const reportShelter = (key, res) => {
2729
+ res.blast = reports.get(key) ?? { shelter: 1, confinement: mult, fragmentHits: 0 };
2730
+ };
2731
+ const summary = [];
2732
+ let fragmentHits = 0;
2733
+ for (const o of objects.values()) {
2734
+ const r = results.get(o);
2735
+ if (!r?.part) continue;
2736
+ reportShelter(o, r);
2737
+ out.push(r);
2738
+ for (const [part, factor] of Object.entries(r.blast?.parts ?? {})) summary.push({ object: o.id, part, factor });
2739
+ fragmentHits += r.blast?.fragmentHits ?? 0;
2740
+ }
2741
+ for (const im of impls.values()) {
2742
+ const r = results.get(im);
2743
+ if (!r) continue;
2744
+ reportShelter(im, r);
2745
+ out.push(r);
2746
+ if (shelter) summary.push({ object: im.impl.id, factor: r.blast?.shelter ?? 1 });
2747
+ fragmentHits += r.blast?.fragmentHits ?? 0;
2748
+ }
2749
+ emit({ type: "blast", object: "", time, ...src !== void 0 ? { source: src } : {}, point, energy: E, radius, confinement: mult, shelter: summary, fragmentHits });
2750
+ return out;
2751
+ };
2752
+ const seedOf = (...nums) => {
2753
+ let h = 2166136261;
2754
+ for (const n of nums) {
2755
+ h = Math.imul(h ^ (Math.round(n * 1e3) | 0), 16777619);
2756
+ h ^= h >>> 15;
2757
+ }
2758
+ return h | 0;
2759
+ };
2760
+ const throwFragment = (frag, objectId, from, g, centre, index, src, shields, results, reports, mult) => {
2761
+ const v = frag.velocity;
2762
+ const speed = Math.hypot(v[0], v[1], v[2]);
2763
+ const energy = g.energyFactor * 0.5 * frag.mass * speed * speed;
2764
+ if (speed < 0.5 || energy < 1) return;
2765
+ const r = createRng(seedOf(options.seed ?? 1, centre[0], centre[1], centre[2], index, frag.center[0], frag.center[1], frag.center[2]));
2766
+ let dir = vec.scale(v, 1 / speed);
2767
+ const behind = from ? shields.get(from) : void 0;
2768
+ if (behind) {
2769
+ let best = Math.cos(g.aim * Math.PI / 180);
2770
+ for (const tp of behind) {
2771
+ const to = vec.sub(tp, frag.center);
2772
+ const len = vec.length(to);
2773
+ if (len < 1e-4) continue;
2774
+ const cos = vec.dot(dir, to) / len;
2775
+ if (cos >= best) {
2776
+ best = cos;
2777
+ dir = vec.scale(to, 1 / len);
2778
+ }
2779
+ }
2780
+ }
2781
+ const spread3 = Math.tan(g.spread * Math.PI / 180);
2782
+ 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);
2783
+ const range = Math.min(g.range, speed * g.flightTime);
2784
+ const origin = vec.madd(frag.center, dir, 0.02);
2785
+ const rh = raycastInternal(origin, dir, range, from ? /* @__PURE__ */ new Set([`${objectId}\0${from.spec.id}`]) : void 0);
2786
+ if (!rh) return;
2787
+ const hit = {
2788
+ type: g.hitType,
2789
+ point: rh.point,
2790
+ direction: dir,
2791
+ energy,
2792
+ radius: 0.05,
2793
+ part: rh.part,
2794
+ ...g.hitType === "ballistic" ? { caliber: clamp2(Math.max(...frag.size) * 1e3, 3, 30), mass: frag.mass * 1e3 } : {},
2795
+ ...src !== void 0 ? { source: src } : {}
2796
+ };
2797
+ 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 });
2798
+ const im = impls.get(rh.object);
2799
+ const o = im ? void 0 : objects.get(rh.object);
2800
+ const key = im ?? o;
2801
+ if (!key) return;
2802
+ const res = im ? implHit(im, hit) : doHit(o, hit, o.byId.get(rh.part));
2803
+ const have = results.get(key);
2804
+ if (have) {
2805
+ have.damage += res.damage;
2806
+ have.events.push(...res.events);
2807
+ if (!have.part) have.part = res.part;
2808
+ if (res.penetrated) have.penetrated = true;
2809
+ } else results.set(key, { ...res, events: [...res.events] });
2810
+ let rep = reports.get(key);
2811
+ if (!rep) reports.set(key, rep = { shelter: 1, confinement: mult, fragmentHits: 0 });
2812
+ rep.fragmentHits++;
2813
+ };
2814
+ const blastWorld = (point, energy, radius, src, call, only) => {
2815
+ const cfg = blastCfg(call);
2816
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return blastPlain(point, energy, radius, src);
2817
+ return blastScene(point, energy, radius, src, cfg, only);
2818
+ };
2543
2819
  const heatObject = (o, point, T, radius, duration, src) => {
2544
2820
  const start = cur.length;
2545
2821
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
@@ -2576,7 +2852,7 @@ var createDestruction = (options = {}) => {
2576
2852
  if (hit.type === "ballistic") {
2577
2853
  const loc = locate(o, point, dir, 0.05, only);
2578
2854
  if (loc) {
2579
- const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src);
2855
+ const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit.piercing ?? 1);
2580
2856
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2581
2857
  }
2582
2858
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -2598,7 +2874,13 @@ var createDestruction = (options = {}) => {
2598
2874
  }
2599
2875
  }
2600
2876
  } else if (hit.type === "explosive") {
2601
- res = blastObject(o, point, E, hit.radius ?? 2, src);
2877
+ const call = hit.blast || hit.confinement !== void 0 ? { ...hit.blast, ...hit.confinement !== void 0 ? { confinement: hit.confinement } : {} } : void 0;
2878
+ const cfg = blastCfg(call);
2879
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) res = blastObject(o, point, E, hit.radius ?? 2, src);
2880
+ else {
2881
+ res = blastWorld(point, E, hit.radius ?? 2, src, call, o)[0] ?? res;
2882
+ res.events = cur.slice(start);
2883
+ }
2602
2884
  } else {
2603
2885
  res = heatObject(o, point, hit.temperature ?? 800, hit.radius ?? 0.25, hit.duration ?? 1, src);
2604
2886
  }
@@ -2855,6 +3137,14 @@ var createDestruction = (options = {}) => {
2855
3137
  for (const e of r.events) if (e.type === "collapse" && !r.value.some((h) => h.events.includes(e))) last.events.push(e);
2856
3138
  return r.value;
2857
3139
  };
3140
+ const blastImpl = (im, h) => {
3141
+ const call = h.blast || h.confinement !== void 0 ? { ...h.blast, ...h.confinement !== void 0 ? { confinement: h.confinement } : {} } : void 0;
3142
+ const cfg = blastCfg(call);
3143
+ if (!cfg.shelter && !cfg.frag && cfg.conf === null) return implHit(im, h);
3144
+ const world2 = h.space === "local" ? toWorld(im.position, im.rotation, h.point) : h.point;
3145
+ const r = blastWorld(world2, h.energy ?? 0, h.radius ?? 2, h.source, call, im);
3146
+ return r.find((x) => x.object === im.impl.id) ?? implHit(im, h);
3147
+ };
2858
3148
  const whole = (fn) => {
2859
3149
  const r = run(fn);
2860
3150
  r.value.events = r.events;
@@ -2942,7 +3232,7 @@ var createDestruction = (options = {}) => {
2942
3232
  },
2943
3233
  hit(objectId, h) {
2944
3234
  const im = impls.get(objectId);
2945
- if (im) return whole(() => implHit(im, h));
3235
+ if (im) return whole(() => h.type === "explosive" ? blastImpl(im, h) : implHit(im, h));
2946
3236
  const o = getObj(objectId);
2947
3237
  return whole(() => doHit(o, h));
2948
3238
  },
@@ -2953,14 +3243,14 @@ var createDestruction = (options = {}) => {
2953
3243
  const p = getPart(o, partId);
2954
3244
  return whole(() => doHit(o, h, p));
2955
3245
  },
2956
- explode(point, energy, radius, source) {
2957
- return withCollapses(run(() => blastWorld(point, energy, radius, source)));
3246
+ explode(point, energy, radius, source, blast) {
3247
+ return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
2958
3248
  },
2959
3249
  raycast(origin, direction, maxDistance = Infinity) {
2960
3250
  const r = raycastInternal(origin, direction, maxDistance);
2961
3251
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
2962
3252
  },
2963
- shoot(origin, direction, energy, caliber, source) {
3253
+ shoot(origin, direction, energy, caliber, source, piercing) {
2964
3254
  return withCollapses(run(() => {
2965
3255
  const results = [];
2966
3256
  const d = vec.normalize(direction);
@@ -2972,7 +3262,7 @@ var createDestruction = (options = {}) => {
2972
3262
  if (!rh) break;
2973
3263
  const im = impls.get(rh.object);
2974
3264
  if (im) {
2975
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...source !== void 0 ? { source } : {} });
3265
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...piercing !== void 0 ? { piercing } : {}, ...source !== void 0 ? { source } : {} });
2976
3266
  r.part = rh.part;
2977
3267
  r.point = rh.point;
2978
3268
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -2986,7 +3276,7 @@ var createDestruction = (options = {}) => {
2986
3276
  const o = getObj(rh.object);
2987
3277
  const p = getPart(o, rh.part);
2988
3278
  const start = cur.length;
2989
- const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source);
3279
+ const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, piercing ?? 1);
2990
3280
  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) });
2991
3281
  ignore.add(`${o.id}\0${p.spec.id}`);
2992
3282
  if (!out.penetrated || !out.exitPoint) break;
@@ -3174,6 +3464,21 @@ var createDestruction = (options = {}) => {
3174
3464
  return world;
3175
3465
  };
3176
3466
 
3467
+ // src/piercing.ts
3468
+ var PIERCING_BY_KIND = {
3469
+ fmj: 1,
3470
+ hp: 0.6,
3471
+ ap: 1.6,
3472
+ buck: 0.5,
3473
+ slug: 0.25,
3474
+ birdshot: 0.3,
3475
+ rimfire: 1,
3476
+ arrow: 1,
3477
+ bolt: 1
3478
+ };
3479
+ var PIERCING_BY_CALIBER = { "5_56x45": 0.5, "5_45x39": 0.5, "22lr": 0.5 };
3480
+ var piercingOf = (caliber, kind) => PIERCING_BY_CALIBER[caliber] ?? PIERCING_BY_KIND[kind ?? "fmj"] ?? 1;
3481
+
3177
3482
  // src/data/assemblies.json
3178
3483
  var assemblies_default = [
3179
3484
  {
@@ -3506,6 +3811,8 @@ var locales = {
3506
3811
  stage: "damage stage",
3507
3812
  expose: "rebar bare",
3508
3813
  body: "body",
3814
+ blast: "blast",
3815
+ fragment_hit: "fragment hit",
3509
3816
  destroyed: "destroyed"
3510
3817
  },
3511
3818
  fractureStyles: { shatter: "shatter", crumble: "crumble", splinter: "splinter", tear: "tear", snap: "snap", none: "none" },
@@ -3592,6 +3899,8 @@ var locales = {
3592
3899
  stage: "est\xE1gio de dano",
3593
3900
  expose: "vergalh\xE3o \xE0 mostra",
3594
3901
  body: "corpo",
3902
+ blast: "explos\xE3o",
3903
+ fragment_hit: "estilha\xE7o atingiu",
3595
3904
  destroyed: "destru\xEDdo"
3596
3905
  },
3597
3906
  fractureStyles: { shatter: "estilha\xE7ar", crumble: "esfarelar", splinter: "lascar", tear: "rasgar", snap: "partir", none: "nenhuma" },
@@ -3609,11 +3918,16 @@ var checkDestructible = (impl, options = {}) => {
3609
3918
  };
3610
3919
  export {
3611
3920
  ASSEMBLIES,
3921
+ CONFINEMENT_DEFAULTS,
3612
3922
  FIRE_PRIME_C,
3923
+ FRAGMENT_HIT_DEFAULTS,
3613
3924
  IMPACT_CONTRACT,
3614
3925
  JOINT_BY_MATERIALS,
3615
3926
  JOINT_DEFAULTS,
3616
3927
  MATERIALS,
3928
+ PIERCING_BY_CALIBER,
3929
+ PIERCING_BY_KIND,
3930
+ SHELTER_DEFAULTS,
3617
3931
  assemblyDepth,
3618
3932
  assemblyPart,
3619
3933
  buildFrame,
@@ -3630,6 +3944,7 @@ export {
3630
3944
  partMass,
3631
3945
  partThickness,
3632
3946
  partVolume,
3947
+ piercingOf,
3633
3948
  quat,
3634
3949
  quatFromEuler,
3635
3950
  registerAssembly,