@adatechnology/game-destruction 0.7.1 → 0.8.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.
@@ -24,40 +24,88 @@ var GameDestruction = (() => {
24
24
  ASSEMBLIES: () => ASSEMBLIES,
25
25
  CONFINEMENT_DEFAULTS: () => CONFINEMENT_DEFAULTS,
26
26
  FIRE_PRIME_C: () => FIRE_PRIME_C,
27
+ FLEXURE_DEFAULTS: () => FLEXURE_DEFAULTS,
27
28
  FRAGMENT_HIT_DEFAULTS: () => FRAGMENT_HIT_DEFAULTS,
29
+ GRAVITY: () => GRAVITY,
28
30
  IMPACT_CONTRACT: () => IMPACT_CONTRACT,
29
31
  JOINT_BY_MATERIALS: () => JOINT_BY_MATERIALS,
30
32
  JOINT_DEFAULTS: () => JOINT_DEFAULTS,
33
+ MASONRY_DEFAULTS: () => MASONRY_DEFAULTS,
31
34
  MATERIALS: () => MATERIALS,
32
- PIERCING_BY_CALIBER: () => PIERCING_BY_CALIBER,
33
- PIERCING_BY_KIND: () => PIERCING_BY_KIND,
34
35
  SHELTER_DEFAULTS: () => SHELTER_DEFAULTS,
36
+ SOILS: () => SOILS,
37
+ SOIL_DEFAULTS: () => SOIL_DEFAULTS,
38
+ WIND_DEFAULTS: () => WIND_DEFAULTS,
39
+ WIND_TERRAIN: () => WIND_TERRAIN,
40
+ analyzeWind: () => analyzeWind,
35
41
  assemblyDepth: () => assemblyDepth,
36
42
  assemblyPart: () => assemblyPart,
43
+ bearingWallThickness: () => bearingWallThickness,
44
+ bendGeometry: () => bendGeometry,
45
+ blastImpulse: () => blastImpulse,
37
46
  buildFrame: () => buildFrame,
38
47
  checkDestructible: () => checkDestructible2,
39
48
  checkHit: () => checkHit,
49
+ crackingMoment: () => crackingMoment,
40
50
  createDestruction: () => createDestruction,
51
+ damperAmplification: () => damperAmplification,
52
+ damperTuning: () => damperTuning,
53
+ elasticEnergy: () => elasticEnergy,
41
54
  explosivePart: () => explosivePart,
55
+ exposureOf: () => exposureOf,
56
+ footingArea: () => footingArea,
57
+ footingSoil: () => footingSoil,
42
58
  fragility: () => fragility,
43
59
  getAssembly: () => getAssembly,
44
60
  guessMaterial: () => guessMaterial,
61
+ gustAt: () => gustAt,
62
+ heightFactor: () => heightFactor,
63
+ impulseEnergy: () => impulseEnergy,
45
64
  inferJoints: () => inferJoints,
65
+ layersBetween: () => layersBetween,
46
66
  locales: () => locales,
67
+ makeGust: () => makeGust,
68
+ meanHeightFactor: () => meanHeightFactor,
69
+ momentNodes: () => momentNodes,
70
+ momentOfEnergy: () => momentOfEnergy,
47
71
  partCapacity: () => partCapacity,
48
72
  partMass: () => partMass,
49
73
  partThickness: () => partThickness,
50
74
  partVolume: () => partVolume,
51
- piercingOf: () => piercingOf,
75
+ pileCapacity: () => pileCapacity,
76
+ pileLength: () => pileLength,
77
+ pileStiffness: () => pileStiffness,
78
+ profileOf: () => profileOf,
52
79
  quat: () => quat,
53
80
  quatFromEuler: () => quatFromEuler,
54
81
  registerAssembly: () => registerAssembly,
55
82
  resolveStrings: () => resolveStrings,
83
+ resolveWind: () => resolveWind,
56
84
  rotateVec: () => rotateVec,
85
+ sectionMoments: () => sectionMoments,
86
+ shapeAt: () => shapeAt,
87
+ shearNodes: () => shearNodes,
88
+ sizeRebar: () => sizeRebar,
57
89
  sizeStructure: () => sizeStructure,
90
+ slendernessFactor: () => slendernessFactor,
91
+ soilAt: () => soilAt,
92
+ solveFoundation: () => solveFoundation,
93
+ solveWallFrame: () => solveWallFrame,
94
+ spanMoment: () => spanMoment,
95
+ spanOf: () => spanOf,
96
+ steelMoment: () => steelMoment,
97
+ stepSway: () => stepSway,
98
+ stiffnessCentre: () => stiffnessCentre,
99
+ subgradeModulus: () => subgradeModulus,
100
+ tBeamInertia: () => tBeamInertia,
101
+ tensionCapacity: () => tensionCapacity,
102
+ tensionSteel: () => tensionSteel,
58
103
  toLocal: () => toLocal,
59
104
  toWorld: () => toWorld,
60
- vec: () => vec
105
+ turbulenceIntensity: () => turbulenceIntensity,
106
+ vec: () => vec,
107
+ windClass: () => windClass,
108
+ windPressure: () => windPressure
61
109
  });
62
110
 
63
111
  // src/chain.ts
