@adatechnology/game-destruction 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;
@@ -2494,22 +4158,25 @@ var GameDestruction = (() => {
2494
4158
  const crackLen = clamp2(0.03 + ratio * 0.2, 0.03, 0.8);
2495
4159
  let exitPoint;
2496
4160
  if (penetrated) {
2497
- exitPoint = vec.madd(entry, d, Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant));
4161
+ const exitDist = p.spec.contains || p.spec.fill !== void 0 && p.spec.fill < 0.5 || p.thickness < pathLen * 0.5 ? pathLen : Math.min(pathLen, effM + layersThickness(p.layers, face) / slant + layersThickness(p.layers, -face) / slant);
4162
+ exitPoint = vec.madd(entry, d, exitDist);
2498
4163
  const hole = addHole(o, p, entry, n, caliber / 1e3 * (mat.ductility >= 0.3 ? 1 : 1.5), true, exitPoint, src);
2499
4164
  if (p.layers.length > 0) spallOuter(o, p, exitPoint, -face, vec.scale(n, -1), Math.max(0.03, caliber / 1e3 * 4), src);
2500
4165
  addBreach(o, p, entry, Math.PI * (hole.diameter / 2) * (hole.diameter / 2), E, d, src);
4166
+ 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
4167
  if (brittle && mat.fracture === "shatter") addCrack(o, p, entry, n, crackLen, src);
2502
- if (p.spec.contains && (p.st.liters ?? 0) > 0) {
4168
+ if (p.spec.contains && (p.st.liters ?? 0) > 1e-4) {
2503
4169
  emit({
2504
4170
  ...base(o, src),
2505
4171
  type: "leak",
2506
4172
  part: id,
2507
4173
  fluid: p.spec.contains.fluid,
2508
4174
  point: entry,
2509
- direction: n,
4175
+ direction: p.spec.contains.outflow ? vec.normalize(rotateVec(o.st.rotation, rotateVec(p.q, p.spec.contains.outflow))) : n,
2510
4176
  rate: holeRate(hole, !!p.spec.contains.pressurized),
2511
4177
  pressurized: !!p.spec.contains.pressurized
2512
4178
  });
4179
+ rollIgnition(o, p, round);
2513
4180
  }
2514
4181
  } else if (mat.ductility >= 0.5) {
2515
4182
  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 +4187,7 @@ var GameDestruction = (() => {
2520
4187
  debris(o, p, mat.hitDebris, entry, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
2521
4188
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
2522
4189
  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 };
4190
+ return { penetrated, exitEnergy: penetrated ? penMm !== void 0 ? E * (1 - thr / Ep) : E - thr / (hard ? piercing : 1) : 0, exitPoint, damage: dmg };
2524
4191
  };
2525
4192
  const melee = (o, p, kind, E, point, n, d, contactR, src) => {
2526
4193
  const mat = p.mat;
@@ -2540,8 +4207,9 @@ var GameDestruction = (() => {
2540
4207
  }
2541
4208
  debris(o, p, mat.hitDebris, point, n, clamp2(Math.round(E / 25) + 1, 1, 10), clamp2(Math.sqrt(E) / 8, 1, 6), src);
2542
4209
  }
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);
4210
+ if (kind === "blunt") {
4211
+ if (!(p.anchored && p.spec.tags?.includes("terrain"))) wearJoints(o, p, E * 0.3, void 0, 0, src);
4212
+ } else wearJoints(o, p, E * 0.3, point, 0.06, src);
2545
4213
  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
4214
  const scale = mat.damageScale?.[kind] ?? 1;
2547
4215
  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 +4256,8 @@ var GameDestruction = (() => {
2588
4256
  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
4257
  const dealt = applyDamage(o, p, amount, "explosive", point, null, Ein * 0.3, 40, src);
2590
4258
  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);
4259
+ if (p.bend && flexure.enabled) bendByBlast(o, p, point, energy, radius, reach, src);
4260
+ if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
2592
4261
  return reach;
2593
4262
  };
2594
4263
  const blastObject = (o, point, energy, radius, src) => {
@@ -2651,7 +4320,7 @@ var GameDestruction = (() => {
2651
4320
  };
2652
4321
  const FLAG_LOST = 1;
2653
4322
  const FLAG_OPEN = 2;
2654
- const blastPlain = (point, energy, radius, src) => {
4323
+ const blastPlain = (point, energy, radius, src, lethal) => {
2655
4324
  const out = [];
2656
4325
  for (const o of [...objects.values()]) {
2657
4326
  if (vec.distance(toWorld(o.st.position, o.st.rotation, o.bc), point) - o.br >= radius) continue;
@@ -2659,7 +4328,7 @@ var GameDestruction = (() => {
2659
4328
  if (r.part) out.push(r);
2660
4329
  }
2661
4330
  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 } : {} }));
4331
+ 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
4332
  }
2664
4333
  return out;
2665
4334
  };
@@ -2668,7 +4337,7 @@ var GameDestruction = (() => {
2668
4337
  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
4338
  });
2670
4339
  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) => {
4340
+ const blastScene = (point, energy, radius, src, cfg, only, lethal) => {
2672
4341
  const mult = confinementOf(cfg.conf, point, radius);
2673
4342
  const E = energy * mult;
2674
4343
  const shelter = cfg.shelter;
@@ -2775,7 +4444,7 @@ var GameDestruction = (() => {
2775
4444
  den += f;
2776
4445
  }
2777
4446
  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 } : {} });
4447
+ 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
4448
  results.set(im, res);
2780
4449
  report(im).shelter = k;
2781
4450
  }
@@ -2873,10 +4542,11 @@ var GameDestruction = (() => {
2873
4542
  if (!rep) reports.set(key, rep = { shelter: 1, confinement: mult, fragmentHits: 0 });
2874
4543
  rep.fragmentHits++;
2875
4544
  };
2876
- const blastWorld = (point, energy, radius, src, call, only) => {
4545
+ const blastWorld = (point, energy, radius, src, call, only, lethal) => {
2877
4546
  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);
4547
+ const res = !cfg.shelter && !cfg.frag && cfg.conf === null ? blastPlain(point, energy, radius, src, lethal) : blastScene(point, energy, radius, src, cfg, only, lethal);
4548
+ lightLeaksNear(point, radius);
4549
+ return res;
2880
4550
  };
2881
4551
  const heatObject = (o, point, T, radius, duration, src) => {
2882
4552
  const start = cur.length;
@@ -2895,6 +4565,7 @@ var GameDestruction = (() => {
2895
4565
  p.st.temperature += (T - p.st.temperature) * (1 - Math.exp(-k * duration));
2896
4566
  }
2897
4567
  o.active = true;
4568
+ if (T >= FLAME_LIGHTS_LEAK_C && !p.rt.jet && !p.rt.lit && leaksFlammable(p)) p.rt.lit = true;
2898
4569
  checkIgnite(o, p, src);
2899
4570
  }
2900
4571
  res.events = cur.slice(start);
@@ -2914,7 +4585,7 @@ var GameDestruction = (() => {
2914
4585
  if (hit.type === "ballistic") {
2915
4586
  const loc = locate(o, point, dir, 0.05, only);
2916
4587
  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);
4588
+ const out = ballistic(o, loc.p, loc.point, loc.normal, loc.dir, E, hit.caliber ?? 9, loc.pathLen, src, hit);
2918
4589
  res = { ...res, part: loc.p.spec.id, point: loc.point, damage: out.damage, penetrated: out.penetrated, exitEnergy: out.exitEnergy };
2919
4590
  }
2920
4591
  } else if (hit.type === "blunt" || hit.type === "slash") {
@@ -3068,7 +4739,8 @@ var GameDestruction = (() => {
3068
4739
  if (rate > 0) {
3069
4740
  s.liters = Math.max(0, (s.liters ?? 0) - rate * dt);
3070
4741
  hot = true;
3071
- if (FLAMMABLE_FLUIDS.includes(cont.fluid) && s.temperature >= 350 && !p.rt.jet) {
4742
+ if (FLAMMABLE_FLUIDS.includes(cont.fluid) && (s.temperature >= 350 || p.rt.lit) && !p.rt.jet) {
4743
+ p.rt.lit = false;
3072
4744
  if (ex && chainDelayOf(p.spec, "fire") !== void 0) {
3073
4745
  prime(o, p, "fire");
3074
4746
  continue;
@@ -3083,6 +4755,7 @@ var GameDestruction = (() => {
3083
4755
  }
3084
4756
  }
3085
4757
  }
4758
+ if ((s.liters ?? 0) <= 0) p.rt.lit = false;
3086
4759
  if (p.rt.jet && (s.liters ?? 0) <= 0) {
3087
4760
  p.rt.jet = false;
3088
4761
  s.burning = false;
@@ -3132,6 +4805,25 @@ var GameDestruction = (() => {
3132
4805
  }
3133
4806
  o.active = hot;
3134
4807
  };
4808
+ const igniteLeaksNearFire = (sources, dt) => {
4809
+ const { radius, chancePerSecond } = ignitionRules.fireNear;
4810
+ if (chancePerSecond <= 0) return;
4811
+ const chance = 1 - Math.exp(-chancePerSecond * dt);
4812
+ for (const src of sources) {
4813
+ if (!src.p.st.burning && !src.p.rt.jet) continue;
4814
+ const sc = partXf(src.o, src.p).c;
4815
+ for (const o of objects.values()) {
4816
+ for (const q of o.parts) {
4817
+ if (q === src.p || isLost(q) || q.rt.lit || q.rt.jet || !leaksFlammable(q)) continue;
4818
+ if (vec.distance(partXf(o, q).c, sc) - q.bound - src.p.bound > radius) continue;
4819
+ if (rnd() < chance) {
4820
+ q.rt.lit = true;
4821
+ o.active = true;
4822
+ }
4823
+ }
4824
+ }
4825
+ }
4826
+ };
3135
4827
  const spread2 = (sources, dt) => {
3136
4828
  for (const src of sources) {
3137
4829
  if (!src.p.st.burning && !src.p.rt.jet) continue;
@@ -3257,8 +4949,8 @@ var GameDestruction = (() => {
3257
4949
  return;
3258
4950
  }
3259
4951
  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]];
4952
+ o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
4953
+ applySoilPose(o);
3262
4954
  },
3263
4955
  splitGroup(id, group, newId) {
3264
4956
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -3281,13 +4973,17 @@ var GameDestruction = (() => {
3281
4973
  const s = { parts: o.spec.parts.filter((p) => keep(p.id)), joints: (o.spec.joints ?? []).filter((j) => keep(j.a) && keep(j.b)) };
3282
4974
  if (o.spec.name !== void 0) s.name = o.spec.name;
3283
4975
  if (o.spec.tags) s.tags = [...o.spec.tags];
4976
+ if (o.spec.soil !== void 0) s.soil = o.spec.soil;
3284
4977
  return clone(s);
3285
4978
  };
3286
4979
  const inNew = (pid) => moved.has(pid);
3287
4980
  const inOld = (pid) => !moved.has(pid);
3288
4981
  const pos = o.st.position, rot = o.st.rotation;
3289
4982
  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))));
4983
+ const restCarry = carryFor(inOld, (a, b) => !(inNew(a) && inNew(b)));
4984
+ if (o.st.soil) restCarry.soil = o.st.soil;
4985
+ if (o.soil?.sets.size) restCarry.soilSets = Object.fromEntries(o.soil.sets);
4986
+ const rest = build(sub(inOld), id, o.soil ? o.base.pos : pos, o.soil ? o.base.rot : rot, restCarry);
3291
4987
  objects.set(id, rest);
3292
4988
  objects.set(nid, fresh);
3293
4989
  return nid;
@@ -3312,7 +5008,8 @@ var GameDestruction = (() => {
3312
5008
  const r = raycastInternal(origin, direction, maxDistance);
3313
5009
  return r ? { object: r.object, part: r.part, point: r.point, normal: r.normal, distance: r.distance } : null;
3314
5010
  },
3315
- shoot(origin, direction, energy, caliber, source, piercing) {
5011
+ shoot(origin, direction, energy, caliber, source, round) {
5012
+ const extras = typeof round === "number" ? { piercing: round } : round ?? {};
3316
5013
  return withCollapses(run(() => {
3317
5014
  const results = [];
3318
5015
  const d = vec.normalize(direction);
@@ -3324,7 +5021,7 @@ var GameDestruction = (() => {
3324
5021
  if (!rh) break;
3325
5022
  const im = impls.get(rh.object);
3326
5023
  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 } : {} });
5024
+ const r = implHit(im, { type: "ballistic", point: rh.point, direction: d, energy: E, caliber, ...extras, ...source !== void 0 ? { source } : {} });
3328
5025
  r.part = rh.part;
3329
5026
  r.point = rh.point;
3330
5027
  r.exitEnergy = r.penetrated ? E * 0.5 : 0;
@@ -3338,7 +5035,7 @@ var GameDestruction = (() => {
3338
5035
  const o = getObj(rh.object);
3339
5036
  const p = getPart(o, rh.part);
3340
5037
  const start = cur.length;
3341
- const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, piercing ?? 1);
5038
+ const out = ballistic(o, p, rh.point, rh.normal, d, E, caliber, rh.pathLen, source, extras);
3342
5039
  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
5040
  ignore.add(`${o.id}\0${p.spec.id}`);
3344
5041
  if (!out.penetrated || !out.exitPoint) break;
@@ -3437,6 +5134,52 @@ var GameDestruction = (() => {
3437
5134
  o.active = true;
3438
5135
  });
3439
5136
  },
5137
+ setWind(next) {
5138
+ windOpts = { ...windOpts, ...Object.fromEntries(Object.entries(next).filter(([, v]) => v !== void 0)) };
5139
+ windCfg = resolveWind(windOpts);
5140
+ for (const o of objects.values()) {
5141
+ if (!o.hasStructure || o.spec.wind === false) continue;
5142
+ o.wind ?? (o.wind = newWind(o));
5143
+ o.dirty = true;
5144
+ o.active = true;
5145
+ }
5146
+ },
5147
+ vent(objectId, partId, opening = {}) {
5148
+ if (impls.has(objectId)) return false;
5149
+ const o = getObj(objectId);
5150
+ const p = getPart(o, partId);
5151
+ const c = p.spec.contains;
5152
+ if (!c || isLost(p) || (p.st.liters ?? 0) <= 0) return false;
5153
+ if (p.st.holes.some((h) => h.vent)) return true;
5154
+ return run(() => {
5155
+ const wp = opening.point ?? partXf(o, p).c;
5156
+ const wn = vec.normalize(opening.normal ?? [0, 1, 0]);
5157
+ const hole = { point: objLocal(o, wp), normal: objLocalDir(o, wn), diameter: opening.diameter ?? VENT_DIAMETER, through: true, vent: true };
5158
+ p.st.holes.push(hole);
5159
+ if (p.st.holes.length > maxHoles) p.st.holes.shift();
5160
+ o.active = true;
5161
+ emit({
5162
+ ...base(o),
5163
+ type: "leak",
5164
+ part: partId,
5165
+ fluid: c.fluid,
5166
+ point: wp,
5167
+ direction: c.outflow ? vec.normalize(rotateVec(o.st.rotation, rotateVec(p.q, c.outflow))) : wn,
5168
+ rate: holeRate(hole, !!c.pressurized),
5169
+ pressurized: !!c.pressurized
5170
+ });
5171
+ return true;
5172
+ }).value;
5173
+ },
5174
+ seal(objectId, partId) {
5175
+ if (impls.has(objectId)) return 0;
5176
+ const o = getObj(objectId);
5177
+ const p = getPart(o, partId);
5178
+ const before = p.st.holes.length;
5179
+ p.st.holes = p.st.holes.filter((h) => !h.vent);
5180
+ o.active = true;
5181
+ return before - p.st.holes.length;
5182
+ },
3440
5183
  setLiters(objectId, partId, liters) {
3441
5184
  if (impls.has(objectId)) return;
3442
5185
  const p = getPart(getObj(objectId), partId);
@@ -3450,7 +5193,12 @@ var GameDestruction = (() => {
3450
5193
  time += dt;
3451
5194
  const sources = [];
3452
5195
  for (const o of [...objects.values()]) if (o.active) stepObject(o, dt, sources);
3453
- if (sources.length) spread2(sources, dt);
5196
+ for (const o of [...objects.values()]) if (o.soil && objects.get(o.id) === o) stepSoil(o, dt);
5197
+ for (const o of [...objects.values()]) if (o.wind && objects.get(o.id) === o) stepWind(o, dt);
5198
+ if (sources.length) {
5199
+ spread2(sources, dt);
5200
+ igniteLeaksNearFire(sources, dt);
5201
+ }
3454
5202
  detonatePrimed(time);
3455
5203
  releaseFront();
3456
5204
  for (const im of [...impls.values()]) {
@@ -3472,7 +5220,7 @@ var GameDestruction = (() => {
3472
5220
  return;
3473
5221
  }
3474
5222
  const o = getObj(id);
3475
- objects.set(id, build(o.spec, id, o.st.position, o.st.rotation));
5223
+ objects.set(id, build(o.spec, id, o.base.pos, o.base.rot));
3476
5224
  },
3477
5225
  on(type, fn) {
3478
5226
  let set = listeners.get(type);
@@ -3489,7 +5237,7 @@ var GameDestruction = (() => {
3489
5237
  rng: rng.state(),
3490
5238
  counter,
3491
5239
  objects: [...objects.values()].map((o) => {
3492
- const runtime = { parts: {}, joints: {}, collapsed: [...o.collapsed] };
5240
+ 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
5241
  for (const p of o.parts) runtime.parts[p.spec.id] = clone(p.rt);
3494
5242
  for (const j of o.joints) runtime.joints[j.spec.id] = j.step;
3495
5243
  return { id: o.id, spec: clone(o.spec), state: clone(o.st), runtime };
@@ -3510,9 +5258,17 @@ var GameDestruction = (() => {
3510
5258
  state.parts.map((st) => [st.id, { st, rt: e.runtime?.parts[st.id] ? clone(e.runtime.parts[st.id]) : { ...freshRuntime(), prevTemp: st.temperature } }])
3511
5259
  ),
3512
5260
  joints: new Map(state.joints.map((st) => [st.id, { st, step: e.runtime?.joints[st.id] ?? 0 }])),
3513
- collapsed: e.runtime?.collapsed
5261
+ collapsed: e.runtime?.collapsed,
5262
+ ...e.runtime?.wind ? { wind: e.runtime.wind } : {}
3514
5263
  };
3515
- objects.set(e.id, build(clone(e.spec), e.id, state.position, state.rotation, carry));
5264
+ let pos = state.position, rot = state.rotation;
5265
+ if (state.soil) {
5266
+ carry.soil = state.soil;
5267
+ if (e.runtime?.soil) carry.soilSets = e.runtime.soil;
5268
+ rot = quat.normalize(quat.mul(state.rotation, quat.conjugate(state.soil.offset.rotation)));
5269
+ pos = vec.sub(state.position, rotateVec(rot, state.soil.offset.position));
5270
+ }
5271
+ objects.set(e.id, build(clone(e.spec), e.id, pos, rot, carry));
3516
5272
  }
3517
5273
  for (const e of snap.impls ?? []) {
3518
5274
  const im = impls.get(e.id);
@@ -3526,21 +5282,6 @@ var GameDestruction = (() => {
3526
5282
  return world;
3527
5283
  };
3528
5284
 
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
5285
  // src/data/assemblies.json
3545
5286
  var assemblies_default = [
3546
5287
  {
@@ -3653,21 +5394,35 @@ var GameDestruction = (() => {
3653
5394
  const dw = o.doorWidth ?? 1, dh = o.doorHeight ?? 2.1;
3654
5395
  const mat = o.material ?? "concrete", wmat = o.wallMaterial ?? "brick";
3655
5396
  const roof = o.roof ?? true;
5397
+ const rebarOf = (kind) => o.rebar === false ? {} : { rebar: { ratio: o.rebar?.[kind] ?? (kind === "beam" ? 0.01 : 5e-3) } };
3656
5398
  const xs = Array.from({ length: nx }, (_, i) => -spanX / 2 + i * spanX / (nx - 1));
3657
5399
  const zs = Array.from({ length: nz }, (_, j) => -spanZ / 2 + j * spanZ / (nz - 1));
3658
5400
  const parts = [];
3659
5401
  const joints = [];
3660
5402
  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
- });
5403
+ const pad = o.footing ?? cs * 2.2;
5404
+ const pile = o.piles ? { pile: { ...o.piles } } : {};
5405
+ if (o.raft) {
5406
+ parts.push({
5407
+ id: "raft",
5408
+ material: mat,
5409
+ shape: { kind: "box", size: [spanX + pad, 0.4, spanZ + pad] },
5410
+ position: [0, -0.2, 0],
5411
+ structure: { role: "foundation", ...pile },
5412
+ anchored: true
5413
+ });
5414
+ } else {
5415
+ for (let i = 0; i < nx; i++) {
5416
+ for (let j = 0; j < nz; j++) {
5417
+ parts.push({
5418
+ id: `foundation_${i}_${j}`,
5419
+ material: mat,
5420
+ shape: { kind: "box", size: [pad, 0.3, pad] },
5421
+ position: [xs[i], -0.15, zs[j]],
5422
+ structure: { role: "foundation", ...pile },
5423
+ anchored: true
5424
+ });
5425
+ }
3671
5426
  }
3672
5427
  }
3673
5428
  for (let f = 0; f < floors; f++) {
@@ -3683,14 +5438,15 @@ var GameDestruction = (() => {
3683
5438
  material: mat,
3684
5439
  shape: { kind: "box", size: [cs, H, cs] },
3685
5440
  position: [xs[i], yb + H / 2, zs[j]],
3686
- structure: o.columnCapacity === void 0 ? { role: "column" } : { role: "column", capacity: o.columnCapacity }
5441
+ structure: o.columnCapacity === void 0 ? { role: "column" } : { role: "column", capacity: o.columnCapacity },
5442
+ ...o.columnCage ? { reinforcement: { ...o.columnCage } } : {}
3687
5443
  });
3688
- on(id, f === 0 ? `foundation_${i}_${j}` : `slab_${f - 1}`, "rebar");
5444
+ on(id, f === 0 ? o.raft ? "raft" : `foundation_${i}_${j}` : o.continuousColumns ? `column_${f - 1}_${i}_${j}` : `slab_${f - 1}`, "rebar");
3689
5445
  }
3690
5446
  }
3691
5447
  const beams = [];
3692
5448
  const beam = (id, size, pos, c1, c2) => {
3693
- parts.push({ id, material: mat, shape: { kind: "box", size }, position: pos, structure: { role: "beam" } });
5449
+ parts.push({ id, material: mat, shape: { kind: "box", size }, position: pos, structure: { role: "beam", ...rebarOf("beam") } });
3694
5450
  on(id, c1, "rebar");
3695
5451
  on(id, c2, "rebar");
3696
5452
  beams.push(id);
@@ -3705,13 +5461,13 @@ var GameDestruction = (() => {
3705
5461
  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
5462
  }
3707
5463
  }
3708
- const thick = last && roof ? T * 0.75 : T;
5464
+ const thick = last && roof ? o.roofThickness ?? T * 0.75 : T;
3709
5465
  parts.push({
3710
5466
  id: slabId,
3711
5467
  material: mat,
3712
5468
  shape: { kind: "box", size: [spanX + cs + 0.2, thick, spanZ + cs + 0.2] },
3713
5469
  position: [0, top + thick / 2, 0],
3714
- structure: { role: last && roof ? "roof" : "slab" }
5470
+ structure: { role: last && roof ? "roof" : "slab", ...rebarOf("slab") }
3715
5471
  });
3716
5472
  for (const b of beams) on(slabId, b, last && roof ? "resting" : "rebar");
3717
5473
  if (o.walls) {
@@ -3744,28 +5500,91 @@ var GameDestruction = (() => {
3744
5500
  }
3745
5501
  }
3746
5502
  }
3747
- return { name: "frame", parts, joints, tags: ["structure"] };
5503
+ return { name: "frame", parts, joints, tags: ["structure"], ...o.soil !== void 0 ? { soil: o.soil } : {} };
3748
5504
  };
3749
5505
 
3750
5506
  // src/size.ts
5507
+ var MAX_REBAR_RATIO = 0.04;
5508
+ var onRock = (spec) => {
5509
+ if (spec.soil === void 0) return spec;
5510
+ const { soil: _soil, ...rest } = spec;
5511
+ return rest;
5512
+ };
5513
+ var without = (spec, id) => {
5514
+ const reduced = { ...spec, parts: spec.parts.filter((p) => p.id !== id), joints: (spec.joints ?? []).filter((j) => j.a !== id && j.b !== id) };
5515
+ if (!spec.joints) delete reduced.joints;
5516
+ return reduced;
5517
+ };
5518
+ var sizeRebar = (spec, options = {}) => {
5519
+ const flexure = resolveFlexure(options.flexure);
5520
+ const materials = new Map(MATERIALS.map((m) => [m.id, m]));
5521
+ for (const m of options.materials ?? []) materials.set(m.id, m);
5522
+ const out = structuredClone(spec);
5523
+ const targets = out.parts.filter((p) => {
5524
+ const m = materials.get(p.material);
5525
+ return p.shape.kind === "box" && !!m && isReinforced(p) && isFlexural(p, m, flexure);
5526
+ });
5527
+ if (targets.length === 0) return out;
5528
+ const safety = options.safety ?? 0.65;
5529
+ const safetyOf = (role) => options.safetyByRole?.[role] ?? safety;
5530
+ const bending = (s) => {
5531
+ const w = createDestruction({ ...options.materials ? { materials: options.materials } : {}, structure: { flexure: options.flexure } });
5532
+ const id = w.add(onRock(s));
5533
+ return new Map(w.state(id).parts.map((p) => [p.id, p.flexure ?? 0]));
5534
+ };
5535
+ const base = bending(out);
5536
+ const factor = new Map(targets.map((p) => [p.id, (base.get(p.id) ?? 0) / safetyOf(p.structure.role)]));
5537
+ if (options.redundancy !== void 0) {
5538
+ const roles = options.redundancyRoles ?? ["column"];
5539
+ for (const lost of out.parts) {
5540
+ if (!lost.structure || !roles.includes(lost.structure.role)) continue;
5541
+ const l = bending(without(out, lost.id));
5542
+ 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));
5543
+ }
5544
+ }
5545
+ for (const p of targets) {
5546
+ const f = factor.get(p.id);
5547
+ const g = bendGeometry(p);
5548
+ if (f <= 1 || g.gravity < 0) continue;
5549
+ const section = { b: sectionWidth(g, g.gravity), h: g.dims[g.gravity] };
5550
+ const steel = tensionSteel(p, g.dims, section, flexure);
5551
+ const m = materials.get(p.material);
5552
+ const need = f * sectionMoments(m, section, steel, flexure).ultimate;
5553
+ const momentOf = (area) => steelMoment(area, steel.yieldMPa, steel.d, section.b, m.strength.compressiveMPa, flexure);
5554
+ const cap = MAX_REBAR_RATIO * section.b * steel.d;
5555
+ if (steel.area >= cap) continue;
5556
+ let lo = steel.area, hi = cap;
5557
+ if (momentOf(hi) > need) {
5558
+ for (let k = 0; k < 60; k++) {
5559
+ const mid = (lo + hi) / 2;
5560
+ if (momentOf(mid) < need) lo = mid;
5561
+ else hi = mid;
5562
+ }
5563
+ }
5564
+ const ratio = hi / (section.b * steel.d);
5565
+ p.structure = { ...p.structure, rebar: { ...p.structure.rebar, ratio } };
5566
+ }
5567
+ return out;
5568
+ };
3751
5569
  var sizeStructure = (spec, options = {}) => {
3752
5570
  const safety = options.safety ?? 0.65;
3753
5571
  const redundancy = options.redundancy;
3754
5572
  const redundancyRoles = options.redundancyRoles ?? ["column"];
3755
5573
  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 }]));
5574
+ const loads = (s, wind) => {
5575
+ const w = createDestruction({ ...options.materials ? { materials: options.materials } : {}, ...wind ? { wind } : {} });
5576
+ const id = w.add(onRock(s));
5577
+ return new Map(w.state(id).parts.map((p) => [p.id, { load: p.load ?? 0, stress: p.stress ?? 0, wind: p.wind ?? 0 }]));
3760
5578
  };
3761
5579
  const out = structuredClone(spec);
3762
- const base = loads(out);
5580
+ const base = loads(out, options.wind);
5581
+ const gamma = options.wind ? resolveWind(options.wind).loadFactor : 0;
3763
5582
  const need = /* @__PURE__ */ new Map();
3764
5583
  for (const p of out.parts) {
3765
5584
  if (!p.structure || p.structure.role === "none") continue;
3766
5585
  const b = base.get(p.id);
3767
5586
  if (!b || b.stress <= 0) continue;
3768
- need.set(p.id, b.load / safetyOf(p.structure.role));
5587
+ need.set(p.id, (b.load + (b.wind > 0 ? gamma * b.wind / GRAVITY : 0)) / safetyOf(p.structure.role));
3769
5588
  }
3770
5589
  if (redundancy !== void 0) {
3771
5590
  for (const col of out.parts) {
@@ -3787,7 +5606,20 @@ var GameDestruction = (() => {
3787
5606
  const n = need.get(p.id);
3788
5607
  if (n !== void 0) p.structure = { ...p.structure, capacity: n };
3789
5608
  }
3790
- return out;
5609
+ if (options.wind) {
5610
+ const yieldMPa = resolveFlexure(options.flexure).rebarYieldMPa;
5611
+ for (const p of out.parts) {
5612
+ const b = base.get(p.id);
5613
+ if (!b || b.wind >= 0 || !p.reinforcement || p.shape.kind !== "box") continue;
5614
+ const pull = -(b.load + gamma * b.wind / GRAVITY) * GRAVITY;
5615
+ if (pull <= 0) continue;
5616
+ const dims = shapeDims(p.shape);
5617
+ const have = tensionCapacity(dims, { reinforcement: p.reinforcement, yieldMPa });
5618
+ const want = pull / safetyOf(p.structure?.role ?? "wall");
5619
+ if (have > 0 && want > have) p.reinforcement = { ...p.reinforcement, diameter: Math.min(0.04, p.reinforcement.diameter * Math.sqrt(want / have)) };
5620
+ }
5621
+ }
5622
+ return sizeRebar(out, options);
3791
5623
  };
3792
5624
 
3793
5625
  // src/locale.ts
@@ -3869,6 +5701,8 @@ var GameDestruction = (() => {
3869
5701
  primed: "about to explode",
3870
5702
  stress: "overload",
3871
5703
  collapse: "collapsed",
5704
+ settle: "ground gave way",
5705
+ sway: "sway",
3872
5706
  spall: "plaster came off",
3873
5707
  stage: "damage stage",
3874
5708
  expose: "rebar bare",
@@ -3957,6 +5791,8 @@ var GameDestruction = (() => {
3957
5791
  primed: "prestes a explodir",
3958
5792
  stress: "sobrecarga",
3959
5793
  collapse: "desabou",
5794
+ settle: "recalque do solo",
5795
+ sway: "balan\xE7o",
3960
5796
  spall: "reboco soltou",
3961
5797
  stage: "est\xE1gio de dano",
3962
5798
  expose: "vergalh\xE3o \xE0 mostra",
@@ -3971,115 +5807,6 @@ var GameDestruction = (() => {
3971
5807
  };
3972
5808
  var resolveStrings = (locale) => locales[locale ?? "en"] ?? locales.en;
3973
5809
 
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
5810
  // src/contracts.ts
4084
5811
  var checkDestructible2 = (impl, options = {}) => {
4085
5812
  const contractOptions = { materials: [...MATERIALS.map((m) => m.id), ...options.materials ?? []] };