@@ -587,6 +635,7 @@ var GameDestruction = (() => {
587
635
  const [a = 0, b = 0, c = 0] = [...dims].sort((x, y) => x - y);
588
636
  return b / a < 3 && c / b >= 3;
589
637
  };
638
+ var memberBarCount = (width, spacing) => spread(width, COVER, spacing, MAX_MEMBER_BARS_PER_SIDE).length;
590
639
  var rebarBars = (dims, thin, spacing) => {
591
640
  const out = [];
592
641
  if (isMember(dims)) {
@@ -622,12 +671,808 @@ var GameDestruction = (() => {
622
671
  }
623
672
  return out;
624
673
  };
674
+ var verticalBarCount = (dims, thin, spacing) => {
675
+ if (isMember(dims)) {
676
+ if (dims.indexOf(Math.max(...dims)) !== 1) return 0;
677
+ const us = spread(dims[0], COVER, spacing, Infinity).length, vs = spread(dims[2], COVER, spacing, Infinity).length;
678
+ return 2 * us + 2 * Math.max(0, vs - 2);
679
+ }
680
+ const [u = 0, v = 0] = [0, 1, 2].filter((i) => i !== thin);
681
+ if (u === 1) return spread(dims[v], COVER, spacing, Infinity).length;
682
+ if (v === 1) return spread(dims[u], COVER, spacing, Infinity).length;
683
+ return 0;
684
+ };
625
685
  var rebarMass = (bars, diameter, density = STEEL_DENSITY) => {
626
686
  let length = 0;
627
687
  for (const [a, b] of bars) length += Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
628
688
  return Math.PI * (diameter / 2) * (diameter / 2) * length * density;
629
689
  };
630
690
 
691
+ // src/flexure.ts
692
+ var GRAVITY = 9.81;
693
+ var MPA = 1e6;
694
+ var FLEXURE_DEFAULTS = {
695
+ enabled: true,
696
+ roles: ["beam", "slab", "roof", "stair"],
697
+ scope: "rebar",
698
+ crackFactor: 1.5,
699
+ rebarYieldMPa: 500,
700
+ cover: 0.03,
701
+ neutralAxisLimit: 0.45,
702
+ failRotationDeg: 10,
703
+ loadFactor: 0.64,
704
+ massFactor: 0.5,
705
+ tntJoules: 4184e3,
706
+ impulse: 200,
707
+ reflection: 2,
708
+ reflectionNear: 4,
709
+ nearScaled: 0.5
710
+ };
711
+ var MASONRY_DEFAULTS = { minEccentricity: 0.05, slendernessLimit: 40, concreteFactor: 0.85 };
712
+ var resolveFlexure = (o) => ({ ...FLEXURE_DEFAULTS, ...definedOf(o) });
713
+ var resolveMasonry = (o) => ({ ...MASONRY_DEFAULTS, ...definedOf(o) });
714
+ var definedOf = (o) => Object.fromEntries(Object.entries(o ?? {}).filter(([, v]) => v !== void 0));
715
+ var RATIO_BAR = 0.01;
716
+ var crackingMoment = (tensileMPa, b, h, alpha = FLEXURE_DEFAULTS.crackFactor) => alpha * tensileMPa * MPA * (b * h * h) / 6;
717
+ var steelMoment = (area, yieldMPa, d, b, compressiveMPa, options = {}) => {
718
+ if (area <= 0 || d <= 0 || b <= 0) return 0;
719
+ const T = area * yieldMPa * MPA;
720
+ const block = 0.68 * compressiveMPa * MPA * b;
721
+ const x = T / block;
722
+ const xl = (options.neutralAxisLimit ?? FLEXURE_DEFAULTS.neutralAxisLimit) * d;
723
+ if (x <= xl) return T * (d - 0.4 * x);
724
+ const top = Math.min(d / 2, (options.cover ?? FLEXURE_DEFAULTS.cover) + RATIO_BAR / 2);
725
+ return block * xl * (d - 0.4 * xl) + (T - block * xl) * (d - top);
726
+ };
727
+ var spanStiffness = (EI, span) => 384 * EI / (5 * span ** 3);
728
+ var elasticEnergy = (moment, span, EI, loadFactor = FLEXURE_DEFAULTS.loadFactor) => {
729
+ const R = 8 * moment / span;
730
+ return loadFactor * R * R / (2 * spanStiffness(EI, span));
731
+ };
732
+ var momentOfEnergy = (energy, span, EI, loadFactor = FLEXURE_DEFAULTS.loadFactor) => span / 8 * Math.sqrt(2 * Math.max(0, energy) * spanStiffness(EI, span) / loadFactor);
733
+ var impulseEnergy = (impulse, mass, options = {}) => {
734
+ const KL = options.loadFactor ?? FLEXURE_DEFAULTS.loadFactor;
735
+ const KM = options.massFactor ?? FLEXURE_DEFAULTS.massFactor;
736
+ return mass > 0 ? (KL * impulse) ** 2 / (2 * KM * mass) : 0;
737
+ };
738
+ var blastImpulse = (tnt, distance, options = {}) => {
739
+ if (tnt <= 0) return 0;
740
+ const o = { ...FLEXURE_DEFAULTS, ...definedOf(options) };
741
+ const cube = Math.cbrt(tnt);
742
+ const r = Math.max(distance, o.nearScaled * cube);
743
+ return (o.reflection + o.reflectionNear / (r / cube)) * (o.impulse * cube * cube / r);
744
+ };
745
+ var slendernessFactor = (h, t, options = {}) => {
746
+ const o = resolveMasonry(options);
747
+ const lambda = h / Math.max(t, 1e-3);
748
+ return Math.max(0, (1 - 2 * o.minEccentricity) * (1 - (lambda / o.slendernessLimit) ** 3));
749
+ };
750
+ var isReinforced = (part) => !!part.reinforcement || (part.structure?.rebar?.ratio ?? 0) > 0;
751
+ var isSlender = (part, material) => material.strength?.slender ?? (!!material.tags?.includes("masonry") && !isReinforced(part));
752
+ var isFlexural = (part, material, options) => options.enabled && material.strength?.tensileMPa !== void 0 && !!part.structure && options.roles.includes(part.structure.role) && (part.structure.rebar !== void 0 || options.scope === "all" || options.scope === "reinforced" && !!part.reinforcement);
753
+ var tensionSteel = (part, dims, section, options) => {
754
+ const rebar = part.structure?.rebar;
755
+ const cage = part.reinforcement;
756
+ const bar2 = rebar ? RATIO_BAR : cage?.diameter ?? RATIO_BAR;
757
+ const d = Math.max(0.01, section.h - (rebar?.cover ?? options.cover) - bar2 / 2);
758
+ const yieldMPa = rebar?.yieldMPa ?? options.rebarYieldMPa;
759
+ if (rebar && rebar.ratio > 0) return { area: rebar.ratio * section.b * d, d, yieldMPa };
760
+ if (!cage) return { area: 0, d, yieldMPa };
761
+ const one = Math.PI * cage.diameter * cage.diameter / 4;
762
+ const count = isMember(dims) ? memberBarCount(section.b, cage.spacing) : section.b / cage.spacing;
763
+ return { area: one * count, d, yieldMPa };
764
+ };
765
+ var sectionMoments = (material, section, steel, options) => {
766
+ const s = material.strength;
767
+ const cracking = crackingMoment(s.tensileMPa ?? 0, section.b, section.h, options.crackFactor);
768
+ const steelM = steelMoment(steel.area, steel.yieldMPa, steel.d, section.b, s.compressiveMPa, options);
769
+ return { cracking, steel: steelM, ultimate: Math.max(cracking, steelM), reinforced: steelM > 0 };
770
+ };
771
+ var spanMoment = (w, span) => w * span * span / 8;
772
+ var bendGeometry = (part) => {
773
+ const dims = shapeDims(part.shape);
774
+ const member = isMember(dims);
775
+ const q = quatFromEuler(...part.rotation ?? [0, 0, 0]);
776
+ const up = rotateVec(quat.conjugate(q), [0, 1, 0]);
777
+ let spanAxes;
778
+ let depthAxes;
779
+ if (member) {
780
+ const long = dims.indexOf(Math.max(...dims));
781
+ spanAxes = [long];
782
+ depthAxes = [0, 1, 2].filter((i) => i !== long);
783
+ } else {
784
+ const thin = thinAxisOf(dims);
785
+ spanAxes = [...planeAxes(thin)];
786
+ depthAxes = [thin];
787
+ }
788
+ let gravity = -1;
789
+ for (const a of depthAxes) if (Math.abs(up[a]) > Math.SQRT1_2 && (gravity < 0 || Math.abs(up[a]) > Math.abs(up[gravity]))) gravity = a;
790
+ return { dims, member, spanAxes, depthAxes, gravity, up };
791
+ };
792
+ var sectionWidth = (g, axis) => g.member ? g.dims[g.depthAxes.find((a) => a !== axis) ?? axis] : 1;
793
+ var lineLoad = (g, load) => {
794
+ const area = g.member ? g.dims[g.spanAxes[0]] : g.dims[g.spanAxes[0]] * g.dims[g.spanAxes[1]];
795
+ return load * GRAVITY / Math.max(area, 1e-6);
796
+ };
797
+ var spanOf = (g, contacts, samples = 9) => {
798
+ if (contacts.length === 0) return 0;
799
+ const axes = g.spanAxes;
800
+ const steps = (a) => Array.from({ length: samples }, (_, i) => (i / (samples - 1) - 0.5) * g.dims[a]);
801
+ const us = steps(axes[0]);
802
+ const vs = axes.length > 1 ? steps(axes[1]) : [0];
803
+ let far = 0;
804
+ for (const u of us) for (const v of vs) {
805
+ let near = Infinity;
806
+ for (const [lo, hi] of contacts) {
807
+ let gap = 0;
808
+ const x = [u, v];
809
+ axes.forEach((a, k) => gap = Math.max(gap, lo[a] - x[k], x[k] - hi[a], 0));
810
+ if (gap < near) near = gap;
811
+ }
812
+ if (near > far) far = near;
813
+ }
814
+ return 2 * far;
815
+ };
816
+ var FLEXURAL_ROLES = FLEXURE_DEFAULTS.roles;
817
+ var bearingWallThickness = (options) => {
818
+ const gf = options.loadFactor ?? 1.4;
819
+ const gm = options.materialFactor ?? 2;
820
+ const min = options.minThickness ?? 0.14;
821
+ const step = options.step ?? 0.01;
822
+ const fd = (options.material.strength?.compressiveMPa ?? 0) * MPA / gm;
823
+ const own = options.material.density * GRAVITY * options.storeyHeight;
824
+ const out = [];
825
+ let above = 0;
826
+ for (let i = 0; i < options.storeys; i++) {
827
+ const n = above + options.floorLoad;
828
+ let t = min;
829
+ for (let k = 0; k < 60; k++) {
830
+ const room = fd * slendernessFactor(options.storeyHeight, t, options.masonry) - gf * own;
831
+ if (room <= 0) {
832
+ t *= 1.25;
833
+ if (t > 20) {
834
+ t = Infinity;
835
+ break;
836
+ }
837
+ continue;
838
+ }
839
+ const next = Math.max(min, gf * n / room);
840
+ if (Math.abs(next - t) < 1e-5) break;
841
+ t = next;
842
+ }
843
+ t = Number.isFinite(t) ? Math.ceil(t / step - 1e-9) * step : Infinity;
844
+ out.push(t);
845
+ above = n + own * (Number.isFinite(t) ? t : 0);
846
+ }
847
+ return out.reverse();
848
+ };
849
+
850
+ // src/data/soils.json
851
+ var soils_default = [
852
+ { id: "rock", kind: "rock", allowableKPa: 3e3, spt: 50, elasticMPa: 1e4, poisson: 0.2, pile: { tipKPa: 9e3, skinKPa: 500 } },
853
+ { id: "rock_fractured", kind: "rock", allowableKPa: 1500, spt: 50, elasticMPa: 2e3, poisson: 0.25, pile: { tipKPa: 4500, skinKPa: 300 } },
854
+ { id: "gravel_dense", kind: "gravel", allowableKPa: 600, spt: 40, elasticMPa: 132, poisson: 0.3 },
855
+ { id: "gravel_loose", kind: "gravel", allowableKPa: 300, spt: 8, elasticMPa: 26, poisson: 0.3 },
856
+ { id: "sand_very_dense", kind: "sand", allowableKPa: 500, spt: 45, elasticMPa: 120, poisson: 0.3 },
857
+ { id: "sand_dense", kind: "sand", allowableKPa: 400, spt: 25, elasticMPa: 68, poisson: 0.3 },
858
+ { id: "sand_medium", kind: "sand", allowableKPa: 200, spt: 12, elasticMPa: 32, poisson: 0.3 },
859
+ { id: "sand_loose", kind: "sand", allowableKPa: 50, spt: 3, elasticMPa: 8, poisson: 0.3 },
860
+ { id: "silt_hard", kind: "silt", allowableKPa: 300, spt: 25, elasticMPa: 44, poisson: 0.35 },
861
+ { id: "silt_stiff", kind: "silt", allowableKPa: 200, spt: 15, elasticMPa: 26, poisson: 0.35 },
862
+ { id: "silt_medium", kind: "silt", allowableKPa: 100, spt: 8, elasticMPa: 14, poisson: 0.35 },
863
+ { id: "clay_hard", kind: "clay", allowableKPa: 300, spt: 25, elasticMPa: 35, poisson: 0.4 },
864
+ { id: "clay_stiff", kind: "clay", allowableKPa: 200, spt: 15, elasticMPa: 21, poisson: 0.4 },
865
+ { id: "clay_medium", kind: "clay", allowableKPa: 100, spt: 8, elasticMPa: 11, poisson: 0.4 },
866
+ { id: "clay_soft", kind: "clay", allowableKPa: 30, spt: 3, elasticMPa: 4.2, poisson: 0.4 },
867
+ { id: "clay_very_soft", kind: "clay", allowableKPa: 15, spt: 1, elasticMPa: 1.4, poisson: 0.4 }
868
+ ];
869
+
870
+ // src/soil.ts
871
+ var SOILS = soils_default;
872
+ var SOIL_DEFAULTS = {
873
+ safety: 3,
874
+ pileSafety: 2,
875
+ rigidity: 0.82,
876
+ influence: 2,
877
+ imperfection: 1 / 200,
878
+ cells: 4,
879
+ sinkRate: 0.05,
880
+ tiltRate: 1.5,
881
+ pileElasticGPa: 25,
882
+ footingWidth: 0.6
883
+ };
884
+ var resolveSoil = (o = {}) => {
885
+ const byId = new Map(SOILS.map((s) => [s.id, s]));
886
+ for (const s of o.soils ?? []) byId.set(s.id, s);
887
+ const { soils: _soils, site, ...rest } = o;
888
+ return { ...SOIL_DEFAULTS, ...rest, ...site !== void 0 ? { site } : {}, byId };
889
+ };
890
+ var profileOf = (soil) => typeof soil === "string" ? { layers: [{ soil }] } : soil;
891
+ var layersBetween = (profile, top, bottom, soils) => {
892
+ const out = [];
893
+ let z = 0;
894
+ const layers = profile.layers;
895
+ for (let i = 0; i < layers.length; i++) {
896
+ const def = soils.get(layers[i].soil);
897
+ if (!def) throw new Error(`unknown soil "${layers[i].soil}"`);
898
+ const last = i === layers.length - 1;
899
+ const end = last ? Infinity : z + Math.max(0, layers[i].thickness ?? 0);
900
+ const a = Math.max(z, top), b = Math.min(end, bottom);
901
+ if (b > a) out.push({ soil: def, thickness: b - a, top: a });
902
+ z = end;
903
+ if (z >= bottom) break;
904
+ }
905
+ return out;
906
+ };
907
+ var soilAt = (profile, depth, soils) => {
908
+ let z = 0;
909
+ for (let i = 0; i < profile.layers.length; i++) {
910
+ const def = soils.get(profile.layers[i].soil);
911
+ if (!def) throw new Error(`unknown soil "${profile.layers[i].soil}"`);
912
+ z += Math.max(0, profile.layers[i].thickness ?? 0);
913
+ if (i === profile.layers.length - 1 || depth < z) return def;
914
+ }
915
+ throw new Error("a soil profile needs at least one layer");
916
+ };
917
+ var footingSoil = (profile, depth, width, soils, influence = SOIL_DEFAULTS.influence) => {
918
+ const zone = layersBetween(profile, depth, depth + Math.max(0.01, influence * width), soils);
919
+ let allowable = Infinity, e = 0, nu = 0, t = 0;
920
+ for (const l of zone) {
921
+ allowable = Math.min(allowable, l.soil.allowableKPa);
922
+ e += l.soil.elasticMPa * l.thickness;
923
+ nu += l.soil.poisson * l.thickness;
924
+ t += l.thickness;
925
+ }
926
+ if (t <= 0) {
927
+ const l = soilAt(profile, depth, soils);
928
+ return { allowableKPa: l.allowableKPa, elasticMPa: l.elasticMPa, poisson: l.poisson };
929
+ }
930
+ return { allowableKPa: allowable, elasticMPa: e / t, poisson: nu / t };
931
+ };
932
+ var DQ_C = { clay: 120, silt: 200, sand: 400, gravel: 400, rock: 400 };
933
+ var BORED = {
934
+ clay: { alpha: 0.85, beta: 0.8 },
935
+ silt: { alpha: 0.6, beta: 0.65 },
936
+ sand: { alpha: 0.5, beta: 0.5 },
937
+ gravel: { alpha: 0.5, beta: 0.5 },
938
+ rock: { alpha: 1, beta: 1 }
939
+ };
940
+ var clampN = (n) => Math.min(50, Math.max(3, n));
941
+ var pileCapacity = (profile, pile, top, soils) => {
942
+ const d = pile.diameter;
943
+ const area = Math.PI * d * d / 4;
944
+ const perimeter = Math.PI * d;
945
+ const driven = pile.method === "driven";
946
+ let skin = 0;
947
+ for (const l of layersBetween(profile, top, top + pile.length, soils)) {
948
+ const f = driven ? 1 : BORED[l.soil.kind].beta;
949
+ const kPa = l.soil.pile?.skinKPa ?? f * 10 * (clampN(l.soil.spt) / 3 + 1);
950
+ skin += kPa * 1e3 * perimeter * l.thickness;
951
+ }
952
+ const tipDepth = top + pile.length;
953
+ const tipSoil = soilAt(profile, tipDepth, soils);
954
+ const alpha = driven ? 1 : BORED[tipSoil.kind].alpha;
955
+ const tipKPa = tipSoil.pile?.tipKPa ?? alpha * DQ_C[tipSoil.kind] * clampN(tipSoil.spt);
956
+ const tip = tipKPa * 1e3 * area;
957
+ return { tip, skin, ultimate: tip + skin, tipSoil };
958
+ };
959
+ var pileLength = (profile, o) => {
960
+ const soils = o.soils ?? resolveSoil().byId;
961
+ const safety = o.safety ?? SOIL_DEFAULTS.pileSafety;
962
+ const step = o.step ?? 0.5;
963
+ const max = o.maxLength ?? 60;
964
+ for (let L = step; L <= max + 1e-9; L += step) {
965
+ if (pileCapacity(profile, { length: L, diameter: o.diameter, ...o.method ? { method: o.method } : {} }, o.top ?? 0, soils).ultimate / safety >= o.load) return Math.round(L / step) * step;
966
+ }
967
+ return max;
968
+ };
969
+ var pileStiffness = (pile, pileElasticGPa, tipElasticMPa, tipPoisson) => {
970
+ const area = Math.PI * pile.diameter * pile.diameter / 4;
971
+ const shaft = pile.length / (2 * pileElasticGPa * 1e9 * area);
972
+ const tip = (1 - tipPoisson * tipPoisson) / (tipElasticMPa * 1e6 * pile.diameter);
973
+ return 1 / (shaft + tip);
974
+ };
975
+ var subgradeModulus = (elasticMPa, poisson, width, rigidity = SOIL_DEFAULTS.rigidity) => elasticMPa * 1e6 / (Math.max(0.05, width) * (1 - poisson * poisson) * rigidity);
976
+ var footingArea = (load, allowableKPa) => load / (allowableKPa * 1e3);
977
+ var yieldSprings = (springs, s, a, b) => {
978
+ const [x0, z0] = stiffnessCentre(springs);
979
+ return springs.map((sp) => {
980
+ const set = sp.set ?? 0;
981
+ const w = s + a * (sp.x - x0) + b * (sp.z - z0);
982
+ return sp.k * (w - set) > sp.cap ? w - sp.cap / sp.k : set;
983
+ });
984
+ };
985
+ var stiffnessCentre = (springs) => {
986
+ let k = 0, x = 0, z = 0;
987
+ for (const s of springs) {
988
+ k += s.k;
989
+ x += s.k * s.x;
990
+ z += s.k * s.z;
991
+ }
992
+ return k > 0 ? [x / k, z / k] : [0, 0];
993
+ };
994
+ var cholesky3 = (J, r) => {
995
+ const L = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
996
+ for (let i = 0; i < 3; i++) {
997
+ for (let j = 0; j <= i; j++) {
998
+ let sum = J[i][j];
999
+ for (let k = 0; k < j; k++) sum -= L[i][k] * L[j][k];
1000
+ if (i === j) {
1001
+ if (!(sum > 1e-9 * Math.max(1, Math.abs(J[i][i])))) return null;
1002
+ L[i][i] = Math.sqrt(sum);
1003
+ } else L[i][j] = sum / L[j][j];
1004
+ }
1005
+ }
1006
+ const y = [0, 0, 0];
1007
+ for (let i = 0; i < 3; i++) {
1008
+ let sum = r[i];
1009
+ for (let k = 0; k < i; k++) sum -= L[i][k] * y[k];
1010
+ y[i] = sum / L[i][i];
1011
+ }
1012
+ const x = [0, 0, 0];
1013
+ for (let i = 2; i >= 0; i--) {
1014
+ let sum = y[i];
1015
+ for (let k = i + 1; k < 3; k++) sum -= L[k][i] * x[k];
1016
+ x[i] = sum / L[i][i];
1017
+ }
1018
+ return x;
1019
+ };
1020
+ var solveFoundation = (springs, weight, xg, zg, height, imperfection, start = { s: 0, a: 0, b: 0 }) => {
1021
+ const [x0, z0] = stiffnessCentre(springs);
1022
+ const capSum = springs.reduce((sum, s2) => sum + s2.cap, 0);
1023
+ const span = Math.max(1, ...springs.map((s2) => Math.max(Math.abs(s2.x - x0) + s2.hx, Math.abs(s2.z - z0) + s2.hz)));
1024
+ let s = start.s, a = start.a, b = start.b;
1025
+ if (springs.every((sp) => s + a * (sp.x - x0) + b * (sp.z - z0) - (sp.set ?? 0) <= 0)) {
1026
+ s = weight / Math.max(1e-9, springs.reduce((sum, sp) => sum + sp.k, 0));
1027
+ a = b = 0;
1028
+ }
1029
+ const Wh = weight * height;
1030
+ const evaluate = () => {
1031
+ let F = 0, Mx = 0, Mz = 0, edge = 0, yielded = false;
1032
+ const Jt = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
1033
+ const Je = [[0, 0, 0], [0, 0, 0], [0, 0, 0]];
1034
+ const add = (J, k, dx, dz) => {
1035
+ J[0][0] += k;
1036
+ J[0][1] += k * dx;
1037
+ J[0][2] += k * dz;
1038
+ J[1][1] += k * dx * dx;
1039
+ J[1][2] += k * dx * dz;
1040
+ J[2][2] += k * dz * dz;
1041
+ };
1042
+ for (const sp of springs) {
1043
+ const dx = sp.x - x0, dz = sp.z - z0;
1044
+ const w = s + a * dx + b * dz - (sp.set ?? 0);
1045
+ if (w <= 0) continue;
1046
+ const f = sp.k * w;
1047
+ add(Je, sp.k, dx, dz);
1048
+ let force = f;
1049
+ if (f >= sp.cap) {
1050
+ force = sp.cap;
1051
+ yielded = true;
1052
+ } else add(Jt, sp.k, dx, dz);
1053
+ edge = Math.max(edge, force / sp.cap);
1054
+ F += force;
1055
+ Mx += force * dx;
1056
+ Mz += force * dz;
1057
+ }
1058
+ for (const J of [Jt, Je]) {
1059
+ J[1][0] = J[0][1];
1060
+ J[2][0] = J[0][2];
1061
+ J[2][1] = J[1][2];
1062
+ J[1][1] -= Wh;
1063
+ J[2][2] -= Wh;
1064
+ }
1065
+ const r = [weight - F, weight * (xg - x0 + height * (a + imperfection[0])) - Mx, weight * (zg - z0 + height * (b + imperfection[1])) - Mz];
1066
+ return { r, Jt, Je, edge, yielded };
1067
+ };
1068
+ let ev = evaluate();
1069
+ let converged = false;
1070
+ const tol = 1e-7 * weight;
1071
+ for (let it = 0; it < 120; it++) {
1072
+ if (Math.abs(ev.r[0]) < tol && Math.abs(ev.r[1]) < tol * span && Math.abs(ev.r[2]) < tol * span) {
1073
+ converged = true;
1074
+ break;
1075
+ }
1076
+ const d = cholesky3(ev.Jt, ev.r) ?? cholesky3(ev.Je, ev.r);
1077
+ if (!d) break;
1078
+ s += d[0];
1079
+ a += d[1];
1080
+ b += d[2];
1081
+ if (!Number.isFinite(s + a + b) || Math.abs(a) > 2 || Math.abs(b) > 2) break;
1082
+ ev = evaluate();
1083
+ }
1084
+ const stable = converged && cholesky3(ev.Jt, [0, 0, 0]) !== null;
1085
+ return { s, a, b, stable, yielded: ev.yielded, bearing: capSum > 0 ? weight / capSum : Infinity, edge: ev.edge, residual: [ev.r[0], ev.r[1], ev.r[2]] };
1086
+ };
1087
+
1088
+ // src/data/wind-terrain.json
1089
+ var wind_terrain_default = [
1090
+ { id: "I", zg: 250, b: { A: 1.1, B: 1.11, C: 1.12 }, p: { A: 0.06, B: 0.065, C: 0.07 }, z0: 5e-3 },
1091
+ { id: "II", zg: 300, b: { A: 1, B: 1, C: 1 }, p: { A: 0.085, B: 0.09, C: 0.1 }, z0: 0.05 },
1092
+ { id: "III", zg: 350, b: { A: 0.94, B: 0.94, C: 0.93 }, p: { A: 0.1, B: 0.105, C: 0.115 }, z0: 0.3 },
1093
+ { id: "IV", zg: 420, b: { A: 0.86, B: 0.85, C: 0.84 }, p: { A: 0.12, B: 0.125, C: 0.135 }, z0: 1 },
1094
+ { id: "V", zg: 500, b: { A: 0.74, B: 0.73, C: 0.71 }, p: { A: 0.15, B: 0.16, C: 0.175 }, z0: 2.5 }
1095
+ ];
1096
+
1097
+ // src/wind.ts
1098
+ var WIND_TERRAIN = wind_terrain_default;
1099
+ var WIND_DEFAULTS = {
1100
+ speed: 40,
1101
+ direction: [1, 0, 0],
1102
+ terrain: "III",
1103
+ class: "auto",
1104
+ topography: 1,
1105
+ statistical: 1,
1106
+ drag: 1.3,
1107
+ damping: 0.016,
1108
+ airDensity: 1.226,
1109
+ minHeight: 5,
1110
+ bandHeight: 3,
1111
+ stiffnessFactor: 0.8,
1112
+ beamFactor: 0.4,
1113
+ flange: 0.1,
1114
+ driftLimit: 1 / 500,
1115
+ loadFactor: 1.4,
1116
+ chord: false,
1117
+ meanRatio: 0.69,
1118
+ gustFactor: { A: 1, B: 0.98, C: 0.95 },
1119
+ gust: { components: 400, maxHz: 2, scale: 1200, seed: 1 },
1120
+ peakDecay: 60,
1121
+ tuning: "den-hartog"
1122
+ };
1123
+ var definedOf2 = (o) => Object.fromEntries(Object.entries(o ?? {}).filter(([, v]) => v !== void 0));
1124
+ var resolveWind = (o = {}) => {
1125
+ const terrains = new Map(WIND_TERRAIN.map((t) => [t.id, t]));
1126
+ for (const t of o.terrains ?? []) terrains.set(t.id, t);
1127
+ const { terrains: _terrains, gustFactor, gust, ...rest } = o;
1128
+ return {
1129
+ ...WIND_DEFAULTS,
1130
+ ...definedOf2(rest),
1131
+ direction: o.direction ?? WIND_DEFAULTS.direction,
1132
+ gustFactor: { ...WIND_DEFAULTS.gustFactor, ...definedOf2(gustFactor) },
1133
+ gust: { ...WIND_DEFAULTS.gust, ...definedOf2(gust) },
1134
+ terrains
1135
+ };
1136
+ };
1137
+ var exposureOf = (w, spec) => spec ? { ...w, ...definedOf2(spec) } : w;
1138
+ var windPressure = (speed, density = WIND_DEFAULTS.airDensity) => 0.5 * density * speed * speed;
1139
+ var windClass = (largest) => largest <= 20 ? "A" : largest <= 50 ? "B" : "C";
1140
+ var heightFactor = (z, terrain, cls, o = {}) => {
1141
+ const zz = Math.min(terrain.zg, Math.max(o.minHeight ?? WIND_DEFAULTS.minHeight, z));
1142
+ return terrain.b[cls] * (o.gustFactor ?? WIND_DEFAULTS.gustFactor)[cls] * (zz / 10) ** terrain.p[cls];
1143
+ };
1144
+ var meanHeightFactor = (z, terrain, cls, o = {}) => (o.meanRatio ?? WIND_DEFAULTS.meanRatio) * heightFactor(z, terrain, cls, { gustFactor: { A: 1, B: 1, C: 1 }, ...o.minHeight !== void 0 ? { minHeight: o.minHeight } : {} });
1145
+ var turbulenceIntensity = (z, z0) => Math.min(0.5, 1 / Math.log(Math.max(z, 1.5 * z0) / z0));
1146
+ var solveWallFrame = (hb, ei, ga, V) => {
1147
+ const n = hb.length;
1148
+ const diag = new Array(n + 1).fill(0);
1149
+ const off = new Array(n).fill(0);
1150
+ const rhs = new Array(n + 1).fill(0);
1151
+ for (let j = 0; j < n; j++) {
1152
+ const h = hb[j], e = ei[j], g = ga[j];
1153
+ diag[j] += e / h + g * h / 3;
1154
+ diag[j + 1] += e / h + g * h / 3;
1155
+ off[j] = -e / h + g * h / 6;
1156
+ rhs[j] += h / 6 * (2 * V[j] + V[j + 1]);
1157
+ rhs[j + 1] += h / 6 * (V[j] + 2 * V[j + 1]);
1158
+ }
1159
+ const wall = ei[0] / (ei[0] + ga[0] * hb[0] * hb[0]);
1160
+ diag[0] += 1e6 * (ei[0] / hb[0] + ga[0] * hb[0]) * wall;
1161
+ const c = new Array(n + 1).fill(0);
1162
+ const d = new Array(n + 1).fill(0);
1163
+ const psi = new Array(n + 1).fill(0);
1164
+ c[0] = off[0] / diag[0];
1165
+ d[0] = rhs[0] / diag[0];
1166
+ for (let i = 1; i <= n; i++) {
1167
+ const m = diag[i] - off[i - 1] * c[i - 1];
1168
+ c[i] = i < n ? off[i] / m : 0;
1169
+ d[i] = (rhs[i] - off[i - 1] * d[i - 1]) / m;
1170
+ }
1171
+ psi[n] = d[n];
1172
+ for (let i = n - 1; i >= 0; i--) psi[i] = d[i] - c[i] * psi[i + 1];
1173
+ const w = new Array(n + 1).fill(0);
1174
+ for (let j = 0; j < n; j++) w[j + 1] = w[j] + hb[j] * (psi[j] + psi[j + 1]) / 2;
1175
+ return { psi, w };
1176
+ };
1177
+ var shearNodes = (f) => {
1178
+ const V = new Array(f.length + 1).fill(0);
1179
+ for (let j = f.length - 1; j >= 0; j--) V[j] = V[j + 1] + f[j];
1180
+ return V;
1181
+ };
1182
+ var momentNodes = (hb, V) => {
1183
+ const M = new Array(V.length).fill(0);
1184
+ for (let j = hb.length - 1; j >= 0; j--) M[j] = M[j + 1] + hb[j] * (V[j] + V[j + 1]) / 2;
1185
+ return M;
1186
+ };
1187
+ var RIGID = 1e3;
1188
+ var tBeamInertia = (bw, h, bf, tf) => {
1189
+ const web = bw * h;
1190
+ const fl = Math.max(0, bf - bw) * Math.min(tf, h);
1191
+ const t = Math.min(tf, h);
1192
+ const yc = (web * (h / 2) + fl * (h - t / 2)) / (web + fl);
1193
+ return bw * h ** 3 / 12 + web * (h / 2 - yc) ** 2 + Math.max(0, bf - bw) * t ** 3 / 12 + fl * (h - t / 2 - yc) ** 2;
1194
+ };
1195
+ var shapeAt = (a, z) => {
1196
+ const { z: zs, phi } = a.shape;
1197
+ if (z <= zs[0]) return phi[0];
1198
+ for (let i = 1; i < zs.length; i++) if (z <= zs[i]) return phi[i - 1] + (phi[i] - phi[i - 1]) * (z - zs[i - 1]) / (zs[i] - zs[i - 1]);
1199
+ return phi[phi.length - 1];
1200
+ };
1201
+ var analyzeWind = (parts, c, ground) => {
1202
+ const [dx, dz] = c.direction;
1203
+ const along = (p) => {
1204
+ const cx = (p.min[0] + p.max[0]) / 2, cz = (p.min[2] + p.max[2]) / 2;
1205
+ const hx = (p.max[0] - p.min[0]) / 2, hz = (p.max[2] - p.min[2]) / 2;
1206
+ return {
1207
+ u: cx * dx + cz * dz,
1208
+ v: -cx * dz + cz * dx,
1209
+ du: 2 * (Math.abs(dx) * hx + Math.abs(dz) * hz),
1210
+ dv: 2 * (Math.abs(dz) * hx + Math.abs(dx) * hz)
1211
+ };
1212
+ };
1213
+ const g = parts.map(along);
1214
+ let topY = ground;
1215
+ let resists = false;
1216
+ for (const p of parts) {
1217
+ if (p.kind === "foundation") continue;
1218
+ topY = Math.max(topY, p.max[1]);
1219
+ if ((p.kind === "wall" || p.kind === "column") && p.elasticPa) resists = true;
1220
+ }
1221
+ const height = topY - ground;
1222
+ if (!(height > 0) || !resists) return null;
1223
+ const n = Math.max(1, Math.ceil(height / c.bandHeight - 1e-9));
1224
+ const bands = Array.from({ length: n }, (_, j) => Math.min(c.bandHeight, height - j * c.bandHeight));
1225
+ const z0 = Array.from({ length: n }, (_, j) => j * c.bandHeight);
1226
+ const mid = bands.map((h, j) => z0[j] + h / 2);
1227
+ const share = (p, j) => {
1228
+ const a = Math.max(p.min[1] - ground, z0[j]), b = Math.min(p.max[1] - ground, z0[j] + bands[j]);
1229
+ return Math.max(0, b - a);
1230
+ };
1231
+ const vmin = new Array(n).fill(Infinity), vmax = new Array(n).fill(-Infinity);
1232
+ const mass = new Array(n).fill(0);
1233
+ let weight = 0, wu = 0;
1234
+ let footEnd = -Infinity;
1235
+ let anyFoundation = false;
1236
+ for (let i = 0; i < parts.length; i++) {
1237
+ const p = parts[i];
1238
+ weight += p.mass * GRAVITY;
1239
+ wu += p.mass * g[i].u;
1240
+ if (p.kind === "foundation") {
1241
+ anyFoundation = true;
1242
+ footEnd = Math.max(footEnd, g[i].u + g[i].du / 2);
1243
+ continue;
1244
+ }
1245
+ const full = Math.max(1e-6, p.max[1] - p.min[1]);
1246
+ for (let j = 0; j < n; j++) {
1247
+ const ov = share(p, j);
1248
+ if (ov <= 0) continue;
1249
+ vmin[j] = Math.min(vmin[j], g[i].v - g[i].dv / 2);
1250
+ vmax[j] = Math.max(vmax[j], g[i].v + g[i].dv / 2);
1251
+ if (!p.damper) mass[j] += p.mass * ov / full;
1252
+ }
1253
+ }
1254
+ if (!anyFoundation) {
1255
+ for (let i = 0; i < parts.length; i++) if (parts[i].min[1] - ground < 0.1) footEnd = Math.max(footEnd, g[i].u + g[i].du / 2);
1256
+ }
1257
+ const totalMass = weight / GRAVITY;
1258
+ const uCg = totalMass > 0 ? wu / totalMass : 0;
1259
+ const width = vmax.map((hi, j) => hi > vmin[j] ? hi - vmin[j] : 0);
1260
+ const widest = Math.max(...width);
1261
+ const cls = c.cls === "auto" ? windClass(Math.max(height, widest)) : c.cls;
1262
+ const k0 = c.speed * c.topography * c.statistical;
1263
+ const pressure = mid.map((z2) => windPressure(k0 * heightFactor(z2, c.terrain, cls, { gustFactor: c.gustFactor, minHeight: c.minHeight }), c.airDensity));
1264
+ const meanPressure = mid.map((z2) => windPressure(k0 * meanHeightFactor(z2, c.terrain, cls, { meanRatio: c.meanRatio, minHeight: c.minHeight }), c.airDensity));
1265
+ const forces = bands.map((h, j) => pressure[j] * c.drag * width[j] * h);
1266
+ const meanForces = bands.map((h, j) => meanPressure[j] * c.drag * width[j] * h);
1267
+ const V = shearNodes(forces);
1268
+ const M = momentNodes(bands, V);
1269
+ const ea = new Array(n).fill(0), eau = new Array(n).fill(0);
1270
+ const vertical = [];
1271
+ for (let i = 0; i < parts.length; i++) {
1272
+ const p = parts[i];
1273
+ if (p.kind !== "wall" && !(p.kind === "column" && c.chord) || !p.elasticPa) continue;
1274
+ vertical.push(i);
1275
+ const area = g[i].du * g[i].dv;
1276
+ const e = p.elasticPa * c.stiffnessFactor;
1277
+ for (let j = 0; j < n; j++) {
1278
+ const w = share(p, j) / bands[j];
1279
+ if (w <= 0) continue;
1280
+ ea[j] += e * area * w;
1281
+ eau[j] += e * area * g[i].u * w;
1282
+ }
1283
+ }
1284
+ const centre = ea.map((v, j) => v > 0 ? eau[j] / v : 0);
1285
+ const ei = new Array(n).fill(0);
1286
+ const ga = new Array(n).fill(0);
1287
+ for (const i of vertical) {
1288
+ const p = parts[i];
1289
+ const e = p.elasticPa * c.stiffnessFactor;
1290
+ for (let j = 0; j < n; j++) {
1291
+ const w = share(p, j) / bands[j];
1292
+ if (w <= 0) continue;
1293
+ ei[j] += e * (g[i].du ** 3 * g[i].dv / 12 + g[i].du * g[i].dv * (g[i].u - centre[j]) ** 2) * w;
1294
+ }
1295
+ }
1296
+ for (let i = 0; i < parts.length; i++) {
1297
+ const p = parts[i];
1298
+ if (p.kind !== "column" || !p.elasticPa) continue;
1299
+ const h = Math.max(1e-6, p.max[1] - p.min[1]);
1300
+ const kc = p.elasticPa * c.stiffnessFactor * (g[i].du ** 3 * g[i].dv) / 12 / h;
1301
+ let beams = 0;
1302
+ for (const b of p.beams ?? []) {
1303
+ const q = parts[b];
1304
+ if (!q?.elasticPa) continue;
1305
+ const hx = q.max[0] - q.min[0], hz = q.max[2] - q.min[2];
1306
+ const long = Math.max(hx, hz), short = Math.min(hx, hz);
1307
+ const cos = hx >= hz ? Math.abs(dx) : Math.abs(dz);
1308
+ const depth = q.max[1] - q.min[1];
1309
+ const inertia = tBeamInertia(short, depth, short + 2 * c.flange * long, q.slab ?? 0);
1310
+ beams += cos * cos * q.elasticPa * c.beamFactor * inertia / Math.max(long, 1e-6);
1311
+ }
1312
+ const k = beams / kc;
1313
+ const base = p.min[1] - ground < 0.05;
1314
+ const a = base ? (0.5 + k) / (2 + k) : k / (2 + k);
1315
+ const stiffness = a * 12 * (p.elasticPa * c.stiffnessFactor) * (g[i].du ** 3 * g[i].dv / 12) / (h * h);
1316
+ for (let j = 0; j < n; j++) ga[j] += stiffness * share(p, j) / bands[j];
1317
+ }
1318
+ let last = -1;
1319
+ for (let j = 0; j < n; j++) if (ei[j] > 0 || ga[j] > 0) last = j;
1320
+ for (let j = last + 1; j < n && last >= 0; j++) {
1321
+ ei[j] = ei[last] * RIGID;
1322
+ ga[j] = ga[last] * RIGID;
1323
+ }
1324
+ const tiny = 1;
1325
+ const eiS = ei.map((v) => Math.max(v, tiny));
1326
+ const gaS = ga.map((v) => Math.max(v, tiny));
1327
+ const sol = solveWallFrame(bands, eiS, gaS, V);
1328
+ const deflection = sol.w[n];
1329
+ const lean = solveWallFrame(bands, eiS, gaS, shearNodes(meanForces)).w[n];
1330
+ const kappa = bands.map((h, j) => (sol.psi[j + 1] - sol.psi[j]) / h);
1331
+ const Mmid = bands.map((h, j) => M[j + 1] + h / 2 * (V[j + 1] + (V[j] + V[j + 1]) / 2) / 2);
1332
+ const Mwall = ei.map((v, j) => Math.min(Mmid[j], Math.max(0, v * kappa[j])));
1333
+ const axial = new Array(parts.length).fill(0);
1334
+ for (const i of vertical) {
1335
+ const p = parts[i];
1336
+ let sum = 0, tot = 0;
1337
+ for (let j = 0; j < n; j++) {
1338
+ const w = share(p, j);
1339
+ if (w <= 0) continue;
1340
+ sum += w * p.elasticPa * c.stiffnessFactor * kappa[j] * g[i].du * g[i].dv * (g[i].u - centre[j]);
1341
+ tot += w;
1342
+ }
1343
+ axial[i] = tot > 0 ? sum / tot : 0;
1344
+ }
1345
+ if (!c.chord) {
1346
+ const colA = new Array(n).fill(0), colAu = new Array(n).fill(0);
1347
+ const cols = [];
1348
+ for (let i = 0; i < parts.length; i++) {
1349
+ if (parts[i].kind !== "column") continue;
1350
+ cols.push(i);
1351
+ for (let j = 0; j < n; j++) {
1352
+ const w = share(parts[i], j) / bands[j];
1353
+ colA[j] += g[i].du * g[i].dv * w;
1354
+ colAu[j] += g[i].du * g[i].dv * g[i].u * w;
1355
+ }
1356
+ }
1357
+ const colC = colA.map((a, j) => a > 0 ? colAu[j] / a : 0);
1358
+ const colI = new Array(n).fill(0);
1359
+ for (const i of cols) for (let j = 0; j < n; j++) colI[j] += g[i].du * g[i].dv * (g[i].u - colC[j]) ** 2 * share(parts[i], j) / bands[j];
1360
+ for (const i of cols) {
1361
+ let sum = 0, tot = 0;
1362
+ for (let j = 0; j < n; j++) {
1363
+ const w = share(parts[i], j);
1364
+ if (w <= 0 || colI[j] <= 0) continue;
1365
+ sum += w * (Math.max(0, Mmid[j] - Mwall[j]) * g[i].du * g[i].dv * (g[i].u - colC[j])) / colI[j];
1366
+ tot += w;
1367
+ }
1368
+ axial[i] = tot > 0 ? sum / tot : 0;
1369
+ }
1370
+ }
1371
+ const arm = footEnd - uCg;
1372
+ const overturn = arm > 0 ? c.loadFactor * M[0] / (weight * arm) : Infinity;
1373
+ const mode = solveWallFrame(bands, eiS, gaS, shearNodes(mass));
1374
+ const top = mode.w[n];
1375
+ const um = bands.map((_, j) => (mode.w[j] + mode.w[j + 1]) / 2);
1376
+ let mu = 0, muu = 0;
1377
+ mass.forEach((m, j) => {
1378
+ mu += m * um[j];
1379
+ muu += m * um[j] * um[j];
1380
+ });
1381
+ const omega = muu > 0 ? Math.sqrt(mu / muu) : 0;
1382
+ const phi = um.map((u) => top > 0 ? u / top : 0);
1383
+ const modalMass = mass.reduce((s, m, j) => s + m * phi[j] * phi[j], 0);
1384
+ const modalForce = meanForces.reduce((s, f, j) => s + f * phi[j], 0);
1385
+ const z = [0, ...bands.map((_, j) => z0[j] + bands[j])];
1386
+ const intensity = turbulenceIntensity(Math.max(0.6 * height, c.minHeight), c.terrain.z0);
1387
+ return {
1388
+ height,
1389
+ cls,
1390
+ width: widest,
1391
+ bands,
1392
+ mid,
1393
+ forces,
1394
+ meanForces,
1395
+ pressure: pressure[n - 1],
1396
+ shear: V[0],
1397
+ moment: M[0],
1398
+ deflection,
1399
+ drift: deflection / height,
1400
+ lean,
1401
+ weight,
1402
+ overturn,
1403
+ wallShare: M[0] > 0 ? Mwall[0] / M[0] : 0,
1404
+ ei,
1405
+ ga,
1406
+ frequency: omega / (2 * Math.PI),
1407
+ omega,
1408
+ modalMass,
1409
+ modalStiffness: omega * omega * modalMass,
1410
+ shape: { z, phi: mode.w.map((w) => top > 0 ? w / top : 0) },
1411
+ modalForce,
1412
+ intensity,
1413
+ axial
1414
+ };
1415
+ };
1416
+ var damperTuning = (mu, kind = "den-hartog") => kind === "warburton" ? { frequency: Math.sqrt(1 - mu / 2) / (1 + mu), damping: Math.sqrt(mu * (1 - mu / 4) / (4 * (1 + mu) * (1 - mu / 2))) } : { frequency: 1 / (1 + mu), damping: Math.sqrt(3 * mu / (8 * (1 + mu) ** 3)) };
1417
+ var damperAmplification = (r, mu, g, damper, structure = 0) => {
1418
+ const mul = (a, b) => [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]];
1419
+ const div = (a, b) => {
1420
+ const d = b[0] * b[0] + b[1] * b[1];
1421
+ return [(a[0] * b[0] + a[1] * b[1]) / d, (a[1] * b[0] - a[0] * b[1]) / d];
1422
+ };
1423
+ const A = [g * g, 2 * damper * g * r];
1424
+ const main = [1 - r * r, 2 * structure * r];
1425
+ const coupling = mu > 0 ? mul([mu * r * r, 0], div(A, [A[0] - r * r, A[1]])) : [0, 0];
1426
+ const total = [main[0] - coupling[0], main[1] - coupling[1]];
1427
+ return 1 / Math.hypot(total[0], total[1]);
1428
+ };
1429
+ var makeGust = (v10, o) => {
1430
+ const rng = createRng(o.seed);
1431
+ const n = Math.max(1, Math.floor(o.components));
1432
+ const df = o.maxHz / n;
1433
+ const freq = Array.from({ length: n }, (_, k) => (k + 0.5) * df);
1434
+ const weight = freq.map((f) => {
1435
+ const x = o.scale * f / Math.max(v10, 1e-6);
1436
+ return 4 * x * x / (1 + x * x) ** (4 / 3) / f * df;
1437
+ });
1438
+ const sum = weight.reduce((s, w) => s + w, 0);
1439
+ return { freq, amp: weight.map((w) => Math.sqrt(2 * w / sum)), phase: freq.map(() => rng.next() * 2 * Math.PI) };
1440
+ };
1441
+ var gustAt = (gust, t) => {
1442
+ let s = 0;
1443
+ for (let k = 0; k < gust.freq.length; k++) s += gust.amp[k] * Math.cos(2 * Math.PI * gust.freq[k] * t + gust.phase[k]);
1444
+ return s;
1445
+ };
1446
+ var stepSway = (s, sys, t, dt, force, maxStep = Infinity) => {
1447
+ const steps = Math.max(1, Math.ceil(dt / maxStep - 1e-9));
1448
+ const h = dt / steps;
1449
+ const d = sys.damper;
1450
+ const f = (x, v, y, w, tt) => {
1451
+ const fd = d ? d.k * (y - x) + d.c * (w - v) : 0;
1452
+ return [v, (force(tt) - sys.K * x - sys.C * v + fd) / sys.M, d ? w : 0, d ? -fd / d.m : 0];
1453
+ };
1454
+ for (let i = 0; i < steps; i++) {
1455
+ const tt = t + i * h;
1456
+ const k1 = f(s.x, s.v, s.y, s.w, tt);
1457
+ const k2 = f(s.x + h / 2 * k1[0], s.v + h / 2 * k1[1], s.y + h / 2 * k1[2], s.w + h / 2 * k1[3], tt + h / 2);
1458
+ const k3 = f(s.x + h / 2 * k2[0], s.v + h / 2 * k2[1], s.y + h / 2 * k2[2], s.w + h / 2 * k2[3], tt + h / 2);
1459
+ const k4 = f(s.x + h * k3[0], s.v + h * k3[1], s.y + h * k3[2], s.w + h * k3[3], tt + h);
1460
+ s.x += h / 6 * (k1[0] + 2 * k2[0] + 2 * k3[0] + k4[0]);
1461
+ s.v += h / 6 * (k1[1] + 2 * k2[1] + 2 * k3[1] + k4[1]);
1462
+ s.y += h / 6 * (k1[2] + 2 * k2[2] + 2 * k3[2] + k4[2]);
1463
+ s.w += h / 6 * (k1[3] + 2 * k2[3] + 2 * k3[3] + k4[3]);
1464
+ }
1465
+ };
1466
+ var tensionCapacity = (dims, o) => {
1467
+ const section = dims[0] * dims[2];
1468
+ if (o.reinforcement) {
1469
+ const bars = verticalBarCount(dims, thinAxisOf(dims), o.reinforcement.spacing);
1470
+ return bars * Math.PI * (o.reinforcement.diameter / 2) ** 2 * o.yieldMPa * 1e6;
1471
+ }
1472
+ if (o.ratio) return o.ratio * section * o.yieldMPa * 1e6;
1473
+ return (o.tensileMPa ?? 0) * 1e6 * section;
1474
+ };
1475
+
631
1476
  // src/data/materials.json
632
1477
  var materials_default = [
633
1478
  {
@@ -635,6 +1480,8 @@ var GameDestruction = (() => {
635
1480
  density: 7850,
636
1481
  hpPerKg: 400,
637
1482
  penetration: 110,
1483
+ penetrationClass: "steel",
1484
+ penetrationScale: 1.3,
638
1485
  ductility: 0.9,
639
1486
  fracture: "none",
640
1487
  damageScale: {
@@ -664,6 +1511,8 @@ var GameDestruction = (() => {
664
1511
  density: 7200,
665
1512
  hpPerKg: 150,
666
1513
  penetration: 160,
1514
+ penetrationClass: "steel",
1515
+ penetrationScale: 1,
667
1516
  ductility: 0.2,
668
1517
  fracture: "snap",
669
1518
  fragmentSize: [
@@ -697,6 +1546,8 @@ var GameDestruction = (() => {
697
1546
  density: 2700,
698
1547
  hpPerKg: 250,
699
1548
  penetration: 60,
1549
+ penetrationClass: "steel",
1550
+ penetrationScale: 3,
700
1551
  ductility: 0.85,
701
1552
  fracture: "none",
702
1553
  damageScale: {
@@ -725,6 +1576,8 @@ var GameDestruction = (() => {
725
1576
  density: 8960,
726
1577
  hpPerKg: 300,
727
1578
  penetration: 70,
1579
+ penetrationClass: "steel",
1580
+ penetrationScale: 1.5,
728
1581
  ductility: 0.95,
729
1582
  fracture: "none",
730
1583
  damageScale: {
@@ -753,6 +1606,8 @@ var GameDestruction = (() => {
753
1606
  density: 500,
754
1607
  hpPerKg: 90,
755
1608
  penetration: 9,
1609
+ penetrationClass: "wood",
1610
+ penetrationScale: 1.5,
756
1611
  ductility: 0.35,
757
1612
  fracture: "splinter",
758
1613
  fragmentSize: [
@@ -789,6 +1644,8 @@ var GameDestruction = (() => {
789
1644
  density: 750,
790
1645
  hpPerKg: 140,
791
1646
  penetration: 14,
1647
+ penetrationClass: "wood",
1648
+ penetrationScale: 1,
792
1649
  ductility: 0.3,
793
1650
  fracture: "splinter",
794
1651
  fragmentSize: [
@@ -824,6 +1681,8 @@ var GameDestruction = (() => {
824
1681
  density: 600,
825
1682
  hpPerKg: 70,
826
1683
  penetration: 7,
1684
+ penetrationClass: "wood",
1685
+ penetrationScale: 2,
827
1686
  ductility: 0.3,
828
1687
  fracture: "snap",
829
1688
  fragmentSize: [
@@ -859,6 +1718,8 @@ var GameDestruction = (() => {
859
1718
  density: 750,
860
1719
  hpPerKg: 45,
861
1720
  penetration: 6,
1721
+ penetrationClass: "wood",
1722
+ penetrationScale: 2.5,
862
1723
  ductility: 0.15,
863
1724
  fracture: "snap",
864
1725
  fragmentSize: [
@@ -987,6 +1848,8 @@ var GameDestruction = (() => {
987
1848
  density: 2300,
988
1849
  hpPerKg: 14,
989
1850
  penetration: 18,
1851
+ penetrationClass: "concrete",
1852
+ penetrationScale: 1,
990
1853
  ductility: 0.03,
991
1854
  fracture: "crumble",
992
1855
  fragmentSize: [
@@ -1006,13 +1869,20 @@ var GameDestruction = (() => {
1006
1869
  "masonry",
1007
1870
  "brittle"
1008
1871
  ],
1009
- compressive: 444e3
1872
+ compressive: 444e3,
1873
+ strength: {
1874
+ compressiveMPa: 25,
1875
+ tensileMPa: 2.56,
1876
+ elasticGPa: 28
1877
+ }
1010
1878
  },
1011
1879
  {
1012
1880
  id: "brick",
1013
1881
  density: 1900,
1014
1882
  hpPerKg: 12,
1015
1883
  penetration: 14,
1884
+ penetrationClass: "brick",
1885
+ penetrationScale: 1,
1016
1886
  ductility: 0.05,
1017
1887
  fracture: "crumble",
1018
1888
  fragmentSize: [
@@ -1029,13 +1899,20 @@ var GameDestruction = (() => {
1029
1899
  "masonry",
1030
1900
  "brittle"
1031
1901
  ],
1032
- compressive: 4e4
1902
+ compressive: 4e4,
1903
+ strength: {
1904
+ compressiveMPa: 2.35,
1905
+ tensileMPa: 0.1,
1906
+ elasticGPa: 2.35
1907
+ }
1033
1908
  },
1034
1909
  {
1035
1910
  id: "plaster",
1036
1911
  density: 850,
1037
1912
  hpPerKg: 18,
1038
1913
  penetration: 3,
1914
+ penetrationClass: "brick",
1915
+ penetrationScale: 3,
1039
1916
  ductility: 0.05,
1040
1917
  fracture: "crumble",
1041
1918
  fragmentSize: [
@@ -1059,6 +1936,8 @@ var GameDestruction = (() => {
1059
1936
  density: 2700,
1060
1937
  hpPerKg: 25,
1061
1938
  penetration: 25,
1939
+ penetrationClass: "concrete",
1940
+ penetrationScale: 0.9,
1062
1941
  ductility: 0.03,
1063
1942
  fracture: "crumble",
1064
1943
  fragmentSize: [
@@ -1322,6 +2201,8 @@ var GameDestruction = (() => {
1322
2201
  density: 650,
1323
2202
  hpPerKg: 5,
1324
2203
  penetration: 10,
2204
+ penetrationClass: "wood",
2205
+ penetrationScale: 1.2,
1325
2206
  ductility: 0.5,
1326
2207
  fracture: "splinter",
1327
2208
  fragmentSize: [
@@ -1623,36 +2504,210 @@ var GameDestruction = (() => {
1623
2504
  }
1624
2505
  return out;
1625
2506
  };
1626
- var partCapacity = (part, material) => {
2507
+ var partCapacity = (part, material, options = {}) => {
1627
2508
  if (part.structure?.capacity !== void 0) return part.structure.capacity;
1628
2509
  const d = shapeDims(part.shape).sort((a, b) => a - b);
1629
2510
  const role = part.structure?.role;
1630
2511
  const sh = shapeDims(part.shape);
1631
2512
  const section = role === "wall" || role === "partition" ? Math.min(sh[0], sh[2]) * Math.max(sh[0], sh[2]) : role === "slab" ? d[0] * d[2] : d[0] * d[1];
1632
- return Math.max(1, section * (material.compressive ?? 2e4) * (role === "foundation" ? 10 : 1));
2513
+ const foundation = role === "foundation" ? 10 : 1;
2514
+ const strength = material.strength;
2515
+ if (!strength) return Math.max(1, section * (material.compressive ?? 2e4) * foundation);
2516
+ const fc = strength.compressiveMPa * 1e6;
2517
+ let newtons = fc * section;
2518
+ const upright = role === "wall" || role === "partition" || role === "column";
2519
+ if (upright && isSlender(part, material)) newtons *= slendernessFactor(sh[1], Math.min(sh[0], sh[2]), options.masonry);
2520
+ else if (role === "column" && part.reinforcement) {
2521
+ const m = resolveMasonry(options.masonry);
2522
+ const bars = rebarBars(sh, thinAxisOf(sh), part.reinforcement.spacing).length;
2523
+ const steel = bars * Math.PI * (part.reinforcement.diameter / 2) ** 2;
2524
+ newtons = m.concreteFactor * fc * section + steel * resolveFlexure(options.flexure).rebarYieldMPa * 1e6;
2525
+ }
2526
+ return Math.max(1, newtons / GRAVITY * foundation);
1633
2527
  };
1634
2528
  var fragility = (ductility, thickness) => {
1635
2529
  if (ductility >= 0.3) return 1;
1636
2530
  return (1 + 4 * (0.3 - ductility) / 0.3) * Math.min(25, Math.max(1, (0.02 / Math.max(thickness, 1e-4)) ** 2));
1637
2531
  };
1638
2532
 
2533
+ // ../game-contracts/dist/index.js
2534
+ var IMPACT_CONTRACT = {
2535
+ ballistic: { required: ["type", "point", "direction", "energy", "caliber"], optional: ["space", "source"], note: "bullets and pellets: joules and diameter in millimetres" },
2536
+ blunt: { required: ["type", "point", "energy"], optional: ["direction", "radius", "space", "source"], note: "contact radius in metres (default 3 cm)" },
2537
+ slash: { required: ["type", "point", "energy"], optional: ["direction", "radius", "space", "source"], note: "axes, knives, machetes" },
2538
+ explosive: { required: ["type", "point", "energy", "radius"], optional: ["lethalRadius", "space", "source"], note: "point is the centre of the blast, radius the range in metres" },
2539
+ fire: { required: ["type", "point", "temperature", "radius"], optional: ["duration", "space", "source"], note: "flame temperature in \xB0C; duration in seconds (call every frame with `dt`)" },
2540
+ heat: { required: ["type", "point", "temperature", "radius"], optional: ["duration", "space", "source"], note: "heat without flame (cutting torch, hot plate)" }
2541
+ };
2542
+ var checkHit = (hit) => {
2543
+ const c = IMPACT_CONTRACT[hit.type];
2544
+ if (!c) return [`unknown damage type "${String(hit.type)}"`];
2545
+ const out = [];
2546
+ for (const f of c.required) {
2547
+ const v = hit[f];
2548
+ if (v === void 0 || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === "number" && !Number.isFinite(v))) out.push(`${hit.type} hit needs "${f}"`);
2549
+ }
2550
+ return out;
2551
+ };
2552
+ var finiteJson = (v, path, out) => {
2553
+ if (typeof v === "number") {
2554
+ if (!Number.isFinite(v)) out.push(`${path} is not a finite number`);
2555
+ } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out));
2556
+ else if (v && typeof v === "object") for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out);
2557
+ else if (typeof v === "function" || typeof v === "symbol" || typeof v === "bigint") out.push(`${path} is not JSON`);
2558
+ };
2559
+ var checkDestructible = (impl, options = {}) => {
2560
+ const problems = [];
2561
+ const known = options.materials ? new Set(options.materials) : null;
2562
+ let spec;
2563
+ try {
2564
+ spec = impl.describe();
2565
+ } catch (e) {
2566
+ return { ok: false, problems: [`describe() threw: ${String(e)}`] };
2567
+ }
2568
+ if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ["describe() must return at least one part"] };
2569
+ const ids = /* @__PURE__ */ new Set();
2570
+ for (const p of spec.parts) {
2571
+ if (ids.has(p.id)) problems.push(`duplicate part id "${p.id}"`);
2572
+ ids.add(p.id);
2573
+ if (known && !known.has(p.material)) problems.push(`part "${p.id}" has unknown material "${p.material}"`);
2574
+ if (!p.shape || !["box", "cylinder", "sphere"].includes(p.shape.kind)) problems.push(`part "${p.id}" has no valid shape`);
2575
+ if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part "${p.id}" has no position`);
2576
+ }
2577
+ const jointIds = /* @__PURE__ */ new Set();
2578
+ for (const j of spec.joints ?? []) {
2579
+ if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint "${j.id ?? `${j.a}~${j.b}`}" references an unknown part`);
2580
+ const jid = j.id ?? `${j.a}~${j.b}`;
2581
+ if (jointIds.has(jid)) problems.push(`duplicate joint id "${jid}"`);
2582
+ jointIds.add(jid);
2583
+ }
2584
+ const checkState = (label) => {
2585
+ let st;
2586
+ try {
2587
+ st = impl.state();
2588
+ } catch (e) {
2589
+ problems.push(`state() threw ${label}: ${String(e)}`);
2590
+ return;
2591
+ }
2592
+ const sids = st.parts.map((p) => p.id).sort().join(",");
2593
+ if (sids !== [...ids].sort().join(",")) problems.push(`state() part ids do not match describe() ${label}`);
2594
+ for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part "${p.id}" has negative or invalid hp ${label}`);
2595
+ };
2596
+ const checkEvents = (events, label) => {
2597
+ if (!Array.isArray(events)) {
2598
+ problems.push(`${label} must return an array`);
2599
+ return;
2600
+ }
2601
+ const evs = events;
2602
+ evs.forEach((e, i) => {
2603
+ if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object "${String(e.object)}", expected "${impl.id}"`);
2604
+ const part = e.part;
2605
+ if (part !== void 0 && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part "${part}"`);
2606
+ });
2607
+ const bad = [];
2608
+ finiteJson(evs, "events", bad);
2609
+ problems.push(...bad.map((b) => `${label}: ${b}`));
2610
+ try {
2611
+ const s = JSON.stringify(evs);
2612
+ if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`);
2613
+ } catch (e) {
2614
+ problems.push(`${label} events are not JSON-serializable: ${String(e)}`);
2615
+ }
2616
+ };
2617
+ checkState("before the hit");
2618
+ const target = spec.parts[0];
2619
+ try {
2620
+ const hit = { type: "ballistic", point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 };
2621
+ checkEvents(impl.hit(hit), "hit()");
2622
+ } catch (e) {
2623
+ problems.push(`hit() threw: ${String(e)}`);
2624
+ }
2625
+ checkState("after the hit");
2626
+ if (impl.update) {
2627
+ try {
2628
+ checkEvents(impl.update(0.016), "update()");
2629
+ } catch (e) {
2630
+ problems.push(`update() threw: ${String(e)}`);
2631
+ }
2632
+ checkState("after update");
2633
+ }
2634
+ try {
2635
+ impl.reset?.();
2636
+ } catch (e) {
2637
+ problems.push(`reset() threw: ${String(e)}`);
2638
+ }
2639
+ return { ok: problems.length === 0, problems };
2640
+ };
2641
+ var DEFAULT_IGNITION_RULES = {
2642
+ fillCurve: [
2643
+ [0, 0.6],
2644
+ [0.1, 1],
2645
+ [0.35, 0.6],
2646
+ [0.65, 0.3],
2647
+ [0.9, 0.15],
2648
+ [1, 0.12]
2649
+ ],
2650
+ flameFillInfluence: 0.5,
2651
+ flameDefaults: { incendiary: 0.7, tracer: 0.2 },
2652
+ sparkTarget: { steel: 1, iron: 1, aluminium: 0.25 },
2653
+ sparkMetalDefault: 0.5,
2654
+ sparkFluid: { diesel: 0.15 },
2655
+ fireNear: { radius: 1.5, chancePerSecond: 0.5 }
2656
+ };
2657
+ var resolveIgnitionRules = (rules) => ({
2658
+ ...DEFAULT_IGNITION_RULES,
2659
+ ...rules,
2660
+ flameDefaults: { ...DEFAULT_IGNITION_RULES.flameDefaults, ...rules?.flameDefaults },
2661
+ sparkTarget: { ...DEFAULT_IGNITION_RULES.sparkTarget, ...rules?.sparkTarget },
2662
+ sparkFluid: { ...DEFAULT_IGNITION_RULES.sparkFluid, ...rules?.sparkFluid },
2663
+ fireNear: { ...DEFAULT_IGNITION_RULES.fireNear, ...rules?.fireNear }
2664
+ });
2665
+ var fillFactor = (fill, curve = DEFAULT_IGNITION_RULES.fillCurve) => {
2666
+ if (curve.length === 0) return 1;
2667
+ const f = Math.min(1, Math.max(0, fill));
2668
+ if (f <= curve[0][0]) return curve[0][1];
2669
+ for (let i = 1; i < curve.length; i++) {
2670
+ const [x1, y1] = curve[i];
2671
+ const [x0, y0] = curve[i - 1];
2672
+ if (f <= x1) return x1 === x0 ? y1 : y0 + (y1 - y0) * (f - x0) / (x1 - x0);
2673
+ }
2674
+ return curve[curve.length - 1][1];
2675
+ };
2676
+ var sparkTargetFactor = (materialId, metal, rules = DEFAULT_IGNITION_RULES) => !metal ? 0 : rules.sparkTarget[materialId] ?? rules.sparkMetalDefault;
2677
+ var ignitionChance = (hit, target, rules = DEFAULT_IGNITION_RULES) => {
2678
+ const k = fillFactor(target.fill, rules.fillCurve);
2679
+ const spark = (hit.ignition?.spark ?? 0) * target.sparkTarget * (target.fluid === void 0 ? 1 : rules.sparkFluid[target.fluid] ?? 1) * k;
2680
+ const flameBase = hit.ignition?.flame ?? Math.max(hit.incendiary ? rules.flameDefaults.incendiary : 0, hit.tracer ? rules.flameDefaults.tracer : 0);
2681
+ const flame = flameBase * (1 - rules.flameFillInfluence * (1 - k));
2682
+ return 1 - (1 - Math.min(1, Math.max(0, spark))) * (1 - Math.min(1, Math.max(0, flame)));
2683
+ };
2684
+
1639
2685
  // src/world.ts
2686
+ var THIN_SHEET_DISHING = 5e-3;
1640
2687
  var DMG_PER_J = 0.02;
1641
2688
  var BLAST_COUPLING = 0.1;
1642
2689
  var SHELTER_DEFAULTS = { k: 2800, brokenTransmission: 0.5, minPath: 5e-3 };
1643
2690
  var FRAGMENT_HIT_DEFAULTS = { max: 24, energyFactor: 0.5, hitType: "blunt", range: 6, flightTime: 0.4, spread: 10, aim: 40 };
1644
2691
  var CONFINEMENT_DEFAULTS = { rays: 26, reach: 1.5, gain: 1.5, power: 2, max: 3 };
1645
2692
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
2693
+ var VENT_DIAMETER = 8e-3;
2694
+ var FLAME_LIGHTS_LEAK_C = 470;
1646
2695
  var BREACH_RADIUS = 0.55;
1647
2696
  var BREACH_BLAST_FACTOR = 6;
1648
2697
  var BREACH_AREA = 0.25;
1649
- var BREACH_BLAST_REACH = Infinity;
2698
+ var BREACH_BLAST_REACH = 0.12;
1650
2699
  var RESIDUAL_CAPACITY = 0.15;
2700
+ var SUPPORT_TOLERANCE = 0.05;
1651
2701
  var UNIT_FRAGMENT_FACTOR = 4;
1652
2702
  var BREACH_GRID_CAP = 4096;
1653
2703
  var CUT_SLICE = 1;
1654
2704
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
1655
2705
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
2706
+ var penetrationOf = (mat, table) => {
2707
+ if (!table) return void 0;
2708
+ const v = table[mat.penetrationClass ?? mat.id] ?? table[mat.id];
2709
+ return v === void 0 ? void 0 : v * (mat.penetrationScale ?? 1);
2710
+ };
1656
2711
  var capacityScale = (p) => {
1657
2712
  if (p.st.stripped) return p.spec.reinforcement?.residualCapacity ?? RESIDUAL_CAPACITY;
1658
2713
  return p.rt.removed ? Math.max(0.05, 1 - p.rt.removed / p.mass0) : 1;
@@ -1680,8 +2735,14 @@ var GameDestruction = (() => {
1680
2735
  const maxHoles = options.maxHoles ?? 32;
1681
2736
  const fireSpread = options.fireSpread ?? 0.6;
1682
2737
  const breachRadius = options.breach?.radius ?? BREACH_RADIUS;
2738
+ const structureSafety = options.structure?.safetyFactor ?? 1;
2739
+ const flexure = resolveFlexure(options.structure?.flexure);
2740
+ const masonry = resolveMasonry(options.structure?.masonry);
2741
+ const soilCfg = resolveSoil(options.soil);
2742
+ let windOpts = options.wind;
2743
+ let windCfg = windOpts ? resolveWind(windOpts) : null;
2744
+ const ignitionRules = resolveIgnitionRules(options.ignition);
1683
2745
  const breachBlastFactor = options.breach?.blastFactor ?? BREACH_BLAST_FACTOR;
1684
- const breachBlastReach = options.breach?.blastReach ?? BREACH_BLAST_REACH;
1685
2746
  const rng = createRng(options.seed ?? 1);
1686
2747
  const rnd = () => rng.next();
1687
2748
  let time = 0;
@@ -1708,7 +2769,7 @@ var GameDestruction = (() => {
1708
2769
  const pendingCollapse = /* @__PURE__ */ new Map();
1709
2770
  let runDepth = 0;
1710
2771
  const drainBlasts = () => {
1711
- for (let blast = blasts.shift(); blast; blast = blasts.shift()) blastWorld(blast.point, blast.energy, blast.radius, blast.source);
2772
+ for (let blast = blasts.shift(); blast; blast = blasts.shift()) blastWorld(blast.point, blast.energy, blast.radius, blast.source, void 0, void 0, blast.lethalRadius);
1712
2773
  };
1713
2774
  const run = (fn) => {
1714
2775
  const prev = cur;
@@ -1778,7 +2839,8 @@ var GameDestruction = (() => {
1778
2839
  collapsed: new Set(carry?.collapsed ?? []),
1779
2840
  unsup: [],
1780
2841
  anyAnchor: false,
1781
- order: ++orderCounter
2842
+ order: ++orderCounter,
2843
+ base: { pos: [pos[0], pos[1], pos[2]], rot: [rot[0], rot[1], rot[2], rot[3]] }
1782
2844
  };
1783
2845
  for (const ps of spec.parts) {
1784
2846
  if (o.byId.has(ps.id)) throw new Error(`duplicate part id "${ps.id}"`);
@@ -1830,10 +2892,17 @@ var GameDestruction = (() => {
1830
2892
  rt: c?.rt ?? { ...freshRuntime(), prevTemp: pst.temperature },
1831
2893
  structural: !!ps.structure && ps.structure.role !== "none",
1832
2894
  anchored: !!ps.anchored || ps.structure?.role === "foundation",
1833
- cap: ps.structure ? partCapacity(ps, mat) : 0,
2895
+ cap: ps.structure ? partCapacity(ps, mat, { masonry, flexure }) : 0,
2896
+ orphan: false,
1834
2897
  staged,
1835
2898
  thin: thinAxisOf(shapeDims(ps.shape)),
1836
- layers
2899
+ layers,
2900
+ span: 0,
2901
+ spanKey: "",
2902
+ restMoment: 0,
2903
+ bendLoad: 0,
2904
+ snap: false,
2905
+ ...ps.shape.kind === "box" && isFlexural(ps, mat, flexure) ? { bend: bendGeometry(ps) } : {}
1837
2906
  };
1838
2907
  if (p.structural) o.hasStructure = true;
1839
2908
  if (p.rt.chainDue !== void 0 && !isLost(p)) primed.set(p, o);
@@ -1867,7 +2936,14 @@ var GameDestruction = (() => {
1867
2936
  updateBound(o);
1868
2937
  o.anyAnchor = o.parts.some((p) => p.anchored);
1869
2938
  o.unsup = o.parts.map(() => false);
2939
+ if (windCfg && o.hasStructure && spec.wind !== false) o.wind = newWind(o, carry?.wind);
2940
+ const ground = spec.soil ?? soilCfg.site;
2941
+ if (ground !== void 0 && o.parts.some((p) => p.spec.structure?.role === "foundation")) initSoil(o, profileOf(ground), carry?.soil, carry?.soilSets);
2942
+ o.seeding = true;
1870
2943
  solveStructure(o);
2944
+ o.seeding = false;
2945
+ if (o.soil && !carry?.soil) settleSoil(o, 0);
2946
+ if (o.soil) o.soil.dirty = false;
1871
2947
  recomputeGroups(o);
1872
2948
  o.st.destroyed = o.parts.length > 0 && o.parts.every(isLost);
1873
2949
  o.active = o.maxStress > 0.8 || o.parts.some((p) => p.st.burning || p.st.temperature > ambient + 0.5 || (p.st.liters ?? 0) > 0);
@@ -1919,12 +2995,126 @@ var GameDestruction = (() => {
1919
2995
  for (const p of o.parts) if (isLost(p)) p.st.group = -1;
1920
2996
  list.forEach((g, gi) => g.ix.forEach((i) => o.parts[i].st.group = gi));
1921
2997
  };
1922
- const restsOn = (j, r) => {
2998
+ const restsOn = (o, j, r) => {
1923
2999
  const b = j.bears;
1924
3000
  if (!b) return -1;
1925
- if (b === true) return j.ia === r ? j.ib : j.ia;
1926
- if (b === "a_on_b") return j.ia === r ? j.ib : -1;
1927
- return j.ib === r ? j.ia : -1;
3001
+ const k = b === true ? j.ia === r ? j.ib : j.ia : b === "a_on_b" ? j.ia === r ? j.ib : -1 : j.ib === r ? j.ia : -1;
3002
+ if (k < 0 || j.spec.kind !== "tie") return k;
3003
+ return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
3004
+ };
3005
+ const contactBox = (p, support) => {
3006
+ const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
3007
+ const lo = [0, 0, 0], hi = [0, 0, 0];
3008
+ for (let k = 0; k < 3; k++) {
3009
+ const a = Math.max(A.min[k], B.min[k]) - SUPPORT_TOLERANCE, b = Math.min(A.max[k], B.max[k]) + SUPPORT_TOLERANCE;
3010
+ lo[k] = Math.min(a, b);
3011
+ hi[k] = Math.max(a, b);
3012
+ }
3013
+ const out = [[Infinity, Infinity, Infinity], [-Infinity, -Infinity, -Infinity]];
3014
+ for (let c = 0; c < 8; c++) {
3015
+ const l = toLocal(p.pos, p.q, [c & 1 ? hi[0] : lo[0], c & 2 ? hi[1] : lo[1], c & 4 ? hi[2] : lo[2]]);
3016
+ for (let k = 0; k < 3; k++) {
3017
+ out[0][k] = Math.min(out[0][k], l[k]);
3018
+ out[1][k] = Math.max(out[1][k], l[k]);
3019
+ }
3020
+ }
3021
+ return out;
3022
+ };
3023
+ const contactCentre = (p, above) => {
3024
+ const [lo, hi] = contactBox(p, above);
3025
+ return toWorld(p.pos, p.q, [(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2, (lo[2] + hi[2]) / 2]);
3026
+ };
3027
+ const overSupportAt = (o, p, at, lower) => {
3028
+ const g = p.bend;
3029
+ const local = toLocal(p.pos, p.q, at);
3030
+ const depth = g.gravity >= 0 ? g.dims[g.gravity] : Math.min(...g.depthAxes.map((a) => g.dims[a]));
3031
+ let best = -1;
3032
+ let bestGap = depth;
3033
+ for (const k of lower) {
3034
+ const [a, b] = contactBox(p, o.parts[k]);
3035
+ let gap = 0;
3036
+ for (const ax of g.spanAxes) gap = Math.max(gap, a[ax] - local[ax], local[ax] - b[ax]);
3037
+ if (gap <= bestGap) {
3038
+ best = k;
3039
+ bestGap = gap - 1e-9;
3040
+ }
3041
+ }
3042
+ return best;
3043
+ };
3044
+ const sectionOf = (p, axis) => {
3045
+ const g = p.bend;
3046
+ const section = { b: sectionWidth(g, axis), h: g.dims[axis] };
3047
+ const rebar = tensionSteel(p.spec, g.dims, section, flexure);
3048
+ return { section, rebar, ...sectionMoments(p.mat, section, rebar, flexure) };
3049
+ };
3050
+ const restFlexure = (o, p, lower) => {
3051
+ const g = p.bend;
3052
+ const key = lower.join(",");
3053
+ if (key !== p.spanKey) {
3054
+ p.spanKey = key;
3055
+ p.span = spanOf(g, lower.map((k) => contactBox(p, o.parts[k])));
3056
+ }
3057
+ p.restMoment = 0;
3058
+ if (g.gravity < 0 || p.span <= 0) return 0;
3059
+ const sec = sectionOf(p, g.gravity);
3060
+ p.restMoment = spanMoment(lineLoad(g, p.bendLoad) * Math.abs(g.up[g.gravity]), p.span);
3061
+ const holds = (p.st.cracked ? sec.steel : sec.ultimate) * capacityScale(p);
3062
+ return p.restMoment * structureSafety / Math.max(holds, 1);
3063
+ };
3064
+ const bendByBlast = (o, p, point, energy, radius, reach, src) => {
3065
+ const g = p.bend;
3066
+ if (!g || p.span <= 0 || isLost(p) || p.st.stripped || reach <= 0) return;
3067
+ const s = p.mat.strength;
3068
+ if (!s.elasticGPa) return;
3069
+ const { c, q } = partXf(o, p);
3070
+ const near = surfacePoint(p.spec.shape, toLocal(c, q, point));
3071
+ const local = toLocal(c, q, point);
3072
+ const away = vec.normalize(vec.sub(near.point, local), vec.scale(near.normal, -1));
3073
+ let axis = g.depthAxes[0];
3074
+ for (const a of g.depthAxes) if (Math.abs(away[a]) > Math.abs(away[axis])) axis = a;
3075
+ const cos = Math.abs(away[axis]);
3076
+ if (cos < 1e-3) return;
3077
+ const tnt = energy / flexure.tntJoules;
3078
+ const L = p.span;
3079
+ const offsets = g.member ? [[-0.5], [-0.25], [0], [0.25], [0.5]] : [[0, 0], [-0.25, 0], [0.25, 0], [0, -0.25], [0, 0.25]];
3080
+ const face = -Math.sign(away[axis]) * g.dims[axis] / 2;
3081
+ let sum = 0;
3082
+ for (const off of offsets) {
3083
+ const sp = [near.point[0], near.point[1], near.point[2]];
3084
+ g.spanAxes.forEach((a, k) => sp[a] = Math.max(-g.dims[a] / 2, Math.min(g.dims[a] / 2, sp[a] + off[k] * L)));
3085
+ sp[axis] = face;
3086
+ const r = vec.distance(toWorld(c, q, sp), point);
3087
+ if (r < radius) sum += blastImpulse(tnt, r, flexure);
3088
+ }
3089
+ const width = sectionWidth(g, axis);
3090
+ const impulse = sum / offsets.length * cos * reach * width * L;
3091
+ const weight = axis === g.gravity ? p.bendLoad : p.st.mass;
3092
+ const kinetic = impulseEnergy(impulse, lineLoad(g, weight) / GRAVITY * L, flexure);
3093
+ const sec = sectionOf(p, axis);
3094
+ const EI = s.elasticGPa * 1e9 * sec.section.b * sec.section.h ** 3 / 12;
3095
+ const along = axis === g.gravity ? -Math.sign(away[axis] * g.up[axis]) : 0;
3096
+ const dynamic = momentOfEnergy(kinetic, L, EI, flexure.loadFactor);
3097
+ const peak = Math.abs(dynamic + along * p.restMoment);
3098
+ if (peak <= sec.cracking) return;
3099
+ if (!p.st.cracked) {
3100
+ p.st.cracked = true;
3101
+ const tension = [near.point[0], near.point[1], near.point[2]];
3102
+ tension[axis] = dynamic + along * p.restMoment >= 0 ? -face : face;
3103
+ const n = [0, 0, 0];
3104
+ n[axis] = Math.sign(tension[axis]) || 1;
3105
+ addCrack(o, p, toWorld(c, q, tension), rotateVec(q, n), sec.section.b, src);
3106
+ }
3107
+ if (!sec.reinforced) {
3108
+ p.snap = true;
3109
+ failPart(o, p, point, away, kinetic, 12, src, "snap");
3110
+ return;
3111
+ }
3112
+ solveStructure(o);
3113
+ if (peak <= sec.steel) return;
3114
+ const rotation = (elasticEnergy(peak, L, EI, flexure.loadFactor) - elasticEnergy(sec.steel, L, EI, flexure.loadFactor)) / (2 * sec.steel);
3115
+ p.rt.hinge = (p.rt.hinge ?? 0) + rotation;
3116
+ markDamaged(p);
3117
+ if (p.rt.hinge >= flexure.failRotationDeg * Math.PI / 180) hingeDrop(o, p, src);
1928
3118
  };
1929
3119
  const solveStructure = (o) => {
1930
3120
  o.maxStress = 0;
@@ -1945,23 +3135,41 @@ var GameDestruction = (() => {
1945
3135
  for (const j of o.byPart.get(parts[s].spec.id)) {
1946
3136
  if (j.st.open || !j.bears) continue;
1947
3137
  const r = j.ia === s ? j.ib : j.ia;
1948
- if (dist[r] >= 0 || !ok(r) || restsOn(j, r) !== s) continue;
3138
+ if (dist[r] >= 0 || !ok(r) || restsOn(o, j, r) !== s) continue;
1949
3139
  dist[r] = dist[s] + 1;
1950
3140
  queue.push(r);
1951
3141
  }
1952
3142
  }
1953
3143
  const incoming = new Array(n).fill(0);
1954
- for (let h = queue.length - 1; h >= 0; h--) {
1955
- const i = queue[h];
1956
- const total = parts[i].st.mass + incoming[i];
1957
- parts[i].st.load = total;
3144
+ const lowerOf = /* @__PURE__ */ new Map();
3145
+ for (const i of queue) {
1958
3146
  const lower = [];
1959
3147
  for (const j of o.byPart.get(parts[i].spec.id)) {
1960
3148
  if (j.st.open || !j.bears) continue;
1961
- const k = restsOn(j, i);
3149
+ const k = restsOn(o, j, i);
1962
3150
  if (k >= 0 && dist[k] >= 0 && dist[k] < dist[i] && !isPartition(k)) lower.push(k);
1963
3151
  }
3152
+ lowerOf.set(i, lower);
3153
+ }
3154
+ const bending = new Array(n).fill(0);
3155
+ const through = parts.map(() => []);
3156
+ const pass = (k, at, mass) => {
3157
+ const over = overSupportAt(o, parts[k], at, lowerOf.get(k) ?? []);
3158
+ if (over < 0) bending[k] += mass;
3159
+ else through[k].push({ at, mass, over });
3160
+ };
3161
+ for (let h = queue.length - 1; h >= 0; h--) {
3162
+ const i = queue[h];
3163
+ const total = parts[i].st.mass + incoming[i];
3164
+ parts[i].st.load = total;
3165
+ const lower = lowerOf.get(i);
1964
3166
  for (const k of lower) incoming[k] += total / lower.length;
3167
+ if (lower.length === 0) continue;
3168
+ const bulk = parts[i].bend ? parts[i].st.mass + bending[i] : total;
3169
+ for (const k of lower) if (parts[k].bend) pass(k, contactCentre(parts[k], parts[i]), bulk / lower.length);
3170
+ if (parts[i].bend) {
3171
+ for (const item of through[i]) if (parts[item.over]?.bend) pass(item.over, item.at, item.mass);
3172
+ }
1965
3173
  }
1966
3174
  for (let i = 0; i < n; i++) {
1967
3175
  const p = parts[i];
@@ -1970,13 +3178,420 @@ var GameDestruction = (() => {
1970
3178
  if (dist[i] < 0 || isLost(p)) {
1971
3179
  p.st.load = p.st.mass;
1972
3180
  p.st.stress = 0;
1973
- o.unsup[i] = o.anyAnchor && !isLost(p);
3181
+ if (o.seeding && !p.anchored && !isLost(p) && p.st.status !== "collapsed" && !o.collapsed.has(p.spec.id)) p.orphan = true;
3182
+ o.unsup[i] = o.anyAnchor && !isLost(p) && !p.orphan;
1974
3183
  continue;
1975
3184
  }
1976
- p.st.stress = p.st.load / (p.cap * capacityScale(p));
3185
+ p.st.stress = p.st.load * structureSafety / (p.cap * capacityScale(p));
3186
+ if (p.bend) {
3187
+ p.bendLoad = p.st.mass + bending[i];
3188
+ const bent = restFlexure(o, p, lowerOf.get(i) ?? []);
3189
+ p.st.flexure = bent;
3190
+ p.snap = bent > 1 && !p.st.stripped && !sectionOf(p, p.bend.gravity).reinforced;
3191
+ if (p.spec.structure?.capacity === void 0 || bent > p.st.stress) p.st.stress = bent;
3192
+ }
1977
3193
  if (p.st.stress > o.maxStress) o.maxStress = p.st.stress;
1978
3194
  }
3195
+ if (o.wind) applyWind(o);
1979
3196
  if (o.maxStress > 0.8) o.active = true;
3197
+ if (o.soil) o.soil.dirty = true;
3198
+ };
3199
+ const newWind = (o, carried) => {
3200
+ const cfg = exposureOf(windCfg, o.spec.wind);
3201
+ const [x = 0, v = 0, y = 0, w = 0, peak = 0] = carried ?? [];
3202
+ return {
3203
+ cfg,
3204
+ gust: { freq: [], amp: [], phase: [] },
3205
+ gustKey: "",
3206
+ analysis: null,
3207
+ parts: [],
3208
+ sys: null,
3209
+ mu: 0,
3210
+ dyn: { x, v, y, w, peak },
3211
+ tension: /* @__PURE__ */ new Map(),
3212
+ emitted: { t: -Infinity, peak: 0, status: "ok" }
3213
+ };
3214
+ };
3215
+ const windDirection = (o, cfg) => {
3216
+ const d = rotateVec(quat.conjugate(o.base.rot), cfg.direction);
3217
+ const h = Math.hypot(d[0], d[2]);
3218
+ return h > 1e-9 ? [d[0] / h, d[2] / h] : [1, 0];
3219
+ };
3220
+ const applyWind = (o) => {
3221
+ const w = o.wind;
3222
+ const cfg = w.cfg = exposureOf(windCfg, o.spec.wind);
3223
+ const terrain = cfg.terrains.get(cfg.terrain);
3224
+ if (!terrain) throw new Error(`unknown wind terrain "${cfg.terrain}"`);
3225
+ const inputs = [];
3226
+ const at = /* @__PURE__ */ new Map();
3227
+ const keep = [];
3228
+ let ground = Infinity, foundationTop = -Infinity;
3229
+ o.parts.forEach((p, i) => {
3230
+ if (isLost(p) || isTerrain(p) || o.collapsed.has(p.spec.id) || o.unsup[i]) return;
3231
+ const box = aabbOf(p.spec.shape, p.pos, p.q);
3232
+ const role = p.spec.structure?.role;
3233
+ const e = p.mat.strength?.elasticGPa;
3234
+ const kind = role === "wall" ? "wall" : role === "column" ? "column" : role === "beam" ? "beam" : role === "foundation" ? "foundation" : "other";
3235
+ at.set(i, inputs.length);
3236
+ keep.push(i);
3237
+ ground = Math.min(ground, box.min[1]);
3238
+ if (kind === "foundation") foundationTop = Math.max(foundationTop, box.max[1]);
3239
+ inputs.push({ min: box.min, max: box.max, kind, ...e ? { elasticPa: e * 1e9 } : {}, mass: p.st.mass, ...p.spec.damper ? { damper: true } : {} });
3240
+ });
3241
+ keep.forEach((i, k) => {
3242
+ if (inputs[k].kind !== "column") return;
3243
+ const beams = [];
3244
+ for (const j of o.byPart.get(o.parts[i].spec.id)) {
3245
+ if (j.st.open) continue;
3246
+ const b = at.get(j.ia === i ? j.ib : j.ia);
3247
+ if (b !== void 0 && inputs[b].kind === "beam") beams.push(b);
3248
+ }
3249
+ inputs[k].beams = beams;
3250
+ });
3251
+ keep.forEach((i, k) => {
3252
+ if (inputs[k].kind !== "beam") return;
3253
+ for (const j of o.byPart.get(o.parts[i].spec.id)) {
3254
+ if (j.st.open) continue;
3255
+ const q = o.parts[j.ia === i ? j.ib : j.ia];
3256
+ const role = q.spec.structure?.role;
3257
+ if ((role === "slab" || role === "roof") && !isLost(q) && q.spec.shape.kind === "box") {
3258
+ inputs[k].slab = q.dims[1];
3259
+ break;
3260
+ }
3261
+ }
3262
+ });
3263
+ const c = {
3264
+ speed: cfg.speed,
3265
+ direction: windDirection(o, cfg),
3266
+ terrain,
3267
+ cls: cfg.class,
3268
+ topography: cfg.topography,
3269
+ statistical: cfg.statistical,
3270
+ drag: cfg.drag,
3271
+ damping: cfg.damping,
3272
+ airDensity: cfg.airDensity,
3273
+ minHeight: cfg.minHeight,
3274
+ bandHeight: cfg.bandHeight,
3275
+ stiffnessFactor: cfg.stiffnessFactor,
3276
+ beamFactor: cfg.beamFactor,
3277
+ loadFactor: cfg.loadFactor,
3278
+ chord: cfg.chord,
3279
+ meanRatio: cfg.meanRatio,
3280
+ gustFactor: cfg.gustFactor,
3281
+ flange: cfg.flange
3282
+ };
3283
+ const a = inputs.length && cfg.speed > 0 ? analyzeWind(inputs, c, foundationTop > -Infinity ? foundationTop : ground) : null;
3284
+ w.analysis = a;
3285
+ w.parts = keep;
3286
+ for (const p of o.parts) delete p.st.wind;
3287
+ if (!a) {
3288
+ w.sys = null;
3289
+ delete o.st.wind;
3290
+ return;
3291
+ }
3292
+ keep.forEach((i, k) => {
3293
+ const N = a.axial[k];
3294
+ const p = o.parts[i];
3295
+ if (!N || !p.structural || o.unsup[i]) return;
3296
+ p.st.wind = N;
3297
+ const dead = p.st.load ?? p.st.mass;
3298
+ const force = N * cfg.loadFactor;
3299
+ let stress = 0;
3300
+ if (force >= 0) stress = (dead * structureSafety + force / GRAVITY) / (p.cap * capacityScale(p));
3301
+ else if (dead + force / GRAVITY < 0) {
3302
+ let cap = w.tension.get(i);
3303
+ if (cap === void 0) {
3304
+ const r = p.spec.reinforcement;
3305
+ const ratio = p.spec.structure?.rebar?.ratio;
3306
+ cap = tensionCapacity(p.dims, {
3307
+ ...r ? { reinforcement: r } : {},
3308
+ ...ratio ? { ratio } : {},
3309
+ yieldMPa: flexure.rebarYieldMPa,
3310
+ ...p.mat.strength?.tensileMPa !== void 0 ? { tensileMPa: p.mat.strength.tensileMPa } : {}
3311
+ });
3312
+ w.tension.set(i, cap);
3313
+ }
3314
+ stress = Math.min(10, -(dead + force / GRAVITY) * GRAVITY / Math.max(cap * capacityScale(p), 1));
3315
+ }
3316
+ if (stress > (p.st.stress ?? 0)) p.st.stress = stress;
3317
+ if ((p.st.stress ?? 0) > o.maxStress) o.maxStress = p.st.stress ?? 0;
3318
+ });
3319
+ const key = `${cfg.speed}|${cfg.terrain}|${cfg.topography}|${cfg.statistical}|${cfg.meanRatio}|${cfg.gust.components}|${cfg.gust.maxHz}|${cfg.gust.scale}|${cfg.gust.seed}`;
3320
+ if (key !== w.gustKey) {
3321
+ w.gustKey = key;
3322
+ const v10 = cfg.speed * cfg.topography * cfg.statistical * meanHeightFactor(10, terrain, a.cls, { meanRatio: cfg.meanRatio, minHeight: cfg.minHeight });
3323
+ w.gust = makeGust(v10, cfg.gust);
3324
+ }
3325
+ let md = 0, spec;
3326
+ for (const i of keep) {
3327
+ const p = o.parts[i];
3328
+ if (!p.spec.damper) continue;
3329
+ const phi = shapeAt(a, p.pos[1] - (foundationTop > -Infinity ? foundationTop : ground));
3330
+ md += p.st.mass * phi * phi;
3331
+ spec ?? (spec = p.spec.damper);
3332
+ }
3333
+ const sys = { M: a.modalMass, K: a.modalStiffness, C: 2 * cfg.damping * a.omega * a.modalMass };
3334
+ w.mu = 0;
3335
+ if (md > 0 && a.modalMass > 0) {
3336
+ w.mu = md / a.modalMass;
3337
+ const tuned = damperTuning(w.mu, cfg.tuning);
3338
+ const f = spec?.frequency ?? tuned.frequency * a.frequency;
3339
+ const zeta = spec?.damping ?? tuned.damping;
3340
+ sys.damper = { m: md, k: md * (2 * Math.PI * f) ** 2, c: 2 * zeta * md * 2 * Math.PI * f };
3341
+ }
3342
+ w.sys = a.modalMass > 0 && a.omega > 0 ? sys : null;
3343
+ const st = {
3344
+ speed: cfg.speed,
3345
+ pressure: a.pressure,
3346
+ shear: a.shear,
3347
+ moment: a.moment,
3348
+ deflection: a.deflection,
3349
+ drift: a.drift,
3350
+ limit: cfg.driftLimit,
3351
+ overturn: Math.min(a.overturn, 1e3),
3352
+ frequency: a.frequency,
3353
+ lean: a.lean,
3354
+ sway: w.dyn.x,
3355
+ peak: w.dyn.peak,
3356
+ status: a.overturn > 1 ? "overturn" : a.drift > cfg.driftLimit ? "drift" : "ok"
3357
+ };
3358
+ if (sys.damper) st.damper = { ratio: w.mu, travel: w.dyn.y - w.dyn.x };
3359
+ o.st.wind = st;
3360
+ };
3361
+ const stepWind = (o, dt) => {
3362
+ const w = o.wind;
3363
+ const a = w.analysis;
3364
+ const st = o.st.wind;
3365
+ if (!a || !st || !w.sys) return;
3366
+ const gust = w.gust;
3367
+ const I = a.intensity;
3368
+ const force = (t) => {
3369
+ const n = gustAt(gust, t);
3370
+ return a.modalForce * (2 * I * n + I * I * (n * n - 1));
3371
+ };
3372
+ const wn = Math.max(a.omega, w.sys.damper ? Math.sqrt(w.sys.damper.k / w.sys.damper.m) : 0);
3373
+ stepSway(w.dyn, w.sys, time - dt, dt, force, 2 * Math.PI / wn / 20);
3374
+ const d = w.dyn;
3375
+ d.peak = Math.max(Math.abs(d.x), d.peak * Math.exp(-dt / w.cfg.peakDecay));
3376
+ st.sway = d.x;
3377
+ st.peak = d.peak;
3378
+ if (st.damper) st.damper.travel = d.y - d.x;
3379
+ const e = w.emitted;
3380
+ if (d.peak < e.peak * 0.8) e.peak = d.peak;
3381
+ if (st.status !== e.status || time - e.t >= 0.5 && d.peak > e.peak * 1.05 + 1e-3) {
3382
+ w.emitted = { t: time, peak: d.peak, status: st.status };
3383
+ const dir = windDirection(o, w.cfg);
3384
+ const world2 = rotateVec(o.base.rot, [dir[0], 0, dir[1]]);
3385
+ emit({ ...base(o), type: "sway", sway: d.x, peak: d.peak, drift: st.drift, overturn: st.overturn, frequency: st.frequency, direction: world2, status: st.status });
3386
+ }
3387
+ };
3388
+ const isTerrain = (p) => !!p.spec.tags?.includes("terrain");
3389
+ const isFoundation = (p) => p.spec.structure?.role === "foundation" && !isLost(p);
3390
+ const slopeTilt = (a, b) => Math.atan(Math.hypot(a, b));
3391
+ const soilSprings = (o, profile) => {
3392
+ const found = o.parts.filter(isFoundation);
3393
+ const box = (p) => aabbOf(p.spec.shape, p.pos, p.q);
3394
+ const surface = profile.surface ?? Math.max(...found.map((p) => box(p).max[1]));
3395
+ const prints = [];
3396
+ for (const f of found) {
3397
+ const b = box(f);
3398
+ if (!isTerrain(f)) {
3399
+ prints.push({ x0: b.min[0], x1: b.max[0], z0: b.min[2], z1: b.max[2], bottom: b.min[1], part: f });
3400
+ continue;
3401
+ }
3402
+ const fi = o.idx.get(f.spec.id);
3403
+ const width = profile.footingWidth ?? soilCfg.footingWidth;
3404
+ for (const j of o.byPart.get(f.spec.id)) {
3405
+ if (j.st.open || !j.bears) continue;
3406
+ const r = j.ia === fi ? j.ib : j.ia;
3407
+ const q = o.parts[r];
3408
+ if (!q.structural || isLost(q) || restsOn(o, j, r) !== fi) continue;
3409
+ const c = box(q);
3410
+ const x0 = Math.max(c.min[0], b.min[0]), x1 = Math.min(c.max[0], b.max[0]), z0 = Math.max(c.min[2], b.min[2]), z1 = Math.min(c.max[2], b.max[2]);
3411
+ const cx = (x0 + x1) / 2, cz = (z0 + z1) / 2;
3412
+ const wx = Math.max(width, x1 - x0), wz = Math.max(width, z1 - z0);
3413
+ prints.push({ x0: cx - wx / 2, x1: cx + wx / 2, z0: cz - wz / 2, z1: cz + wz / 2, bottom: c.min[1], part: q });
3414
+ }
3415
+ }
3416
+ const springs = [];
3417
+ for (const fp of prints) {
3418
+ const wx = Math.max(0.01, fp.x1 - fp.x0), wz = Math.max(0.01, fp.z1 - fp.z0);
3419
+ const depth = Math.max(0, surface - fp.bottom);
3420
+ const pile = fp.part.spec.structure?.role === "foundation" ? fp.part.spec.structure.pile : void 0;
3421
+ if (pile) {
3422
+ const count = Math.max(1, Math.round(pile.count ?? 1));
3423
+ const nx2 = Math.ceil(Math.sqrt(count)), nz2 = Math.ceil(count / nx2);
3424
+ const cap = pileCapacity(profile, pile, depth, soilCfg.byId).ultimate;
3425
+ const tip = soilAt(profile, depth + pile.length, soilCfg.byId);
3426
+ const k = pileStiffness(pile, fp.part.mat.strength?.elasticGPa ?? soilCfg.pileElasticGPa, tip.elasticMPa, tip.poisson);
3427
+ for (let i = 0; i < count; i++) {
3428
+ const col = i % nx2, row = Math.floor(i / nx2);
3429
+ springs.push({ key: `${fp.part.spec.id}#${i}`, x: fp.x0 + (col + 0.5) * wx / nx2, z: fp.z0 + (row + 0.5) * wz / nz2, hx: wx / (2 * nx2), hz: wz / (2 * nz2), k, cap });
3430
+ }
3431
+ continue;
3432
+ }
3433
+ const B = Math.min(wx, wz);
3434
+ const g = footingSoil(profile, depth, B, soilCfg.byId, soilCfg.influence);
3435
+ const ks = subgradeModulus(g.elasticMPa, g.poisson, B, soilCfg.rigidity);
3436
+ const size = Math.max(wx, wz) / Math.max(1, soilCfg.cells);
3437
+ const nx = Math.max(1, Math.round(wx / size)), nz = Math.max(1, Math.round(wz / size));
3438
+ const area = wx * wz / (nx * nz);
3439
+ for (let i = 0; i < nx; i++) {
3440
+ for (let k = 0; k < nz; k++) {
3441
+ springs.push({ key: `${fp.part.spec.id}#${i}.${k}`, x: fp.x0 + (i + 0.5) * wx / nx, z: fp.z0 + (k + 0.5) * wz / nz, hx: wx / (2 * nx), hz: wz / (2 * nz), k: ks * area, cap: soilCfg.safety * g.allowableKPa * 1e3 * area });
3442
+ }
3443
+ }
3444
+ }
3445
+ return { springs, baseY: prints.length ? Math.min(...prints.map((fp) => fp.bottom)) : 0 };
3446
+ };
3447
+ const soilWeight = (o) => {
3448
+ let m = 0;
3449
+ const c = [0, 0, 0];
3450
+ for (const p of o.parts) {
3451
+ if (isLost(p) || isTerrain(p) || o.collapsed.has(p.spec.id) || o.unsup[o.idx.get(p.spec.id)]) continue;
3452
+ m += p.st.mass;
3453
+ for (let k = 0; k < 3; k++) c[k] += p.st.mass * p.pos[k];
3454
+ }
3455
+ return { w: m * GRAVITY, c: m > 0 ? [c[0] / m, c[1] / m, c[2] / m] : c };
3456
+ };
3457
+ const initSoil = (o, profile, carried, carrySets) => {
3458
+ const so = { profile, s: 0, a: 0, b: 0, imp: [0, 0], pivot: [0, 0, 0], dirty: true, sets: /* @__PURE__ */ new Map(), emitted: { t: -Infinity, s: 0, tilt: 0, status: "stable" } };
3459
+ const { springs, baseY } = soilSprings(o, profile);
3460
+ if (!springs.length) return;
3461
+ const [x0, z0] = stiffnessCentre(springs);
3462
+ so.pivot = [x0, baseY, z0];
3463
+ let ix = 0, iz = 0;
3464
+ for (const sp of springs) {
3465
+ ix += sp.k * (sp.x - x0) ** 2;
3466
+ iz += sp.k * (sp.z - z0) ** 2;
3467
+ }
3468
+ const { c } = soilWeight(o);
3469
+ const along = ix <= iz ? 0 : 1;
3470
+ const off = along === 0 ? c[0] - x0 : c[2] - z0;
3471
+ const sign = Math.abs(off) > 1e-6 ? Math.sign(off) : 1;
3472
+ so.imp = along === 0 ? [sign * soilCfg.imperfection, 0] : [0, sign * soilCfg.imperfection];
3473
+ if (carried) {
3474
+ const t = Math.tan(carried.tilt * Math.PI / 180);
3475
+ so.s = carried.settlement;
3476
+ so.a = t * carried.direction[0];
3477
+ so.b = t * carried.direction[2];
3478
+ so.emitted = { t: -Infinity, s: carried.settlement, tilt: carried.tilt, status: carried.status };
3479
+ o.st.soil = clone(carried);
3480
+ for (const [k, v] of Object.entries(carrySets ?? {})) so.sets.set(k, v);
3481
+ if (carried.status === "toppled") for (const p of o.parts) p.anchored = false;
3482
+ }
3483
+ o.soil = so;
3484
+ applySoilPose(o);
3485
+ };
3486
+ const applySoilPose = (o) => {
3487
+ const so = o.soil;
3488
+ if (!so) {
3489
+ o.st.position = [o.base.pos[0], o.base.pos[1], o.base.pos[2]];
3490
+ o.st.rotation = [o.base.rot[0], o.base.rot[1], o.base.rot[2], o.base.rot[3]];
3491
+ return;
3492
+ }
3493
+ const angle = slopeTilt(so.a, so.b);
3494
+ const R = angle > 1e-12 ? quat.fromAxisAngle(vec.normalize([so.b, 0, -so.a]), angle) : quat.identity();
3495
+ const t = vec.sub(vec.sub(so.pivot, rotateVec(R, so.pivot)), [0, so.s, 0]);
3496
+ o.st.rotation = quat.normalize(quat.mul(o.base.rot, R));
3497
+ o.st.position = vec.add(o.base.pos, rotateVec(o.base.rot, t));
3498
+ const lean = Math.hypot(so.a, so.b);
3499
+ const st = o.st.soil ?? { settlement: 0, tilt: 0, direction: [0, 0, 0], bearing: 0, edge: 0, status: "stable", offset: { position: [0, 0, 0], rotation: [0, 0, 0, 1] } };
3500
+ st.settlement = so.s;
3501
+ st.tilt = angle * 180 / Math.PI;
3502
+ st.direction = lean > 1e-12 ? [so.a / lean, 0, so.b / lean] : [0, 0, 0];
3503
+ st.offset = { position: t, rotation: R };
3504
+ o.st.soil = st;
3505
+ };
3506
+ const topple = (o, src) => {
3507
+ for (const p of o.parts) p.anchored = false;
3508
+ solveStructure(o);
3509
+ recomputeGroups(o);
3510
+ if (!pendingCollapse.has(o)) pendingCollapse.set(o, src);
3511
+ };
3512
+ const settleSoil = (o, dt) => {
3513
+ const so = o.soil;
3514
+ const st = o.st.soil;
3515
+ if (st?.status === "toppled") return;
3516
+ const { springs } = soilSprings(o, so.profile);
3517
+ for (const sp of springs) sp.set = so.sets.get(sp.key) ?? 0;
3518
+ const { w, c } = soilWeight(o);
3519
+ if (!springs.length || w <= 0) {
3520
+ if (springs.length === 0 && w > 0) {
3521
+ so.s += soilCfg.sinkRate * dt;
3522
+ applySoilPose(o);
3523
+ o.st.soil.status = "yielding";
3524
+ }
3525
+ return;
3526
+ }
3527
+ const height = Math.max(0, c[1] - so.pivot[1]);
3528
+ const wa = o.wind?.analysis;
3529
+ const push = wa ? windDirection(o, o.wind.cfg) : [0, 0];
3530
+ const shift = wa ? wa.moment / w : 0;
3531
+ const r = solveFoundation(springs, w, c[0] + shift * push[0], c[2] + shift * push[1], height, so.imp, { s: so.s, a: so.a, b: so.b });
3532
+ let status = "stable";
3533
+ const sinkStep = soilCfg.sinkRate * dt;
3534
+ const tiltStep = Math.tan(soilCfg.tiltRate * Math.PI / 180) * dt;
3535
+ if (r.stable && (!r.yielded || dt === 0)) {
3536
+ so.s = r.s;
3537
+ so.a = r.a;
3538
+ so.b = r.b;
3539
+ } else if (r.stable) {
3540
+ const ds = r.s - so.s, da = r.a - so.a, db = r.b - so.b;
3541
+ const fs = Math.abs(ds) > sinkStep ? sinkStep / Math.abs(ds) : 1;
3542
+ const lean2 = Math.hypot(da, db);
3543
+ const ft = lean2 > tiltStep ? tiltStep / lean2 : 1;
3544
+ so.s += ds * fs;
3545
+ so.a += da * ft;
3546
+ so.b += db * ft;
3547
+ if (fs < 1 || ft < 1) status = "yielding";
3548
+ } else {
3549
+ status = "yielding";
3550
+ if (dt === 0) {
3551
+ so.s = w / springs.reduce((sum, sp) => sum + sp.k, 0);
3552
+ } else {
3553
+ so.s += sinkStep * (r.bearing >= 1 ? 1 : 0.25);
3554
+ const lean2 = Math.hypot(so.a, so.b);
3555
+ const ux = lean2 > 1e-9 ? so.a / lean2 : Math.sign(so.imp[0]), uz = lean2 > 1e-9 ? so.b / lean2 : Math.sign(so.imp[1]);
3556
+ so.a += ux * tiltStep;
3557
+ so.b += uz * tiltStep;
3558
+ }
3559
+ }
3560
+ if (so.s !== 0 || so.a !== 0 || so.b !== 0) {
3561
+ const sets = yieldSprings(springs, so.s, so.a, so.b);
3562
+ springs.forEach((sp, i) => {
3563
+ if (sets[i] > 0) so.sets.set(sp.key, sets[i]);
3564
+ });
3565
+ }
3566
+ applySoilPose(o);
3567
+ const out = o.st.soil;
3568
+ out.bearing = r.bearing;
3569
+ out.edge = r.edge;
3570
+ out.status = status;
3571
+ const lean = Math.hypot(so.a, so.b);
3572
+ if (lean > 1e-9) {
3573
+ const [x0, z0] = [so.pivot[0], so.pivot[2]];
3574
+ const ux = so.a / lean, uz = so.b / lean;
3575
+ const e = (c[0] - x0 + height * (so.a + so.imp[0])) * ux + (c[2] - z0 + height * (so.b + so.imp[1])) * uz;
3576
+ let edge = 0;
3577
+ for (const sp of springs) edge = Math.max(edge, (sp.x - x0) * ux + (sp.z - z0) * uz + sp.hx * Math.abs(ux) + sp.hz * Math.abs(uz));
3578
+ if (e > edge || slopeTilt(so.a, so.b) > Math.PI / 3) out.status = "toppled";
3579
+ }
3580
+ };
3581
+ const stepSoil = (o, dt) => {
3582
+ const so = o.soil;
3583
+ if (o.st.soil?.status === "toppled") return;
3584
+ if (!so.dirty && o.st.soil?.status !== "yielding") return;
3585
+ so.dirty = false;
3586
+ settleSoil(o, dt);
3587
+ const st = o.st.soil;
3588
+ const e = so.emitted;
3589
+ const changed = st.status !== e.status || (time - e.t >= 0.5 || e.t === -Infinity) && (Math.abs(st.tilt - e.tilt) > 0.01 || Math.abs(st.settlement - e.s) > 2e-3);
3590
+ if (changed) {
3591
+ so.emitted = { t: time, s: st.settlement, tilt: st.tilt, status: st.status };
3592
+ emit({ ...base(o), type: "settle", settlement: st.settlement, tilt: st.tilt, direction: rotateVec(o.base.rot, st.direction), bearing: st.bearing, status: st.status });
3593
+ }
3594
+ if (st.status === "toppled") topple(o);
1980
3595
  };
1981
3596
  const collapseCheck = (o, src) => {
1982
3597
  if (!o.hasStructure || !o.parts.some((p) => p.anchored)) return;
@@ -2213,7 +3828,7 @@ var GameDestruction = (() => {
2213
3828
  if (split && mine.length) amount /= mine.length;
2214
3829
  for (const j of mine) {
2215
3830
  if (j.st.open) continue;
2216
- if (j.spec.kind === "rebar" && (o.parts[j.ia].spec.reinforcement || o.parts[j.ib].spec.reinforcement)) continue;
3831
+ if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
2217
3832
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
2218
3833
  j.st.hp -= amount;
2219
3834
  if (j.st.hp <= 0) {
@@ -2369,14 +3984,37 @@ var GameDestruction = (() => {
2369
3984
  debris(o, p, "sparks", partXf(o, p).c, [0, 1, 0], 6, 3, src);
2370
3985
  partLost(o, p, src);
2371
3986
  };
2372
- const failPart = (o, p, origin, dir, fragEnergy, cap, src) => {
3987
+ const hingeDrop = (o, p, src) => {
3988
+ const i = o.idx.get(p.spec.id);
3989
+ const opened = [];
3990
+ for (const j of o.byPart.get(p.spec.id) ?? []) {
3991
+ if (j.st.open || restsOn(o, j, i) < 0) continue;
3992
+ j.st.open = true;
3993
+ j.st.hp = 0;
3994
+ j.st.openedBy = "break";
3995
+ opened.push(j);
3996
+ }
3997
+ p.st.hp = Math.max(1, p.st.maxHp * 0.1);
3998
+ p.st.cracked = true;
3999
+ markDamaged(p);
4000
+ debris(o, p, p.mat.breakDebris, partXf(o, p).c, [0, -1, 0], 12, 2, src);
4001
+ solveStructure(o);
4002
+ recomputeGroups(o);
4003
+ detachEvents(o, opened, "break", src);
4004
+ collapseCheck(o, src);
4005
+ };
4006
+ const failPart = (o, p, origin, dir, fragEnergy, cap, src, force) => {
2373
4007
  if (isLost(p)) return;
2374
4008
  if (p.st.stripped) {
2375
4009
  loseCage(o, p, src);
2376
4010
  return;
2377
4011
  }
4012
+ if (!force && p.bend && (p.st.flexure ?? 0) > 1 && sectionOf(p, p.bend.gravity).reinforced) {
4013
+ hingeDrop(o, p, src);
4014
+ return;
4015
+ }
2378
4016
  const ex = p.spec.explodes;
2379
- const style = p.mat.fracture === "none" ? "snap" : p.mat.fracture;
4017
+ const style = force ?? (p.snap ? "snap" : p.mat.fracture === "none" ? "snap" : p.mat.fracture);
2380
4018
  const { c, q } = partXf(o, p);
2381
4019
  const hitW = origin ?? c;
2382
4020
  const grain = p.spec.grain ? AXIS[p.spec.grain] : void 0;
@@ -2433,8 +4071,8 @@ var GameDestruction = (() => {
2433
4071
  return;
2434
4072
  }
2435
4073
  if (ex) {
2436
- emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
2437
- blasts.push({ point: c, energy: ex.energy, radius: ex.radius, source: src });
4074
+ emit({ ...base(o, src), type: "explode", part: p.spec.id, point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, ...ex.effect !== void 0 ? { effect: ex.effect } : {} });
4075
+ blasts.push({ point: c, energy: ex.energy, radius: ex.radius, ...ex.lethalRadius !== void 0 ? { lethalRadius: ex.lethalRadius } : {}, source: src });
2438
4076
  }
2439
4077
  partLost(o, p, src);
2440
4078
  };
@@ -2463,8 +4101,33 @@ var GameDestruction = (() => {
2463
4101
  else if (chained && (trigger === "explosion" || 1 - p.st.hp / p.st.maxHp >= shotDamageFractionOf(p.spec))) prime(o, p, trigger, src);
2464
4102
  return done;
2465
4103
  };
2466
- const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, piercing = 1) => {
4104
+ const leaksFlammable = (p) => {
4105
+ const c = p.spec.contains;
4106
+ return !!c && FLAMMABLE_FLUIDS.includes(c.fluid) && (p.st.liters ?? 0) > 0 && p.st.holes.some((h) => h.through);
4107
+ };
4108
+ const rollIgnition = (o, p, round) => {
4109
+ const c = p.spec.contains;
4110
+ if (!c || !FLAMMABLE_FLUIDS.includes(c.fluid) || p.rt.jet || p.rt.lit) return;
4111
+ const fill = c.liters > 0 ? (p.st.liters ?? 0) / c.liters : 0;
4112
+ const chance = ignitionChance(round, { sparkTarget: sparkTargetFactor(p.mat.id, !!p.mat.tags?.includes("metal"), ignitionRules), fill, fluid: c.fluid }, ignitionRules);
4113
+ if (chance > 0 && rnd() < chance) {
4114
+ p.rt.lit = true;
4115
+ o.active = true;
4116
+ }
4117
+ };
4118
+ const lightLeaksNear = (centre, radius) => {
4119
+ for (const o of objects.values()) {
4120
+ for (const q of o.parts) {
4121
+ if (isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
4122
+ if (vec.distance(partXf(o, q).c, centre) - q.bound > radius) continue;
4123
+ q.rt.lit = true;
4124
+ o.active = true;
4125
+ }
4126
+ }
4127
+ };
4128
+ const ballistic = (o, p, entry, n, d, E, caliber, pathLen, src, round = {}) => {
2467
4129
  const mat = p.mat;
4130
+ const piercing = round.piercing ?? 1;
2468
4131
  if (p.st.stripped || p.st.opening && insideOpening(o, p, objLocal(o, entry))) {
2469
4132
  return { penetrated: true, exitEnergy: E, exitPoint: vec.madd(entry, d, Math.min(pathLen, p.thickness)), damage: 0 };
2470
4133
  }
@@ -2477,7 +4140,8 @@ var GameDestruction = (() => {
2477
4140
  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;
2478
4141
  const thr = coats > 0 ? (mat.penetration * p.thickness * 1e3 + coats) / slant : mat.penetration * effM * 1e3;
2479
4142
  const hard = mat.tags?.includes("metal") || mat.tags?.includes("masonry");
2480
- const Ep = piercing === 1 || !hard ? E : E * piercing;
4143
+ const penMm = penetrationOf(mat, round.penetration);
4144
+ const Ep = penMm !== void 0 ? penMm * mat.penetration : piercing === 1 || !hard ? E : E * piercing;
2481
4145
  const penetrated = Ep > thr;
2482
4146
  const scale = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
2483
4147
  const id = p.spec.id;
@@ -2498,6 +4162,7 @@ var GameDestruction = (() => {
2498
4162
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2499
4163
  if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2500
4164
  addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
4165
+ if (mat.ductility >= 0.5 && p.thickness <= THIN_SHEET_DISHING) addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(E * 2e-5, 1e-3, 0.03), src);
2501
4166
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2502
4167
  if (p.spec.contains && (p.st.liters ?? 0) > 0) {
2503
4168
  emit({
@@ -2506,10 +4171,11 @@ var GameDestruction = (() => {
2506
4171
  part: id,
2507
4172
  fluid: p.spec.contains.fluid,
2508
4173
  point: entry,
2509
- direction: n,
4174
+ direction: p.spec.contains.outflow ? vec.normalize(rotateVec(o.st.rotation, rotateVec(p.q, p.spec.contains.outflow))) : n,
2510
4175
  rate: holeRate(hole, !!p.spec.contains.pressurized),
2511
4176
  pressurized: !!p.spec.contains.pressurized
2512
4177
  });
4178
+ rollIgnition(o, p, round);
2513
4179
  }
2514
4180
  } else if (mat.ductility >= 0.5) {
2515
4181
  addDent(o, p, entry, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
@@ -2520,7 +4186,7 @@ var GameDestruction = (() => {
2520
4186
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2521
4187
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2522
4188
  const dmg = applyDamage(o, p, E * DMG_PER_J * scale, "ballistic", entry, d, E * 0.1, 12, src);
2523
- return { penetrated, exitEnergy: penetrated ? E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
4189
+ return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2524
4190
  };
2525
4191
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2526
4192
  const mat = p.mat;
@@ -2540,8 +4206,9 @@ var GameDestruction = (() => {
2540
4206
  }
2541
4207
  debris(o, p, mat.hitDebris, point, n, clamp2(Math.round(E / 25) + 1, 1, 10), clamp2(Math.sqrt(E) / 8, 1, 6), src);
2542
4208
  }
2543
- if (kind === "blunt") wearJoints(o, p, E * 0.3, void 0, 0, src);
2544
- else wearJoints(o, p, E * 0.3, point, 0.06, src);
4209
+ if (kind === "blunt") {
4210
+ if (!(p.anchored && p.spec.tags?.includes("terrain"))) wearJoints(o, p, E * 0.3, void 0, 0, src);
4211
+ } else wearJoints(o, p, E * 0.3, point, 0.06, src);
2545
4212
  if (kind === "blunt" && E > 0 && p.layers.length > 0) spallOuter(o, p, point, faceOf(o, p, n), n, Math.max(contactR, 0.05), src);
2546
4213
  const scale = mat.damageScale?.[kind] ?? 1;
2547
4214
  const done = applyDamage(o, p, Ec * DMG_PER_J * scale * (kind === "slash" ? p.cutFactor : 1), kind, point, d, E * 0.1, 12, src);
@@ -2588,7 +4255,8 @@ var GameDestruction = (() => {
2588
4255
  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
4256
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2590
4257
  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);
4258
+ if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
4259
+ if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2592
4260
  return reach;
2593
4261
  };
2594
4262
  const blastObject = (o, point, energy, radius, src) => {
@@ -2651,7 +4319,7 @@ var GameDestruction = (() => {
2651
4319
  };
2652
4320
  const FLAG_LOST = 1;
2653
4321
  const FLAG_OPEN = 2;
2654
- const blastPlain = (point, energy, radius, src) => {
4322
+ const blastPlain = (point, energy, radius, src, lethal) => {
2655
4323
  const out = [];
2656
4324
  for (const o of [...objects.values()]) {
2657
4325
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2659,7 +4327,7 @@ var GameDestruction = (() => {
2659
4327
  if (r.part) out.push(r);
2660
4328
  }
2661
4329
  for (const im of [...impls.values()]) {
2662
- if (implInRange(im, point, radius)) out.push(implHit(im, { type: "explosive", point, energy, radius, ...src !== void 0 ? { source: src } : {} }));
4330
+ if (implInRange(im, point, radius)) out.push(implHit(im, { type: "explosive", point, energy, radius, ...lethal !== void 0 ? { lethalRadius: lethal } : {}, ...src !== void 0 ? { source: src } : {} }));
2663
4331
  }
2664
4332
  return out;
2665
4333
  };
@@ -2668,7 +4336,7 @@ var GameDestruction = (() => {
2668
4336
  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
4337
  });
2670
4338
  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) => {
4339
+ const blastScene = (point, energy, radius, src, cfg, only, lethal) => {
2672
4340
  const mult = confinementOf(cfg.conf, point, radius);
2673
4341
  const E = energy * mult;
2674
4342
  const shelter = cfg.shelter;
@@ -2775,7 +4443,7 @@ var GameDestruction = (() => {
2775
4443
  den += f;
2776
4444
  }
2777
4445
  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 } : {} });
4446
+ const res = implHit(im, { type: "explosive", point, energy: E * k, radius, ...lethal !== void 0 ? { lethalRadius: lethal } : {}, ...shelter ? { shelter: k } : {}, ...src !== void 0 ? { source: src } : {} });
2779
4447
  results.set(im, res);
2780
4448
  report(im).shelter = k;
2781
4449
  }
@@ -2873,10 +4541,11 @@ var GameDestruction = (() => {
2873
4541
  if (!rep) reports.set(key, rep = { shelter: 1, confinement: mult, fragmentHits: 0 });
2874
4542
  rep.fragmentHits++;
2875
4543
  };
2876
- const blastWorld = (point, energy, radius, src, call, only) => {
4544
+ const blastWorld = (point, energy, radius, src, call, only, lethal) => {
2877
4545
  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);
4546
+ const res = !cfg.shelter && !cfg.frag && cfg.conf === null ? blastPlain(point, energy, radius, src, lethal) : blastScene(point, energy, radius, src, cfg, only, lethal);
4547
+ lightLeaksNear(point, radius);
4548
+ return res;
2880
4549
  };
2881
4550
  const heatObject = (o, point, T, radius, duration, src) => {
2882
4551
  const start = cur.length;
@@ -2895,6 +4564,7 @@ var GameDestruction = (() => {
2895
4564
  p.st.temperature += (T - p.st.temperature) * (1 - Math.exp(-k * duration));
2896
4565
  }
2897
4566
  o.active = true;
4567
+ if (T >= FLAME_LIGHTS_LEAK_C && !p.rt.jet && !p.rt.lit && leaksFlammable(p)) p.rt.lit = true;
2898
4568
  checkIgnite(o, p, src);
2899
4569
  }
2900
4570
  res.events = cur.slice(start);
@@ -2914,7 +4584,7 @@ var GameDestruction = (() => {
2914
4584
  if (hit.type === "ballistic") {
2915
4585
  const loc = locate(o, point, dir, 0.05, only);
2916
4586
  if (loc) {
2917
- const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit.piercing ?? 1);
4587
+ const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit);
2918
4588
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2919
4589
  }
2920
4590
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -3068,7 +4738,8 @@ var GameDestruction = (() => {
3068
4738
  if (rate > 0) {
3069
4739
  s.liters = Math.max(0, (s.liters ?? 0) - rate * dt);
3070
4740
  hot = true;
3071
- if (FLAMMABLE_FLUIDS.includes(cont.fluid) && s.temperature >= 350 && !p.rt.jet) {
4741
+ if (FLAMMABLE_FLUIDS.includes(cont.fluid) && (s.temperature >= 350 || p.rt.lit) && !p.rt.jet) {
4742
+ p.rt.lit = false;
3072
4743
  if (ex && chainDelayOf(p.spec, "fire") !== void 0) {
3073
4744
  prime(o, p, "fire");
3074
4745
  continue;
@@ -3083,6 +4754,7 @@ var GameDestruction = (() => {
3083
4754
  }
3084
4755
  }
3085
4756
  }
4757
+ if ((s.liters ?? 0) <= 0) p.rt.lit = false;
3086
4758
  if (p.rt.jet && (s.liters ?? 0) <= 0) {
3087
4759
  p.rt.jet = false;
3088
4760
  s.burning = false;
@@ -3132,6 +4804,25 @@ var GameDestruction = (() => {
3132
4804
  }
3133
4805
  o.active = hot;
3134
4806
  };
4807
+ const igniteLeaksNearFire = (sources, dt) => {
4808
+ const { radius, chancePerSecond } = ignitionRules.fireNear;
4809
+ if (chancePerSecond <= 0) return;
4810
+ const chance = 1 - Math.exp(-chancePerSecond * dt);
4811
+ for (const src of sources) {
4812
+ if (!src.p.st.burning && !src.p.rt.jet) continue;
4813
+ const sc = partXf(src.o, src.p).c;
4814
+ for (const o of objects.values()) {
4815
+ for (const q of o.parts) {
4816
+ if (q === src.p || isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
4817
+ if (vec.distance(partXf(o, q).c, sc) - q.bound - src.p.bound > radius) continue;
4818
+ if (rnd() < chance) {
4819
+ q.rt.lit = true;
4820
+ o.active = true;
4821
+ }
4822
+ }
4823
+ }
4824
+ }
4825
+ };
3135
4826
  const spread2 = (sources, dt) => {
3136
4827
  for (const src of sources) {
3137
4828
  if (!src.p.st.burning && !src.p.rt.jet) continue;
@@ -3257,8 +4948,8 @@ var GameDestruction = (() => {
3257
4948
  return;
3258
4949
  }
3259
4950
  const o = getObj(id);
3260
- o.st.position = [position[0], position[1], position[2]];
3261
- o.st.rotation = [rotation[0], rotation[1], rotation[2], rotation[3]];
4951
+ o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
4952
+ applySoilPose(o);
3262
4953
  },
3263
4954
  splitGroup(id, group, newId) {
3264
4955
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -3281,13 +4972,17 @@ var GameDestruction = (() => {
3281
4972
  const s = { parts: o.spec.parts.filter((p) => keep(p.id)), joints: (o.spec.joints ?? []).filter((j) => keep(j.a) && keep(j.b)) };
3282
4973
  if (o.spec.name !== void 0) s.name = o.spec.name;
3283
4974
  if (o.spec.tags) s.tags = [...o.spec.tags];
4975
+ if (o.spec.soil !== void 0) s.soil = o.spec.soil;
3284
4976
  return clone(s);
3285
4977
  };
3286
4978
  const inNew = (pid) => moved.has(pid);
3287
4979
  const inOld = (pid) => !moved.has(pid);
3288
4980
  const pos = o.st.position, rot = o.st.rotation;
3289
4981
  const fresh = build(sub(inNew), nid, pos, rot, carryFor(inNew, (a, b) => inNew(a) && inNew(b)));
3290
- const rest = build(sub(inOld), id, pos, rot, carryFor(inOld, (a, b) => !(inNew(a) && inNew(b))));
4982
+ const restCarry = carryFor(inOld, (a, b) => !(inNew(a) && inNew(b)));
4983
+ if (o.st.soil) restCarry.soil = o.st.soil;
4984
+ if (o.soil?.sets.size) restCarry.soilSets = Object.fromEntries(o.soil.sets);
4985
+ const rest = build(sub(inOld), id, o.soil ? o.base.pos : pos, o.soil ? o.base.rot : rot, restCarry);
3291
4986
  objects.set(id, rest);
3292
4987
  objects.set(nid, fresh);
3293
4988
  return nid;
@@ -3312,7 +5007,8 @@ var GameDestruction = (() => {
3312
5007
  const r = raycastInternal(origin, direction, maxDistance);
3313
5008
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
3314
5009
  },
3315
- shoot(origin, direction, energy, caliber, source, piercing) {
5010
+ shoot(origin, direction, energy, caliber, source, round) {
5011
+ const extras = typeof round === "number" ? { piercing: round } : round ?? {};
3316
5012
  return withCollapses(run(() => {
3317
5013
  const results = [];
3318
5014
  const d = vec.normalize(direction);
@@ -3324,7 +5020,7 @@ var GameDestruction = (() => {
3324
5020
  if (!rh) break;
3325
5021
  const im = impls.get(rh.object);
3326
5022
  if (im) {
3327
- const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...piercing !== void 0 ? { piercing } : {}, ...source !== void 0 ? { source } : {} });
5023
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
3328
5024
  r.part = rh.part;
3329
5025
  r.point = rh.point;
3330
5026
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -3338,7 +5034,7 @@ var GameDestruction = (() => {
3338
5034
  const o = getObj(rh.object);
3339
5035
  const p = getPart(o, rh.part);
3340
5036
  const start = cur.length;
3341
- const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, piercing ?? 1);
5037
+ const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, extras);
3342
5038
  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) });
3343
5039
  ignore.add(`${o.id}\0${p.spec.id}`);
3344
5040
  if (!out.penetrated || !out.exitPoint) break;
@@ -3437,6 +5133,52 @@ var GameDestruction = (() => {
3437
5133
  o.active = true;
3438
5134
  });
3439
5135
  },
5136
+ setWind(next) {
5137
+ windOpts = { ...windOpts, ...Object.fromEntries(Object.entries(next).filter(([, v]) => v !== void 0)) };
5138
+ windCfg = resolveWind(windOpts);
5139
+ for (const o of objects.values()) {
5140
+ if (!o.hasStructure || o.spec.wind === false) continue;
5141
+ o.wind ?? (o.wind = newWind(o));
5142
+ o.dirty = true;
5143
+ o.active = true;
5144
+ }
5145
+ },
5146
+ vent(objectId, partId, opening = {}) {
5147
+ if (impls.has(objectId)) return false;
5148
+ const o = getObj(objectId);
5149
+ const p = getPart(o, partId);
5150
+ const c = p.spec.contains;
5151
+ if (!c || isLost(p) || (p.st.liters ?? 0) <= 0) return false;
5152
+ if (p.st.holes.some((h) => h.vent)) return true;
5153
+ return run(() => {
5154
+ const wp = opening.point ?? partXf(o, p).c;
5155
+ const wn = vec.normalize(opening.normal ?? [0, 1, 0]);
5156
+ const hole = { point: objLocal(o, wp), normal: objLocalDir(o, wn), diameter: opening.diameter ?? VENT_DIAMETER, through: true, vent: true };
5157
+ p.st.holes.push(hole);
5158
+ if (p.st.holes.length > maxHoles) p.st.holes.shift();
5159
+ o.active = true;
5160
+ emit({
5161
+ ...base(o),
5162
+ type: "leak",
5163
+ part: partId,
5164
+ fluid: c.fluid,
5165
+ point: wp,
5166
+ direction: c.outflow ? vec.normalize(rotateVec(o.st.rotation, rotateVec(p.q, c.outflow))) : wn,
5167
+ rate: holeRate(hole, !!c.pressurized),
5168
+ pressurized: !!c.pressurized
5169
+ });
5170
+ return true;
5171
+ }).value;
5172
+ },
5173
+ seal(objectId, partId) {
5174
+ if (impls.has(objectId)) return 0;
5175
+ const o = getObj(objectId);
5176
+ const p = getPart(o, partId);
5177
+ const before = p.st.holes.length;
5178
+ p.st.holes = p.st.holes.filter((h) => !h.vent);
5179
+ o.active = true;
5180
+ return before - p.st.holes.length;
5181
+ },
3440
5182
  setLiters(objectId, partId, liters) {
3441
5183
  if (impls.has(objectId)) return;
3442
5184
  const p = getPart(getObj(objectId), partId);
@@ -3450,7 +5192,12 @@ var GameDestruction = (() => {
3450
5192
  time += dt;
3451
5193
  const sources = [];
3452
5194
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
3453
- if (sources.length) spread2(sources, dt);
5195
+ for (const o of [...objects.values()]) if (o.soil && objects.get(o.id) === o) stepSoil(o, dt);
5196
+ for (const o of [...objects.values()]) if (o.wind && objects.get(o.id) === o) stepWind(o, dt);
5197
+ if (sources.length) {
5198
+ spread2(sources, dt);
5199
+ igniteLeaksNearFire(sources, dt);
5200
+ }
3454
5201
  detonatePrimed(time);
3455
5202
  releaseFront();
3456
5203
  for (const im of [...impls.values()]) {
@@ -3472,7 +5219,7 @@ var GameDestruction = (() => {
3472
5219
  return;
3473
5220
  }
3474
5221
  const o = getObj(id);
3475
- objects.set(id, build(o.spec, id, o.st.position, o.st.rotation));
5222
+ objects.set(id, build(o.spec, id, o.base.pos, o.base.rot));
3476
5223
  },
3477
5224
  on(type, fn) {
3478
5225
  let set = listeners.get(type);
@@ -3489,7 +5236,7 @@ var GameDestruction = (() => {
3489
5236
  rng: rng.state(),
3490
5237
  counter,
3491
5238
  objects: [...objects.values()].map((o) => {
3492
- const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
5239
+ const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed], ...o.soil?.sets.size ? { soil: Object.fromEntries(o.soil.sets) } : {}, ...o.wind ? { wind: [o.wind.dyn.x, o.wind.dyn.v, o.wind.dyn.y, o.wind.dyn.w, o.wind.dyn.peak] } : {} };
3493
5240
  for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
3494
5241
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
3495
5242
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
@@ -3510,9 +5257,17 @@ var GameDestruction = (() => {
3510
5257
  state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
3511
5258
  ),
3512
5259
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
3513
- collapsed: e.runtime?.collapsed
5260
+ collapsed: e.runtime?.collapsed,
5261
+ ...e.runtime?.wind ? { wind: e.runtime.wind } : {}
3514
5262
  };
3515
- objects.set(e.id, build(clone(e.spec), e.id, state.position, state.rotation, carry));
5263
+ let pos = state.position, rot = state.rotation;
5264
+ if (state.soil) {
5265
+ carry.soil = state.soil;
5266
+ if (e.runtime?.soil) carry.soilSets = e.runtime.soil;
5267
+ rot = quat.normalize(quat.mul(state.rotation, quat.conjugate(state.soil.offset.rotation)));
5268
+ pos = vec.sub(state.position, rotateVec(rot, state.soil.offset.position));
5269
+ }
5270
+ objects.set(e.id, build(clone(e.spec), e.id, pos, rot, carry));
3516
5271
  }
3517
5272
  for (const e of snap.impls ?? []) {
3518
5273
  const im = impls.get(e.id);
@@ -3526,21 +5281,6 @@ var GameDestruction = (() => {
3526
5281
  return world;
3527
5282
  };
3528
5283
 
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
-
3544
5284
  // src/data/assemblies.json
3545
5285
  var assemblies_default = [
3546
5286
  {
@@ -3653,21 +5393,35 @@ var GameDestruction = (() => {
3653
5393
  const dw = o.doorWidth ?? 1, dh = o.doorHeight ?? 2.1;
3654
5394
  const mat = o.material ?? "concrete", wmat = o.wallMaterial ?? "brick";
3655
5395
  const roof = o.roof ?? true;
5396
+ const rebarOf = (kind) => o.rebar === false ? {} : { rebar: { ratio: o.rebar?.[kind] ?? (kind === "beam" ? 0.01 : 5e-3) } };
3656
5397
  const xs = Array.from({ length: nx }, (_, i) => -spanX / 2 + i * spanX / (nx - 1));
3657
5398
  const zs = Array.from({ length: nz }, (_, j) => -spanZ / 2 + j * spanZ / (nz - 1));
3658
5399
  const parts = [];
3659
5400
  const joints = [];
3660
5401
  const on = (a, b, kind) => joints.push({ id: `${a}~${b}`, a, b, kind, bears: "a_on_b" });
3661
- for (let i = 0; i < nx; i++) {
3662
- for (let j = 0; j < nz; j++) {
3663
- parts.push({
3664
- id: `foundation_${i}_${j}`,
3665
- material: mat,
3666
- shape: { kind: "box", size: [cs * 2.2, 0.3, cs * 2.2] },
3667
- position: [xs[i], -0.15, zs[j]],
3668
- structure: { role: "foundation" },
3669
- anchored: true
3670
- });
5402
+ const pad = o.footing ?? cs * 2.2;
5403
+ const pile = o.piles ? { pile: { ...o.piles } } : {};
5404
+ if (o.raft) {
5405
+ parts.push({
5406
+ id: "raft",
5407
+ material: mat,
5408
+ shape: { kind: "box", size: [spanX + pad, 0.4, spanZ + pad] },
5409
+ position: [0, -0.2, 0],
5410
+ structure: { role: "foundation", ...pile },
5411
+ anchored: true
5412
+ });
5413
+ } else {
5414
+ for (let i = 0; i < nx; i++) {
5415
+ for (let j = 0; j < nz; j++) {
5416
+ parts.push({
5417
+ id: `foundation_${i}_${j}`,
5418
+ material: mat,
5419
+ shape: { kind: "box", size: [pad, 0.3, pad] },
5420
+ position: [xs[i], -0.15, zs[j]],
5421
+ structure: { role: "foundation", ...pile },
5422
+ anchored: true
5423
+ });
5424
+ }
3671
5425
  }
3672
5426
  }
3673
5427
  for (let f = 0; f < floors; f++) {
@@ -3683,14 +5437,15 @@ var GameDestruction = (() => {
3683
5437
  material: mat,
3684
5438
  shape: { kind: "box", size: [cs, H, cs] },
3685
5439
  position: [xs[i], yb + H / 2, zs[j]],
3686
- structure: o.columnCapacity === void 0 ? { role: "column" } : { role: "column", capacity: o.columnCapacity }
5440
+ structure: o.columnCapacity === void 0 ? { role: "column" } : { role: "column", capacity: o.columnCapacity },
5441
+ ...o.columnCage ? { reinforcement: { ...o.columnCage } } : {}
3687
5442
  });
3688
- on(id, f === 0 ? `foundation_${i}_${j}` : `slab_${f - 1}`, "rebar");
5443
+ on(id, f === 0 ? o.raft ? "raft" : `foundation_${i}_${j}` : o.continuousColumns ? `column_${f - 1}_${i}_${j}` : `slab_${f - 1}`, "rebar");
3689
5444
  }
3690
5445
  }
3691
5446
  const beams = [];
3692
5447
  const beam = (id, size, pos, c1, c2) => {
3693
- parts.push({ id, material: mat, shape: { kind: "box", size }, position: pos, structure: { role: "beam" } });
5448
+ parts.push({ id, material: mat, shape: { kind: "box", size }, position: pos, structure: { role: "beam", ...rebarOf("beam") } });
3694
5449
  on(id, c1, "rebar");
3695
5450
  on(id, c2, "rebar");
3696
5451
  beams.push(id);
@@ -3705,13 +5460,13 @@ var GameDestruction = (() => {
3705
5460
  beam(`beam_z_${f}_${i}_${j}`, [bw, bh, zs[j + 1] - zs[j] - cs], [xs[i], top - bh / 2, (zs[j] + zs[j + 1]) / 2], `column_${f}_${i}_${j}`, `column_${f}_${i}_${j + 1}`);
3706
5461
  }
3707
5462
  }
3708
- const thick = last && roof ? T * 0.75 : T;
5463
+ const thick = last && roof ? o.roofThickness ?? T * 0.75 : T;
3709
5464
  parts.push({
3710
5465
  id: slabId,
3711
5466
  material: mat,
3712
5467
  shape: { kind: "box", size: [spanX + cs + 0.2, thick, spanZ + cs + 0.2] },
3713
5468
  position: [0, top + thick / 2, 0],
3714
- structure: { role: last && roof ? "roof" : "slab" }
5469
+ structure: { role: last && roof ? "roof" : "slab", ...rebarOf("slab") }
3715
5470
  });
3716
5471
  for (const b of beams) on(slabId, b, last && roof ? "resting" : "rebar");
3717
5472
  if (o.walls) {
@@ -3744,28 +5499,91 @@ var GameDestruction = (() => {
3744
5499
  }
3745
5500
  }
3746
5501
  }
3747
- return { name: "frame", parts, joints, tags: ["structure"] };
5502
+ return { name: "frame", parts, joints, tags: ["structure"], ...o.soil !== void 0 ? { soil: o.soil } : {} };
3748
5503
  };
3749
5504
 
3750
5505
  // src/size.ts
5506
+ var MAX_REBAR_RATIO = 0.04;
5507
+ var onRock = (spec) => {
5508
+ if (spec.soil === void 0) return spec;
5509
+ const { soil: _soil, ...rest } = spec;
5510
+ return rest;
5511
+ };
5512
+ var without = (spec, id) => {
5513
+ const reduced = { ...spec, parts: spec.parts.filter((p) => p.id !== id), joints: (spec.joints ?? []).filter((j) => j.a !== id && j.b !== id) };
5514
+ if (!spec.joints) delete reduced.joints;
5515
+ return reduced;
5516
+ };
5517
+ var sizeRebar = (spec, options = {}) => {
5518
+ const flexure = resolveFlexure(options.flexure);
5519
+ const materials = new Map(MATERIALS.map((m) => [m.id, m]));
5520
+ for (const m of options.materials ?? []) materials.set(m.id, m);
5521
+ const out = structuredClone(spec);
5522
+ const targets = out.parts.filter((p) => {
5523
+ const m = materials.get(p.material);
5524
+ return p.shape.kind === "box" && !!m && isReinforced(p) && isFlexural(p, m, flexure);
5525
+ });
5526
+ if (targets.length === 0) return out;
5527
+ const safety = options.safety ?? 0.65;
5528
+ const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
5529
+ const bending = (s) => {
5530
+ const w = createDestruction({ ...options.materials ? { materials: options.materials } : {}, structure: { flexure: options.flexure } });
5531
+ const id = w.add(onRock(s));
5532
+ return new Map(w.state(id).parts.map((p) => [p.id, p.flexure ?? 0]));
5533
+ };
5534
+ const base = bending(out);
5535
+ const factor = new Map(targets.map((p) => [p.id, (base.get(p.id) ?? 0) / safetyOf(p.structure.role)]));
5536
+ if (options.redundancy !== void 0) {
5537
+ const roles = options.redundancyRoles ?? ["column"];
5538
+ for (const lost of out.parts) {
5539
+ if (!lost.structure || !roles.includes(lost.structure.role)) continue;
5540
+ const l = bending(without(out, lost.id));
5541
+ for (const p of targets) if (p.id !== lost.id) factor.set(p.id, Math.max(factor.get(p.id), (l.get(p.id) ?? 0) / options.redundancy));
5542
+ }
5543
+ }
5544
+ for (const p of targets) {
5545
+ const f = factor.get(p.id);
5546
+ const g = bendGeometry(p);
5547
+ if (f <= 1 || g.gravity < 0) continue;
5548
+ const section = { b: sectionWidth(g, g.gravity), h: g.dims[g.gravity] };
5549
+ const steel = tensionSteel(p, g.dims, section, flexure);
5550
+ const m = materials.get(p.material);
5551
+ const need = f * sectionMoments(m, section, steel, flexure).ultimate;
5552
+ const momentOf = (area) => steelMoment(area, steel.yieldMPa, steel.d, section.b, m.strength.compressiveMPa, flexure);
5553
+ const cap = MAX_REBAR_RATIO * section.b * steel.d;
5554
+ if (steel.area >= cap) continue;
5555
+ let lo = steel.area, hi = cap;
5556
+ if (momentOf(hi) > need) {
5557
+ for (let k = 0; k < 60; k++) {
5558
+ const mid = (lo + hi) / 2;
5559
+ if (momentOf(mid) < need) lo = mid;
5560
+ else hi = mid;
5561
+ }
5562
+ }
5563
+ const ratio = hi / (section.b * steel.d);
5564
+ p.structure = { ...p.structure, rebar: { ...p.structure.rebar, ratio } };
5565
+ }
5566
+ return out;
5567
+ };
3751
5568
  var sizeStructure = (spec, options = {}) => {
3752
5569
  const safety = options.safety ?? 0.65;
3753
5570
  const redundancy = options.redundancy;
3754
5571
  const redundancyRoles = options.redundancyRoles ?? ["column"];
3755
5572
  const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
3756
- const loads = (s) => {
3757
- const w = createDestruction(options.materials ? { materials: options.materials } : {});
3758
- const id = w.add(s);
3759
- return new Map(w.state(id).parts.map((p) => [p.id, { load: p.load ?? 0, stress: p.stress ?? 0 }]));
5573
+ const loads = (s, wind) => {
5574
+ const w = createDestruction({ ...options.materials ? { materials: options.materials } : {}, ...wind ? { wind } : {} });
5575
+ const id = w.add(onRock(s));
5576
+ return new Map(w.state(id).parts.map((p) => [p.id, { load: p.load ?? 0, stress: p.stress ?? 0, wind: p.wind ?? 0 }]));
3760
5577
  };
3761
5578
  const out = structuredClone(spec);
3762
- const base = loads(out);
5579
+ const base = loads(out, options.wind);
5580
+ const gamma = options.wind ? resolveWind(options.wind).loadFactor : 0;
3763
5581
  const need = /* @__PURE__ */ new Map();
3764
5582
  for (const p of out.parts) {
3765
5583
  if (!p.structure || p.structure.role === "none") continue;
3766
5584
  const b = base.get(p.id);
3767
5585
  if (!b || b.stress <= 0) continue;
3768
- need.set(p.id, b.load / safetyOf(p.structure.role));
5586
+ need.set(p.id, (b.load + (b.wind > 0 ? gamma * b.wind / GRAVITY : 0)) / safetyOf(p.structure.role));
3769
5587
  }
3770
5588
  if (redundancy !== void 0) {
3771
5589
  for (const col of out.parts) {
@@ -3787,7 +5605,20 @@ var GameDestruction = (() => {
3787
5605
  const n = need.get(p.id);
3788
5606
  if (n !== void 0) p.structure = { ...p.structure, capacity: n };
3789
5607
  }
3790
- return out;
5608
+ if (options.wind) {
5609
+ const yieldMPa = resolveFlexure(options.flexure).rebarYieldMPa;
5610
+ for (const p of out.parts) {
5611
+ const b = base.get(p.id);
5612
+ if (!b || b.wind >= 0 || !p.reinforcement || p.shape.kind !== "box") continue;
5613
+ const pull = -(b.load + gamma * b.wind / GRAVITY) * GRAVITY;
5614
+ if (pull <= 0) continue;
5615
+ const dims = shapeDims(p.shape);
5616
+ const have = tensionCapacity(dims, { reinforcement: p.reinforcement, yieldMPa });
5617
+ const want = pull / safetyOf(p.structure?.role ?? "wall");
5618
+ if (have > 0 && want > have) p.reinforcement = { ...p.reinforcement, diameter: Math.min(0.04, p.reinforcement.diameter * Math.sqrt(want / have)) };
5619
+ }
5620
+ }
5621
+ return sizeRebar(out, options);
3791
5622
  };
3792
5623
 
3793
5624
  // src/locale.ts
@@ -3869,6 +5700,8 @@ var GameDestruction = (() => {
3869
5700
  primed: "about to explode",
3870
5701
  stress: "overload",
3871
5702
  collapse: "collapsed",
5703
+ settle: "ground gave way",
5704
+ sway: "sway",
3872
5705
  spall: "plaster came off",
3873
5706
  stage: "damage stage",
3874
5707
  expose: "rebar bare",
@@ -3957,6 +5790,8 @@ var GameDestruction = (() => {
3957
5790
  primed: "prestes a explodir",
3958
5791
  stress: "sobrecarga",
3959
5792
  collapse: "desabou",
5793
+ settle: "recalque do solo",
5794
+ sway: "balan\xE7o",
3960
5795
  spall: "reboco soltou",
3961
5796
  stage: "est\xE1gio de dano",
3962
5797
  expose: "vergalh\xE3o \xE0 mostra",
@@ -3971,115 +5806,6 @@ var GameDestruction = (() => {
3971
5806
  };
3972
5807
  var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
3973
5808
 
3974
- // ../game-contracts/dist/index.js
3975
- var IMPACT_CONTRACT = {
3976
- ballistic: { required: ["type", "point", "direction", "energy", "caliber"], optional: ["space", "source"], note: "bullets and pellets: joules and diameter in millimetres" },
3977
- blunt: { required: ["type", "point", "energy"], optional: ["direction", "radius", "space", "source"], note: "contact radius in metres (default 3 cm)" },
3978
- slash: { required: ["type", "point", "energy"], optional: ["direction", "radius", "space", "source"], note: "axes, knives, machetes" },
3979
- explosive: { required: ["type", "point", "energy", "radius"], optional: ["space", "source"], note: "point is the centre of the blast, radius the range in metres" },
3980
- fire: { required: ["type", "point", "temperature", "radius"], optional: ["duration", "space", "source"], note: "flame temperature in \xB0C; duration in seconds (call every frame with `dt`)" },
3981
- heat: { required: ["type", "point", "temperature", "radius"], optional: ["duration", "space", "source"], note: "heat without flame (cutting torch, hot plate)" }
3982
- };
3983
- var checkHit = (hit) => {
3984
- const c = IMPACT_CONTRACT[hit.type];
3985
- if (!c) return [`unknown damage type "${String(hit.type)}"`];
3986
- const out = [];
3987
- for (const f of c.required) {
3988
- const v = hit[f];
3989
- if (v === void 0 || (Array.isArray(v) ? v.some((n) => !Number.isFinite(n)) : typeof v === "number" && !Number.isFinite(v))) out.push(`${hit.type} hit needs "${f}"`);
3990
- }
3991
- return out;
3992
- };
3993
- var finiteJson = (v, path, out) => {
3994
- if (typeof v === "number") {
3995
- if (!Number.isFinite(v)) out.push(`${path} is not a finite number`);
3996
- } else if (Array.isArray(v)) v.forEach((x, i) => finiteJson(x, `${path}[${i}]`, out));
3997
- else if (v && typeof v === "object") for (const [k, x] of Object.entries(v)) finiteJson(x, `${path}.${k}`, out);
3998
- else if (typeof v === "function" || typeof v === "symbol" || typeof v === "bigint") out.push(`${path} is not JSON`);
3999
- };
4000
- var checkDestructible = (impl, options = {}) => {
4001
- const problems = [];
4002
- const known = options.materials ? new Set(options.materials) : null;
4003
- let spec;
4004
- try {
4005
- spec = impl.describe();
4006
- } catch (e) {
4007
- return { ok: false, problems: [`describe() threw: ${String(e)}`] };
4008
- }
4009
- if (!spec || !Array.isArray(spec.parts) || spec.parts.length === 0) return { ok: false, problems: ["describe() must return at least one part"] };
4010
- const ids = /* @__PURE__ */ new Set();
4011
- for (const p of spec.parts) {
4012
- if (ids.has(p.id)) problems.push(`duplicate part id "${p.id}"`);
4013
- ids.add(p.id);
4014
- if (known && !known.has(p.material)) problems.push(`part "${p.id}" has unknown material "${p.material}"`);
4015
- if (!p.shape || !["box", "cylinder", "sphere"].includes(p.shape.kind)) problems.push(`part "${p.id}" has no valid shape`);
4016
- if (!Array.isArray(p.position) || p.position.length !== 3) problems.push(`part "${p.id}" has no position`);
4017
- }
4018
- const jointIds = /* @__PURE__ */ new Set();
4019
- for (const j of spec.joints ?? []) {
4020
- if (!ids.has(j.a) || !ids.has(j.b)) problems.push(`joint "${j.id ?? `${j.a}~${j.b}`}" references an unknown part`);
4021
- const jid = j.id ?? `${j.a}~${j.b}`;
4022
- if (jointIds.has(jid)) problems.push(`duplicate joint id "${jid}"`);
4023
- jointIds.add(jid);
4024
- }
4025
- const checkState = (label) => {
4026
- let st;
4027
- try {
4028
- st = impl.state();
4029
- } catch (e) {
4030
- problems.push(`state() threw ${label}: ${String(e)}`);
4031
- return;
4032
- }
4033
- const sids = st.parts.map((p) => p.id).sort().join(",");
4034
- if (sids !== [...ids].sort().join(",")) problems.push(`state() part ids do not match describe() ${label}`);
4035
- for (const p of st.parts) if (!(p.hp >= 0)) problems.push(`part "${p.id}" has negative or invalid hp ${label}`);
4036
- };
4037
- const checkEvents = (events, label) => {
4038
- if (!Array.isArray(events)) {
4039
- problems.push(`${label} must return an array`);
4040
- return;
4041
- }
4042
- const evs = events;
4043
- evs.forEach((e, i) => {
4044
- if (e.object !== impl.id) problems.push(`${label} event ${i} (${e.type}) has object "${String(e.object)}", expected "${impl.id}"`);
4045
- const part = e.part;
4046
- if (part !== void 0 && !ids.has(part)) problems.push(`${label} event ${i} (${e.type}) names unknown part "${part}"`);
4047
- });
4048
- const bad = [];
4049
- finiteJson(evs, "events", bad);
4050
- problems.push(...bad.map((b) => `${label}: ${b}`));
4051
- try {
4052
- const s = JSON.stringify(evs);
4053
- if (JSON.stringify(JSON.parse(s)) !== s) problems.push(`${label} events do not survive a JSON round trip`);
4054
- } catch (e) {
4055
- problems.push(`${label} events are not JSON-serializable: ${String(e)}`);
4056
- }
4057
- };
4058
- checkState("before the hit");
4059
- const target = spec.parts[0];
4060
- try {
4061
- const hit = { type: "ballistic", point: [target.position[0], target.position[1], target.position[2]], direction: [0, 0, -1], energy: 500, caliber: 9 };
4062
- checkEvents(impl.hit(hit), "hit()");
4063
- } catch (e) {
4064
- problems.push(`hit() threw: ${String(e)}`);
4065
- }
4066
- checkState("after the hit");
4067
- if (impl.update) {
4068
- try {
4069
- checkEvents(impl.update(0.016), "update()");
4070
- } catch (e) {
4071
- problems.push(`update() threw: ${String(e)}`);
4072
- }
4073
- checkState("after update");
4074
- }
4075
- try {
4076
- impl.reset?.();
4077
- } catch (e) {
4078
- problems.push(`reset() threw: ${String(e)}`);
4079
- }
4080
- return { ok: problems.length === 0, problems };
4081
- };
4082
-
4083
5809
  // src/contracts.ts
4084
5810
  var checkDestructible2 = (impl, options = {}) => {
4085
5811
  const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []] };