@adatechnology/game-items-three 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,6 +29,7 @@ var GameItemsThree = (() => {
29
29
  ITEM_FAMILIES: () => ITEM_FAMILIES,
30
30
  ITEM_PREFIX: () => ITEM_PREFIX,
31
31
  MODEL_BUDGET_DEVIATIONS: () => MODEL_BUDGET_DEVIATIONS,
32
+ OPENING_NAME: () => OPENING_NAME,
32
33
  ROW_LIMIT: () => ROW_LIMIT,
33
34
  RULER_HEIGHT: () => RULER_HEIGHT,
34
35
  RULER_ID: () => RULER_ID,
@@ -42,6 +43,7 @@ var GameItemsThree = (() => {
42
43
  WEAR_AMOUNT: () => WEAR_AMOUNT,
43
44
  WEAR_TAGS: () => WEAR_TAGS,
44
45
  WEAR_TARGET: () => WEAR_TARGET,
46
+ asLid: () => asLid,
45
47
  buildItem: () => buildItem,
46
48
  buildItemLod: () => buildItemLod,
47
49
  canonicalFrame: () => canonicalFrame,
@@ -60,14 +62,18 @@ var GameItemsThree = (() => {
60
62
  itemFamily: () => itemFamily,
61
63
  itemModelBuilder: () => itemModelBuilder,
62
64
  itemModelEntry: () => itemModelEntry,
65
+ itemOpening: () => itemOpening,
63
66
  itemThumbnail: () => itemThumbnail,
64
67
  itemThumbnailSvg: () => itemThumbnailSvg,
65
68
  listItemModels: () => listItemModels,
66
69
  mergeItem: () => mergeItem,
70
+ openItem: () => openItem,
67
71
  registerItemModel: () => registerItemModel,
68
72
  renderItemThumbnail: () => renderItemThumbnail,
73
+ setOpeningContents: () => setOpeningContents,
69
74
  setWear: () => setWear,
70
75
  shade: () => shade,
76
+ shapeDestruction: () => shapeDestruction,
71
77
  shapeOf: () => shapeOf,
72
78
  shapeThumbnailSvg: () => shapeThumbnailSvg,
73
79
  shelvesLayout: () => shelvesLayout,
@@ -75,6 +81,7 @@ var GameItemsThree = (() => {
75
81
  tagForDestruction: () => tagForDestruction,
76
82
  thumbnailPatch: () => thumbnailPatch,
77
83
  triangleBudgetOf: () => triangleBudgetOf,
84
+ turnItem: () => turnItem,
78
85
  unregisterItemModel: () => unregisterItemModel,
79
86
  wearableMaterials: () => wearableMaterials
80
87
  });
@@ -2812,6 +2819,55 @@ var GameItemsThree = (() => {
2812
2819
  ends: 22e-5
2813
2820
  }
2814
2821
  }
2822
+ },
2823
+ plastic_bottle: {
2824
+ models: [
2825
+ "water_bottle",
2826
+ "water_jug_5l",
2827
+ "dirty_water",
2828
+ "sports_drink",
2829
+ "coconut_water",
2830
+ "cooking_oil",
2831
+ "alcohol_70",
2832
+ "fuel_jerrycan"
2833
+ ],
2834
+ rule: {
2835
+ hollow: true,
2836
+ welded: true,
2837
+ skinTags: [
2838
+ "paper",
2839
+ "food"
2840
+ ],
2841
+ materials: {
2842
+ glass: "plastic"
2843
+ }
2844
+ }
2845
+ },
2846
+ glass_bottle: {
2847
+ models: [
2848
+ "cachaca",
2849
+ "communion_wine",
2850
+ "honey_jar",
2851
+ "home_preserves"
2852
+ ],
2853
+ rule: {
2854
+ hollow: true,
2855
+ welded: true,
2856
+ skinTags: [
2857
+ "paper",
2858
+ "food"
2859
+ ]
2860
+ }
2861
+ },
2862
+ drink_carton: {
2863
+ models: [
2864
+ "juice_box",
2865
+ "milk_carton"
2866
+ ],
2867
+ rule: {
2868
+ hollow: true,
2869
+ welded: true
2870
+ }
2815
2871
  }
2816
2872
  },
2817
2873
  contents: {
@@ -2827,6 +2883,67 @@ var GameItemsThree = (() => {
2827
2883
  liquid: "soda",
2828
2884
  fill: 0.95
2829
2885
  },
2886
+ water_bottle: {
2887
+ liquid: "water",
2888
+ fill: 0.95
2889
+ },
2890
+ water_jug_5l: {
2891
+ liquid: "water",
2892
+ fill: 0.95,
2893
+ box: true
2894
+ },
2895
+ dirty_water: {
2896
+ liquid: "water",
2897
+ fill: 0.9
2898
+ },
2899
+ sports_drink: {
2900
+ liquid: "juice",
2901
+ fill: 0.95
2902
+ },
2903
+ coconut_water: {
2904
+ liquid: "water",
2905
+ fill: 0.9,
2906
+ box: true
2907
+ },
2908
+ juice_box: {
2909
+ liquid: "juice",
2910
+ fill: 0.95,
2911
+ box: true
2912
+ },
2913
+ milk_carton: {
2914
+ liquid: "milk",
2915
+ fill: 0.95,
2916
+ box: true
2917
+ },
2918
+ cachaca: {
2919
+ liquid: "spirits",
2920
+ fill: 0.9
2921
+ },
2922
+ communion_wine: {
2923
+ liquid: "wine",
2924
+ fill: 0.9
2925
+ },
2926
+ cooking_oil: {
2927
+ liquid: "cooking_oil",
2928
+ fill: 0.9
2929
+ },
2930
+ alcohol_70: {
2931
+ liquid: "spirits",
2932
+ fill: 0.9
2933
+ },
2934
+ honey_jar: {
2935
+ liquid: "honey",
2936
+ fill: 0.85
2937
+ },
2938
+ home_preserves: {
2939
+ liquid: "water",
2940
+ fill: 0.5
2941
+ },
2942
+ fuel_jerrycan: {
2943
+ liquid: "fuel",
2944
+ fill: 0.85,
2945
+ box: true
2946
+ },
2830
2947
  canned_beans: {
2831
2948
  liquid: "juice",
2832
2949
  fill: 0.4
@@ -2848,6 +2965,12 @@ var GameItemsThree = (() => {
2848
2965
  liquid: "honey",
2849
2966
  fill: 0.9
2850
2967
  }
2968
+ },
2969
+ shapes: {
2970
+ box: {
2971
+ hollow: true,
2972
+ welded: true
2973
+ }
2851
2974
  }
2852
2975
  };
2853
2976
 
@@ -2863,6 +2986,8 @@ var GameItemsThree = (() => {
2863
2986
  };
2864
2987
  var FAMILIES = destruction_default.families;
2865
2988
  var CONTENTS = destruction_default.contents;
2989
+ var SHAPES2 = destruction_default.shapes;
2990
+ var shapeDestruction = (shapeId) => SHAPES2[shapeId];
2866
2991
  var FAMILY_OF_MODEL = new Map(Object.entries(FAMILIES).flatMap(([family, entry2]) => entry2.models.map((model) => [model, family])));
2867
2992
  var itemFamily = (modelId) => FAMILY_OF_MODEL.get(modelId);
2868
2993
  var itemDestruction = (modelId) => {
@@ -2978,9 +3103,9 @@ var GameItemsThree = (() => {
2978
3103
  root.userData.tessellationTriangles = added;
2979
3104
  }
2980
3105
  function tagForDestruction(root, options = {}) {
2981
- const { modelId, weight, size, three } = options;
3106
+ const { modelId, shapeId, weight, size, powder, three } = options;
2982
3107
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
2983
- const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
3108
+ const rule = modelId !== void 0 ? itemDestruction(modelId) : shapeId !== void 0 ? shapeDestruction(shapeId) : void 0;
2984
3109
  if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
2985
3110
  const solid = [];
2986
3111
  root.traverse((object2) => {
@@ -2995,6 +3120,7 @@ var GameItemsThree = (() => {
2995
3120
  return;
2996
3121
  }
2997
3122
  if (rule?.hollow) mesh2.userData.hollow = true;
3123
+ if (powder) mesh2.userData.destructionPowder = { color: powder.color };
2998
3124
  solid.push(mesh2);
2999
3125
  });
3000
3126
  if (!rule?.welded || solid.length < 2) return;
@@ -5477,8 +5603,168 @@ var GameItemsThree = (() => {
5477
5603
  return G(mesh2(cyl(0.32, 0.32, 0.52), m), mesh2(cyl(0.326, 0.326, 0.34, 14), food(10172964)), mesh2(cyl(0.33, 0.33, 0.06), paper2(14861162), [0, 0.02, 0]), mesh2(torus2(0.31, 0.025), m, [0, 0.26, 0], [Math.PI / 2, 0, 0]), mesh2(torus2(0.31, 0.025), m, [0, -0.26, 0], [Math.PI / 2, 0, 0]));
5478
5604
  };
5479
5605
 
5606
+ // src/data/opening.json
5607
+ var opening_default = {
5608
+ families: {
5609
+ drink_can: { style: "pull_tab" },
5610
+ food_tin: { style: "cut_lid" }
5611
+ },
5612
+ models: {
5613
+ empty_can: { style: "none" },
5614
+ canned_beans: { food: "#7a2e1c" },
5615
+ canned_corn_and_peas: { food: "#c7b03c" },
5616
+ canned_sausages: { food: "#c98d5c" },
5617
+ canned_sardines: { style: "peel_lid", food: "#8e979c" },
5618
+ condensed_milk: { food: "#e3cf9c" },
5619
+ energy_drink: { style: "pull_tab" },
5620
+ honey_jar: { style: "screw_cap", food: "#d99a1c" },
5621
+ home_preserves: { style: "screw_cap", food: "#8e1f3a" },
5622
+ cooking_oil: { style: "screw_cap", food: "#e3b830" },
5623
+ water_bottle: { style: "screw_cap" },
5624
+ water_jug_5l: { style: "screw_cap" },
5625
+ dirty_water: { style: "screw_cap" },
5626
+ sports_drink: { style: "screw_cap" },
5627
+ coconut_water: { style: "screw_cap" },
5628
+ cachaca: { style: "screw_cap" },
5629
+ communion_wine: { style: "screw_cap" }
5630
+ }
5631
+ };
5632
+
5633
+ // src/opening.ts
5634
+ var FAMILIES2 = opening_default.families;
5635
+ var MODELS = opening_default.models;
5636
+ var itemOpening = (modelId) => {
5637
+ const family = itemFamily(modelId);
5638
+ const row = { ...family === void 0 ? {} : FAMILIES2[family], ...MODELS[modelId] };
5639
+ if (row.style === void 0 || row.style === "none") return void 0;
5640
+ return { style: row.style, ...row.food === void 0 ? {} : { food: row.food } };
5641
+ };
5642
+ var OPENING_NAME = "opening";
5643
+ var INSIDE = 1315086;
5644
+ var CUT_EDGE = 14278114;
5645
+ var LID = 12172996;
5646
+ var isLid = (o) => o.userData.itemLid === true;
5647
+ var asLid = (mesh2) => {
5648
+ mesh2.userData.itemLid = true;
5649
+ return mesh2;
5650
+ };
5651
+ var localBox = (T2, root, only) => {
5652
+ root.updateMatrixWorld(true);
5653
+ const toRoot = new T2.Matrix4().copy(root.matrixWorld).invert();
5654
+ const box4 = new T2.Box3();
5655
+ const part = new T2.Box3();
5656
+ const m = new T2.Matrix4();
5657
+ root.traverse((o) => {
5658
+ const mesh2 = o;
5659
+ if (!mesh2.isMesh || mesh2.userData.destructionDebris || mesh2.name === OPENING_NAME || mesh2.parent?.name === OPENING_NAME) return;
5660
+ if (only && !only(mesh2)) return;
5661
+ if (!mesh2.geometry.boundingBox) mesh2.geometry.computeBoundingBox();
5662
+ part.copy(mesh2.geometry.boundingBox).applyMatrix4(m.multiplyMatrices(toRoot, mesh2.matrixWorld));
5663
+ box4.union(part);
5664
+ });
5665
+ return box4;
5666
+ };
5667
+ function openItem(T2, root, modelId) {
5668
+ const look = itemOpening(modelId);
5669
+ if (!look) return false;
5670
+ if (root.userData.itemOpened) return true;
5671
+ const box4 = look.style === "screw_cap" ? localBox(T2, root, isLid) : localBox(T2, root);
5672
+ if (box4.isEmpty()) return false;
5673
+ const size = box4.getSize(new T2.Vector3());
5674
+ const centre = box4.getCenter(new T2.Vector3());
5675
+ const top = look.style === "screw_cap" ? box4.min.y : box4.max.y;
5676
+ const lift = size.y * 3e-3;
5677
+ const sheet = Math.max(size.y * 6e-3, 1e-4);
5678
+ const group = new T2.Group();
5679
+ group.name = OPENING_NAME;
5680
+ group.position.set(centre.x, top, centre.z);
5681
+ const material = (color2, metal = 0) => new T2.MeshStandardMaterial({ color: color2, metalness: metal, roughness: metal ? 0.45 : 0.85, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 });
5682
+ const add = (geometry, mat, y, parent = group) => {
5683
+ const mesh2 = new T2.Mesh(geometry, mat);
5684
+ mesh2.position.y = y;
5685
+ mesh2.renderOrder = 2;
5686
+ mesh2.userData.destructionDebris = true;
5687
+ mesh2.userData.itemOpening = true;
5688
+ parent.add(mesh2);
5689
+ return mesh2;
5690
+ };
5691
+ const flat = (geometry) => geometry.rotateX(-Math.PI / 2);
5692
+ const food = look.food ?? INSIDE;
5693
+ if (look.style === "screw_cap") {
5694
+ const r = Math.min(size.x, size.z) / 2;
5695
+ root.traverse((o) => isLid(o) && (o.visible = false));
5696
+ add(flat(new T2.CircleGeometry(r * 0.85, 24)), material(INSIDE), lift);
5697
+ if (look.food !== void 0) add(flat(new T2.CircleGeometry(r * 0.7, 24)), material(look.food), lift * 2).userData.itemFood = true;
5698
+ } else if (look.style === "pull_tab") {
5699
+ const r = Math.min(size.x, size.z) / 2;
5700
+ const mouth = add(flat(new T2.CircleGeometry(r * 0.2, 24)), material(INSIDE), lift);
5701
+ mouth.scale.set(1.15, 1, 0.85);
5702
+ mouth.position.z = r * 0.52;
5703
+ } else if (look.style === "cut_lid") {
5704
+ const r = Math.min(size.x, size.z) / 2;
5705
+ add(flat(new T2.CircleGeometry(r * 0.88, 40)), material(INSIDE), lift);
5706
+ add(flat(new T2.CircleGeometry(r * 0.76, 40)), material(food), lift * 2).userData.itemFood = true;
5707
+ add(flat(new T2.RingGeometry(r * 0.86, r * 0.9, 40)), material(CUT_EDGE, 0.9), lift * 3);
5708
+ const hinge = new T2.Group();
5709
+ hinge.position.set(0, lift, -r * 0.86);
5710
+ hinge.rotation.x = -1.95;
5711
+ group.add(hinge);
5712
+ const lid = add(new T2.CylinderGeometry(r * 0.86, r * 0.86, sheet, 40), material(LID, 0.4), 0, hinge);
5713
+ lid.position.z = r * 0.86;
5714
+ } else {
5715
+ const long = size.x >= size.z ? "x" : "z";
5716
+ const length = Math.max(size.x, size.z);
5717
+ const width = Math.min(size.x, size.z);
5718
+ const plane2 = (a, b) => flat(long === "x" ? new T2.PlaneGeometry(a, b) : new T2.PlaneGeometry(b, a));
5719
+ add(plane2(length * 0.92, width * 0.86), material(INSIDE), lift);
5720
+ add(plane2(length * 0.84, width * 0.76), material(food), lift * 2).userData.itemFood = true;
5721
+ const hinge = new T2.Group();
5722
+ const end = length * 0.46;
5723
+ if (long === "x") {
5724
+ hinge.position.set(end, lift, 0);
5725
+ hinge.rotation.z = -1.7;
5726
+ } else {
5727
+ hinge.position.set(0, lift, end);
5728
+ hinge.rotation.x = 1.7;
5729
+ }
5730
+ group.add(hinge);
5731
+ const lid = add(long === "x" ? new T2.BoxGeometry(length * 0.9, sheet, width * 0.84) : new T2.BoxGeometry(width * 0.84, sheet, length * 0.9), material(LID, 0.4), 0, hinge);
5732
+ if (long === "x") lid.position.x = -length * 0.45;
5733
+ else lid.position.z = -length * 0.45;
5734
+ }
5735
+ root.add(group);
5736
+ root.userData.itemOpened = true;
5737
+ return true;
5738
+ }
5739
+ var CAP_OFF = 1.6;
5740
+ function turnItem(root, modelId, state) {
5741
+ if (itemOpening(modelId)?.style !== "screw_cap") return false;
5742
+ root.traverse((o) => {
5743
+ var _a;
5744
+ if (!isLid(o)) return;
5745
+ const mesh2 = o;
5746
+ const base = (_a = mesh2.userData).lidBase ?? (_a.lidBase = { y: mesh2.position.y, turn: mesh2.rotation.y });
5747
+ if (!mesh2.geometry.boundingBox) mesh2.geometry.computeBoundingBox();
5748
+ const height = (mesh2.geometry.boundingBox.max.y - mesh2.geometry.boundingBox.min.y) * mesh2.scale.y;
5749
+ mesh2.rotation.y = base.turn + state.turned;
5750
+ mesh2.position.y = base.y + Math.min(1, Math.max(0, state.lift)) * height * CAP_OFF;
5751
+ });
5752
+ return true;
5753
+ }
5754
+ function setOpeningContents(root, share) {
5755
+ const opening = root.getObjectByName(OPENING_NAME);
5756
+ if (!opening) return;
5757
+ const left = Math.max(0, Math.min(1, share));
5758
+ opening.traverse((o) => {
5759
+ if (!o.userData.itemFood) return;
5760
+ o.visible = left > 0.02;
5761
+ const k = 0.35 + 0.65 * left;
5762
+ o.scale.set(k, 1, k);
5763
+ });
5764
+ }
5765
+
5480
5766
  // src/models/water-bottle.ts
5481
- var waterBottle = ({ mesh: mesh2, cyl, lathe: lathe2, G, plastic, paper: paper2, glass }) => G(mesh2(lathe2([[0, -0.6], [0.2, -0.6], [0.22, -0.5], [0.22, 0.2], [0.19, 0.32], [0.09, 0.45], [0.08, 0.55], [0, 0.55]]), glass(9421788, 0.55)), mesh2(cyl(0.085, 0.085, 0.1, 12), plastic(2907530), [0, 0.6, 0]), mesh2(cyl(0.225, 0.225, 0.22, 16), paper2(15330543), [0, -0.15, 0]), mesh2(cyl(0.226, 0.226, 0.05, 16), plastic(3831976), [0, -0.12, 0]));
5767
+ var waterBottle = ({ mesh: mesh2, cyl, lathe: lathe2, G, plastic, paper: paper2, glass }) => G(mesh2(lathe2([[0, -0.6], [0.2, -0.6], [0.22, -0.5], [0.22, 0.2], [0.19, 0.32], [0.09, 0.45], [0.08, 0.55], [0, 0.55]]), glass(9421788, 0.55)), asLid(mesh2(cyl(0.085, 0.085, 0.1, 12), plastic(2907530), [0, 0.6, 0])), mesh2(cyl(0.225, 0.225, 0.22, 16), paper2(15330543), [0, -0.15, 0]), mesh2(cyl(0.226, 0.226, 0.05, 16), plastic(3831976), [0, -0.12, 0]));
5482
5768
 
5483
5769
  // src/models/rice.ts
5484
5770
  var rice = ({ mesh: mesh2, bx, G, paper: paper2, food }) => G(mesh2(bx(0.7, 0.95, 0.28), paper2(15129792)), mesh2(bx(0.72, 0.12, 0.1), paper2(14010792), [0, 0.52, 0]), mesh2(bx(0.5, 0.3, 0.02), food(11883039), [0, 0.05, 0.15]), mesh2(bx(0.3, 0.1, 0.02), paper2(4160069), [0, -0.25, 0.15]));
@@ -5487,7 +5773,7 @@ var GameItemsThree = (() => {
5487
5773
  var militaryRation = ({ mesh: mesh2, bx, G, plastic, paper: paper2 }) => G(mesh2(bx(0.75, 1, 0.22), plastic(5923142)), mesh2(bx(0.77, 0.1, 0.23), plastic(4672823), [0, 0.48, 0]), mesh2(bx(0.5, 0.14, 0.01), paper2(2237727), [0, 0.18, 0.115]), mesh2(bx(0.36, 0.06, 0.01), paper2(2237727), [0, -0.02, 0.115]));
5488
5774
 
5489
5775
  // src/models/cachaca.ts
5490
- var cachaca = ({ mesh: mesh2, cyl, lathe: lathe2, G, wood, paper: paper2, food, glass }) => G(mesh2(lathe2([[0, -0.6], [0.25, -0.6], [0.27, -0.52], [0.27, 0.1], [0.2, 0.25], [0.09, 0.35], [0.09, 0.58], [0, 0.58]]), glass(12749354, 0.78)), mesh2(cyl(0.08, 0.07, 0.14, 10), wood(9071172), [0, 0.64, 0]), mesh2(cyl(0.275, 0.275, 0.34, 16), paper2(15327692), [0, -0.25, 0]), mesh2(cyl(0.276, 0.276, 0.06, 16), food(8007455), [0, -0.15, 0]));
5776
+ var cachaca = ({ mesh: mesh2, cyl, lathe: lathe2, G, wood, paper: paper2, food, glass }) => G(mesh2(lathe2([[0, -0.6], [0.25, -0.6], [0.27, -0.52], [0.27, 0.1], [0.2, 0.25], [0.09, 0.35], [0.09, 0.58], [0, 0.58]]), glass(12749354, 0.78)), asLid(mesh2(cyl(0.08, 0.07, 0.14, 10), wood(9071172), [0, 0.64, 0])), mesh2(cyl(0.275, 0.275, 0.34, 16), paper2(15327692), [0, -0.25, 0]), mesh2(cyl(0.276, 0.276, 0.06, 16), food(8007455), [0, -0.15, 0]));
5491
5777
 
5492
5778
  // src/models/first-aid-kit.ts
5493
5779
  var firstAidKit = ({ mesh: mesh2, bx, torus: torus2, G, plastic }) => {
@@ -5703,7 +5989,7 @@ var GameItemsThree = (() => {
5703
5989
  mesh2(cyl(0.41, 0.41, 0.86, 12), food(9314106), [0, -0.2, 0]),
5704
5990
  mesh2(cyl(0.34, 0.445, 0.12, 12), glass(13625070, 0.5), [0, 0.39, 0]),
5705
5991
  mesh2(cyl(0.34, 0.34, 0.05, 12), glass(13625070, 0.5), [0, 0.475, 0]),
5706
- mesh2(cyl(0.38, 0.38, 0.15, 12), metal(12163644), [0, 0.575, 0]),
5992
+ asLid(mesh2(cyl(0.38, 0.38, 0.15, 12), metal(12163644), [0, 0.575, 0])),
5707
5993
  mesh2(cyl(0.45, 0.45, 0.4, 12), paper2(15327172), [0, -0.2, 0]),
5708
5994
  mesh2(cyl(0.452, 0.452, 0.08, 12), cloth2(10172986), [0, -0.2, 0])
5709
5995
  );
@@ -5908,7 +6194,7 @@ var GameItemsThree = (() => {
5908
6194
  mesh2(cyl(0.395, 0.395, 0.68, 12), glass(14925936, 0.55), [0, -0.16, 0]),
5909
6195
  mesh2(cyl(0.37, 0.37, 0.6, 12), food(14260764), [0, -0.2, 0]),
5910
6196
  mesh2(cyl(0.3, 0.395, 0.14, 12), glass(14925936, 0.55), [0, 0.25, 0]),
5911
- mesh2(cyl(0.3, 0.3, 0.18, 12), metal(7031324), [0, 0.41, 0]),
6197
+ asLid(mesh2(cyl(0.3, 0.3, 0.18, 12), metal(7031324), [0, 0.41, 0])),
5912
6198
  mesh2(cyl(0.4, 0.4, 0.34, 12), paper2(15853512), [0, -0.2, 0]),
5913
6199
  mesh2(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.1, 0]),
5914
6200
  mesh2(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.3, 0])
@@ -6012,7 +6298,7 @@ var GameItemsThree = (() => {
6012
6298
  mesh2(cyl(0.345, 0.345, 1.7, 12), glass(14923824, 0.65), [0, -0.45, 0]),
6013
6299
  mesh2(cyl(0.12, 0.345, 0.5, 12), glass(14923824, 0.65), [0, 0.65, 0]),
6014
6300
  mesh2(cyl(0.12, 0.12, 0.25, 12), glass(14923824, 0.65), [0, 1.025, 0]),
6015
- mesh2(cyl(0.14, 0.14, 0.15, 12), plastic(4160042), [0, 1.225, 0]),
6301
+ asLid(mesh2(cyl(0.14, 0.14, 0.15, 12), plastic(4160042), [0, 1.225, 0])),
6016
6302
  mesh2(cyl(0.35, 0.35, 0.8, 12), paper2(15853512), [0, -0.5, 0]),
6017
6303
  mesh2(cyl(0.352, 0.352, 0.2, 12), paper2(4160042), [0, -0.5, 0])
6018
6304
  );
@@ -6050,7 +6336,7 @@ var GameItemsThree = (() => {
6050
6336
  const body = mesh2(bx(1.5, 2.3, 1.5), glass(10473710, 0.75), [0, -0.5, 0]);
6051
6337
  const shoulder = mesh2(bx(1.2, 0.45, 1.2), glass(10473710, 0.75), [0, 0.875, 0]);
6052
6338
  const neck = mesh2(cyl(0.34, 0.34, 0.3, 12), dark, [0, 1.25, 0]);
6053
- const cap = mesh2(cyl(0.4, 0.4, 0.25, 12), dark, [0, 1.525, 0]);
6339
+ const cap = asLid(mesh2(cyl(0.4, 0.4, 0.25, 12), dark, [0, 1.525, 0]));
6054
6340
  const grip = mesh2(bx(0.16, 0.9, 0.3), dark, [1.07, 0.6, 0]);
6055
6341
  const upperBar = mesh2(bx(0.45, 0.16, 0.3), dark, [0.925, 0.97, 0]);
6056
6342
  const lowerBar = mesh2(bx(0.45, 0.16, 0.3), dark, [0.925, 0.23, 0]);
@@ -6065,7 +6351,7 @@ var GameItemsThree = (() => {
6065
6351
  const label = mesh2(cyl(0.332, 0.332, 0.7, 12), paper2(1448738), [0, -0.12, 0]);
6066
6352
  const stripe = mesh2(cyl(0.332, 0.332, 0.1, 12), paper2(15753232), [0, 0.28, 0]);
6067
6353
  const collar = mesh2(cyl(0.2, 0.2, 0.04, 12), plastic(15263976), [0, 0.705, 0]);
6068
- const cap = mesh2(cyl(0.19, 0.19, 0.2, 12), plastic(15753232), [0, 0.815, 0]);
6354
+ const cap = asLid(mesh2(cyl(0.19, 0.19, 0.2, 12), plastic(15753232), [0, 0.815, 0]));
6069
6355
  return G(bottle2, label, stripe, collar, cap);
6070
6356
  };
6071
6357
 
@@ -6112,7 +6398,7 @@ var GameItemsThree = (() => {
6112
6398
  const goldBand = mesh2(cyl(0.382, 0.382, 0.05, 12), metal(11896880), [0, 0.075, 0]);
6113
6399
  const crossUpright = mesh2(bx(0.06, 0.26, 0.02), paper2(8003371), [0, -0.35, 0.375]);
6114
6400
  const crossArm = mesh2(bx(0.18, 0.06, 0.02), paper2(8003371), [0, -0.3, 0.375]);
6115
- const foil = mesh2(cyl(0.15, 0.15, 0.46, 12), metal(11896880), [0, 1.27, 0]);
6401
+ const foil = asLid(mesh2(cyl(0.15, 0.15, 0.46, 12), metal(11896880), [0, 1.27, 0]));
6116
6402
  return G(bottle2, label, goldBand, crossUpright, crossArm, foil);
6117
6403
  };
6118
6404
 
@@ -6123,7 +6409,7 @@ var GameItemsThree = (() => {
6123
6409
  const nut = mesh2(cyl(0.11, 0.11, 0.03, 10), paper2(7031339), [0, -0.12, 0.285], [Math.PI / 2, 0, 0]);
6124
6410
  const leafLeft = mesh2(bx(0.2, 0.05, 0.01), paper2(15135448), [-0.12, 0.08, 0.292], [0, 0, 0.5]);
6125
6411
  const leafRight = mesh2(bx(0.2, 0.05, 0.01), paper2(15135448), [0.12, 0.08, 0.292], [0, 0, -0.5]);
6126
- const cap = mesh2(cyl(0.14, 0.14, 0.2, 12), plastic(4165450), [0, 0.75, 0]);
6412
+ const cap = asLid(mesh2(cyl(0.14, 0.14, 0.2, 12), plastic(4165450), [0, 0.75, 0]));
6127
6413
  return G(bottle2, label, nut, leafLeft, leafRight, cap);
6128
6414
  };
6129
6415
 
@@ -6133,7 +6419,7 @@ var GameItemsThree = (() => {
6133
6419
  const murk = mesh2(lathe2([[0, -0.89], [0.27, -0.89], [0.315, -0.84], [0.315, 0.1], [0, 0.1]], 12), food(7035444));
6134
6420
  const sediment = mesh2(cyl(0.31, 0.27, 0.12, 8), food(4865314), [0, -0.82, 0]);
6135
6421
  const label = mesh2(cyl(0.332, 0.332, 0.22, 12), paper2(13617588), [0, 0.38, 0]);
6136
- const cap = mesh2(cyl(0.19, 0.19, 0.2, 8), plastic(5922920), [0, 0.815, 0]);
6422
+ const cap = asLid(mesh2(cyl(0.19, 0.19, 0.2, 8), plastic(5922920), [0, 0.815, 0]));
6137
6423
  return G(bottle2, murk, sediment, label, cap);
6138
6424
  };
6139
6425
 
@@ -9122,7 +9408,7 @@ var GameItemsThree = (() => {
9122
9408
  const materials = kit.materials.slice(first);
9123
9409
  if (options.wear) setWear(materials, options.wear);
9124
9410
  root.userData.wear = options.wear ?? 0;
9125
- if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size, three: T2 });
9411
+ if (options.destructible) tagForDestruction(root, { ...kind === "model" && item.model !== void 0 ? { modelId: item.model } : {}, ...shape === void 0 ? {} : { shapeId: shape }, ...item.weight === void 0 ? {} : { weight: item.weight }, size: item.size, ...item.powder ? { powder: item.powder } : {}, three: T2 });
9126
9412
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
9127
9413
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
9128
9414
  }
@@ -15037,6 +15323,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15037
15323
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
15038
15324
  var VENT_DIAMETER = 8e-3;
15039
15325
  var FLAME_LIGHTS_LEAK_C = 470;
15326
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
15040
15327
  var BREACH_RADIUS = 0.55;
15041
15328
  var BREACH_BLAST_FACTOR = 6;
15042
15329
  var BREACH_AREA = 0.25;
@@ -15052,6 +15339,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15052
15339
  var BREACH_GRID_CAP = 4096;
15053
15340
  var CHIP_ENERGY = 200;
15054
15341
  var CHIP_SPLASH = 0.5;
15342
+ var CRATER_SPENT = 0.6;
15055
15343
  var BAR_BOW = 0.08;
15056
15344
  var BULLET_JOINT_SPAN = 0.3;
15057
15345
  var CUT_SLICE = 1;
@@ -15072,12 +15360,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15072
15360
  const cells = p.st.chipped;
15073
15361
  if (!cells?.length) return 0;
15074
15362
  const [u, v] = planeAxes(p.thin);
15075
- const along = p.dims[u] >= p.dims[v] ? u : v;
15363
+ const role = p.spec.structure?.role;
15364
+ const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
15365
+ const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
15366
+ const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
15076
15367
  const across2 = along === u ? v : u;
15077
15368
  let worst = 0;
15078
15369
  for (const c of cells) {
15079
- let gone = 0;
15080
- for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) gone += d.size[across2];
15370
+ const spans = [];
15371
+ for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across2] - d.size[across2] / 2, d.center[across2] + d.size[across2] / 2]);
15372
+ spans.sort((a, b) => a[0] - b[0]);
15373
+ let gone = 0, end = -Infinity;
15374
+ for (const [a, b] of spans) {
15375
+ if (b <= end) continue;
15376
+ gone += b - Math.max(a, end);
15377
+ end = b;
15378
+ }
15081
15379
  worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
15082
15380
  }
15083
15381
  return Math.min(1, worst);
@@ -15373,7 +15671,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15373
15671
  if (k < 0 || j.spec.kind !== "tie") return k;
15374
15672
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
15375
15673
  };
15674
+ let poseVersion = 0;
15675
+ const contactMemo = /* @__PURE__ */ new WeakMap();
15376
15676
  const contactBox = (p, support) => {
15677
+ let byTouching = contactMemo.get(p);
15678
+ if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
15679
+ const known = byTouching.get(support);
15680
+ if (known && known.version === poseVersion) return known.box;
15681
+ const box4 = measureContact(p, support);
15682
+ byTouching.set(support, { version: poseVersion, box: box4 });
15683
+ return box4;
15684
+ };
15685
+ const measureContact = (p, support) => {
15377
15686
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
15378
15687
  const lo = [0, 0, 0], hi = [0, 0, 0];
15379
15688
  for (let k = 0; k < 3; k++) {
@@ -16168,7 +16477,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16168
16477
  p.st.holes.push(hole);
16169
16478
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
16170
16479
  markDamaged(p);
16171
- emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
16480
+ emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
16172
16481
  setStage(o, p, 1, {}, src);
16173
16482
  return hole;
16174
16483
  };
@@ -16185,6 +16494,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16185
16494
  const detachEvents = (o, opened, by, src) => {
16186
16495
  for (const j of opened) emit({ ...base(o, src), type: "detach", joint: j.spec.id, parts: [j.spec.a, j.spec.b], by, groups: o.st.groups.map((g) => [...g]) });
16187
16496
  };
16497
+ const slideOffResting = (o, rotation) => {
16498
+ const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
16499
+ if (upY >= RESTING_HOLD_COS) return;
16500
+ const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
16501
+ if (opened.length === 0) return;
16502
+ run(() => {
16503
+ for (const j of opened) {
16504
+ j.st.open = true;
16505
+ j.st.openedBy = "break";
16506
+ }
16507
+ solveStructure(o);
16508
+ recomputeGroups(o);
16509
+ detachEvents(o, opened, "break");
16510
+ collapseCheck(o);
16511
+ });
16512
+ };
16188
16513
  const checkDestroyed = (o, src) => {
16189
16514
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
16190
16515
  o.st.destroyed = true;
@@ -16285,7 +16610,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16285
16610
  const localHolesOf = (p) => p.st.holes.map((h) => ({ point: toLocal(p.pos, p.q, h.point), normal: rotateVec(quat.conjugate(p.q), h.normal), diameter: h.diameter }));
16286
16611
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
16287
16612
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
16288
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
16613
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
16289
16614
  const { c, q } = partXf(o, p);
16290
16615
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
16291
16616
  const [u, v] = planeAxes(p.thin);
@@ -16309,7 +16634,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16309
16634
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
16310
16635
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
16311
16636
  mass: mass * faceArea(cell) / total,
16312
- ...marks.length > 0 ? { marks } : {}
16637
+ ...marks.length > 0 ? { marks } : {},
16638
+ ...inner ? { inner } : {}
16313
16639
  };
16314
16640
  });
16315
16641
  };
@@ -16350,7 +16676,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16350
16676
  return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
16351
16677
  };
16352
16678
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
16353
- var _a, _b;
16679
+ var _a, _b, _c;
16354
16680
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16355
16681
  const rule = p.mat.hits?.[kind];
16356
16682
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -16360,7 +16686,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16360
16686
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16361
16687
  const grid = gridOf(p);
16362
16688
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
16689
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
16363
16690
  const out = [];
16691
+ const shot = kind === "ballistic";
16364
16692
  for (let i = 0; i < grid.length; i++) {
16365
16693
  const cell = grid[i];
16366
16694
  if (worn[i] === -1) continue;
@@ -16373,18 +16701,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16373
16701
  worn[i] = total;
16374
16702
  continue;
16375
16703
  }
16376
- worn[i] = -1;
16377
- out.push(cell);
16704
+ if (!shot) {
16705
+ worn[i] = -1;
16706
+ out.push(cell);
16707
+ continue;
16708
+ }
16709
+ const size = [cell.size[0], cell.size[1], cell.size[2]];
16710
+ const center = [cell.center[0], cell.center[1], cell.center[2]];
16711
+ for (const a of [u, v]) {
16712
+ const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
16713
+ const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
16714
+ size[a] = w;
16715
+ center[a] = clamp2(lp[a], lo, hi);
16716
+ }
16717
+ const taken2 = (cut[i] ?? 0) + size[u] * size[v];
16718
+ if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
16719
+ worn[i] = -1;
16720
+ out.push({ ...cell, crater: true });
16721
+ continue;
16722
+ }
16723
+ worn[i] = 0;
16724
+ cut[i] = taken2;
16725
+ out.push({ center, size, crater: true });
16378
16726
  }
16379
16727
  if (out.length === 0) return;
16380
16728
  const { c, q } = partXf(o, p);
16381
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
16729
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
16382
16730
  const holes = localHolesOf(p);
16383
16731
  for (const cell of out) {
16384
16732
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
16385
16733
  const center = toWorld(c, q, cell.center);
16386
16734
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
16387
- const fragments = cellFragments(o, p, [cell], wp, dir, speed, mass, `c${chipped.length}_`);
16735
+ const tag = `c${chipped.length}_`;
16736
+ const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
16388
16737
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
16389
16738
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
16390
16739
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -16401,6 +16750,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16401
16750
  collapseCheck(o, src);
16402
16751
  }
16403
16752
  };
16753
+ const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
16754
+ const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
16755
+ const { c, q } = partXf(o, p);
16756
+ const [u, v] = planeAxes(p.thin);
16757
+ const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
16758
+ const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
16759
+ const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
16760
+ const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
16761
+ const out = [];
16762
+ for (let i = 0; i < n; i++) {
16763
+ const back = i % 3 === 2;
16764
+ const lc = [cell.center[0], cell.center[1], cell.center[2]];
16765
+ lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
16766
+ lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
16767
+ lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
16768
+ const size = [0, 0, 0];
16769
+ size[u] = bit * (0.6 + 0.8 * rnd());
16770
+ size[v] = bit * (0.5 + 0.7 * rnd());
16771
+ size[p.thin] = bit * (0.4 + 0.6 * rnd());
16772
+ const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
16773
+ const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
16774
+ const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
16775
+ out.push({
16776
+ id: `${p.spec.id}#${tag}${i}`,
16777
+ part: p.spec.id,
16778
+ material: p.spec.material,
16779
+ shape: "chunk",
16780
+ center: toWorld(c, q, lc),
16781
+ size,
16782
+ rotation: q,
16783
+ velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
16784
+ angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
16785
+ mass: mass / n,
16786
+ inner: true
16787
+ });
16788
+ }
16789
+ return out;
16790
+ };
16404
16791
  const exposeRebar = (o, p, src) => {
16405
16792
  const r = p.spec.reinforcement;
16406
16793
  if (!r) return;
@@ -16688,7 +17075,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16688
17075
  });
16689
17076
  }
16690
17077
  const patch = [scale * 4, scale * 4, scale * 4];
16691
- emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
17078
+ emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
16692
17079
  };
16693
17080
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16694
17081
  const mat = p.mat;
@@ -16747,12 +17134,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16747
17134
  } else if (mat.ductility >= 0.5) {
16748
17135
  addDent(o, p, entry2, 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);
16749
17136
  } else if (brittle) {
16750
- addCrack(o, p, entry2, n, crackLen, src);
17137
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
16751
17138
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16752
17139
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16753
17140
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16754
17141
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
16755
- const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
17142
+ const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
17143
+ const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
16756
17144
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16757
17145
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
16758
17146
  chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
@@ -16837,10 +17225,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16837
17225
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
16838
17226
  return reach;
16839
17227
  };
17228
+ const blastProbe = { partsTested: 0, partsNear: 0 };
17229
+ const blastCandidates = (o, point, radius) => {
17230
+ const local = toLocal(o.st.position, o.st.rotation, point);
17231
+ const near = [];
17232
+ for (const p of o.parts) {
17233
+ const reach = radius + p.bound;
17234
+ const dx = local[0] - p.pos[0];
17235
+ const dy = local[1] - p.pos[1];
17236
+ const dz = local[2] - p.pos[2];
17237
+ if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
17238
+ }
17239
+ blastProbe.partsTested += o.parts.length;
17240
+ blastProbe.partsNear += near.length;
17241
+ return near;
17242
+ };
16840
17243
  const blastObject = (o, point, energy, radius, src) => {
16841
17244
  const start = cur.length;
16842
17245
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
16843
- for (const p of o.parts) blastPart(o, p, point, energy, radius, src, res);
17246
+ for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
16844
17247
  res.events = cur.slice(start);
16845
17248
  return res;
16846
17249
  };
@@ -16964,7 +17367,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16964
17367
  let order = 0;
16965
17368
  for (const o of inRange) {
16966
17369
  if (only && only !== o) continue;
16967
- for (const p of o.parts) {
17370
+ for (const p of blastCandidates(o, point, radius)) {
16968
17371
  if (isLost(p)) continue;
16969
17372
  const dist = distToPart(o, p, point);
16970
17373
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -17511,6 +17914,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17511
17914
  const p = getPart(o, partId);
17512
17915
  p.pos = [position[0], position[1], position[2]];
17513
17916
  p.q = q;
17917
+ poseVersion++;
17514
17918
  updateBound(o);
17515
17919
  },
17516
17920
  remove(id) {
@@ -17529,6 +17933,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17529
17933
  const o = getObj(id);
17530
17934
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
17531
17935
  applySoilPose(o);
17936
+ slideOffResting(o, rotation);
17532
17937
  },
17533
17938
  splitGroup(id, group, newId) {
17534
17939
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -17552,7 +17957,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17552
17957
  if (o.spec.name !== void 0) s.name = o.spec.name;
17553
17958
  if (o.spec.tags) s.tags = [...o.spec.tags];
17554
17959
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
17555
- return clone(s);
17960
+ return s;
17556
17961
  };
17557
17962
  const inNew = (pid) => moved.has(pid);
17558
17963
  const inOld = (pid) => !moved.has(pid);
@@ -17579,7 +17984,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17579
17984
  const p = getPart(o, partId);
17580
17985
  return whole(() => doHit(o, h, p));
17581
17986
  },
17987
+ blastProbe() {
17988
+ return { ...blastProbe };
17989
+ },
17582
17990
  explode(point, energy, radius, source, blast) {
17991
+ blastProbe.partsTested = 0;
17992
+ blastProbe.partsNear = 0;
17583
17993
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
17584
17994
  },
17585
17995
  raycast(origin, direction, maxDistance = Infinity) {
@@ -17589,7 +17999,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17589
17999
  sweepBox(box4, direction, distance2, options2 = {}) {
17590
18000
  const d = vec.normalize(direction);
17591
18001
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
17592
- const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : void 0;
18002
+ const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
18003
+ const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : hosted ? new Set(hosted.impl.describe().parts.map((p) => `${hosted.impl.id}\0${p.id}`)) : void 0;
17593
18004
  let best = null;
17594
18005
  for (const origin of sweepOrigins(box4, d, options2.spacing ?? 0.2)) {
17595
18006
  const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
@@ -27192,6 +27603,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27192
27603
  function collapseRange(mesh2, v0, v1) {
27193
27604
  const pos = mesh2.geometry.attributes.position;
27194
27605
  const a = pos.array;
27606
+ const u = mesh2.userData;
27607
+ if (!u._p0) u._p0 = a.slice();
27608
+ const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
27609
+ if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
27195
27610
  const saved = a.slice(v0 * 3, v1 * 3);
27196
27611
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
27197
27612
  for (let k = v0; k < v1; k++) {
@@ -27206,6 +27621,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27206
27621
  const pos = mesh2.geometry.attributes.position;
27207
27622
  pos.array.set(saved, v0 * 3);
27208
27623
  pos.needsUpdate = true;
27624
+ const list2 = mesh2.userData.collapsedRanges;
27625
+ if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
27209
27626
  }
27210
27627
  function noise2(x, y, cell) {
27211
27628
  const h = (i2, j2) => {
@@ -27220,13 +27637,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27220
27637
  var SPALL_MIN = 0.025;
27221
27638
  var SPALL_RAGGED = 0.045;
27222
27639
  var COVER2 = 0.015;
27640
+ var MAX_FLAKES = 64;
27223
27641
  function installDestructionBreach(K, _shared) {
27224
27642
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
27225
27643
  function createBreachView(opts) {
27226
27644
  const { house, structure } = opts;
27227
27645
  const views = /* @__PURE__ */ new Map();
27646
+ const dirty = /* @__PURE__ */ new Set();
27647
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
27648
+ const dropped = [];
27228
27649
  let lod = 0;
27229
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
27230
27650
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
27231
27651
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
27232
27652
  const local = (p) => {
@@ -27295,6 +27715,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27295
27715
  const mesh2 = new THREE.Mesh(g, material);
27296
27716
  mesh2.name = "breach-shell";
27297
27717
  mesh2.userData.breachMask = mask;
27718
+ mesh2.userData.cutAt = (p) => {
27719
+ const v = local([p.x, p.y, p.z]);
27720
+ const i = Math.floor(((mask.alongU ? v[0] : v[2]) - mask.start) / mask.len * mask.w), j = Math.floor((v[1] - mask.y0) / mask.height * mask.h);
27721
+ return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
27722
+ };
27298
27723
  mesh2.position.copy(it.mesh.position);
27299
27724
  mesh2.quaternion.copy(it.mesh.quaternion);
27300
27725
  mesh2.scale.copy(it.mesh.scale);
@@ -27310,6 +27735,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27310
27735
  toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27311
27736
  return mesh2;
27312
27737
  }
27738
+ function unitsMaterial(core) {
27739
+ const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
27740
+ material.onBeforeCompile = (sh) => {
27741
+ sh.uniforms.uCore = { value: core.texture };
27742
+ sh.uniforms.uCoreRect = { value: core.rect };
27743
+ sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
27744
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "varying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\n#ifdef USE_INSTANCING\nvHousePos = (instanceMatrix * vec4(transformed, 1.0)).xyz;\n#else\nvHousePos = transformed;\n#endif");
27745
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uCore;\nuniform vec4 uCoreRect;\nuniform float uCoreAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 cUv = vec2(((uCoreAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uCoreRect.x) / uCoreRect.z, (vHousePos.y - uCoreRect.y) / uCoreRect.w);\n if (cUv.x >= 0.0 && cUv.x <= 1.0 && cUv.y >= 0.0 && cUv.y <= 1.0 && texture2D(uCore, cUv).r > 0.5) discard;").replace("#include <color_fragment>", "#include <color_fragment>\nif (!gl_FrontFacing) diffuseColor.rgb *= 0.55;");
27746
+ };
27747
+ material.customProgramCacheKey = () => "breachCore";
27748
+ material.userData.breachCore = core;
27749
+ return material;
27750
+ }
27313
27751
  function maskOf(hostId, parts) {
27314
27752
  const host = structure.byId.get(hostId);
27315
27753
  const [u0, v0, u1, v1] = host.aabb;
@@ -27325,41 +27763,73 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27325
27763
  return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
27326
27764
  }
27327
27765
  const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
27766
+ const ellipseDistance = (s, y, cs, cy, rs, ry) => (Math.hypot((s - cs) / Math.max(rs, 1e-4), (y - cy) / Math.max(ry, 1e-4)) - 1) * Math.min(rs, ry);
27767
+ function covered(c, boxes) {
27768
+ const n = 8;
27769
+ let hit = 0;
27770
+ for (let j = 0; j < n; j++) {
27771
+ const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
27772
+ for (let i = 0; i < n; i++) {
27773
+ const s = c.a + (i + 0.5) / n * (c.b - c.a);
27774
+ if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
27775
+ }
27776
+ }
27777
+ return hit / (n * n);
27778
+ }
27328
27779
  function paint(view) {
27329
- const { mask } = view;
27780
+ const { mask, core } = view;
27330
27781
  const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
27331
27782
  const holes = [];
27783
+ const pits = [];
27332
27784
  const discs = [];
27785
+ const flakes = [];
27333
27786
  const shown = [];
27334
27787
  for (const id of view.parts) {
27335
- const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
27788
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
27336
27789
  const all = cells(part, units2);
27337
27790
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
27338
27791
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
27339
27792
  if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
27340
- const gone = (c) => chips.some((p) => p[1] >= c.y0 && p[1] <= c.y1 && along(p) >= c.a && along(p) <= c.b) || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
27793
+ for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
27794
+ const boxes = chips.map((k) => ({ s0: along(k.center) - k.along, s1: along(k.center) + k.along, y0: k.center[1] - k.up, y1: k.center[1] + k.up }));
27795
+ const gone = (c) => covered(c, boxes.filter((k) => k.s1 > c.a && k.s0 < c.b && k.y1 > c.y0 && k.y0 < c.y1)) >= 0.9 || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
27341
27796
  const kept = [];
27342
27797
  for (const c of all) (gone(c) ? holes : kept).push(c);
27343
27798
  for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
27799
+ for (const k of boxes) {
27800
+ if (holes.some((h) => k.s0 >= h.a - 1e-4 && k.s1 <= h.b + 1e-4 && k.y0 >= h.y0 - 1e-4 && k.y1 <= h.y1 + 1e-4)) continue;
27801
+ pits.push({ s: (k.s0 + k.s1) / 2, y: (k.y0 + k.y1) / 2, rs: (k.s1 - k.s0) / 2, ry: (k.y1 - k.y0) / 2 });
27802
+ }
27344
27803
  }
27345
27804
  const reach = SPALL_MIN + SPALL_RAGGED;
27805
+ const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
27346
27806
  mask.data.fill(0);
27347
- const stamp = (s0, s1, y0, y1, inside2) => {
27348
- const i0 = Math.max(0, Math.floor((s0 - reach) / mask.len * mask.w)), i1 = Math.min(mask.w - 1, Math.ceil((s1 + reach) / mask.len * mask.w));
27349
- const j0 = Math.max(0, Math.floor((y0 - reach - mask.y0) / mask.height * mask.h)), j1 = Math.min(mask.h - 1, Math.ceil((y1 + reach - mask.y0) / mask.height * mask.h));
27807
+ core.data.fill(0);
27808
+ const stamp = (m, s0, s1, y0, y1, inside2) => {
27809
+ const i0 = Math.max(0, Math.floor((s0 - reach) / m.len * m.w)), i1 = Math.min(m.w - 1, Math.ceil((s1 + reach) / m.len * m.w));
27810
+ const j0 = Math.max(0, Math.floor((y0 - reach - m.y0) / m.height * m.h)), j1 = Math.min(m.h - 1, Math.ceil((y1 + reach - m.y0) / m.height * m.h));
27350
27811
  for (let j = j0; j <= j1; j++) {
27351
- const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
27812
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
27352
27813
  for (let i = i0; i <= i1; i++) {
27353
- const s = (i + 0.5) / mask.w * mask.len;
27354
- const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
27355
- if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
27814
+ const s = (i + 0.5) / m.w * m.len;
27815
+ if (inside2(s, y)) m.data[j * m.w + i] = 255;
27356
27816
  }
27357
27817
  }
27358
27818
  };
27359
- for (const c of holes) stamp(c.a, c.b, c.y0, c.y1, (s, y) => boxDistance(s, y, c.a, c.b, c.y0, c.y1));
27360
- for (const d of discs) stamp(d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r));
27819
+ const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
27820
+ for (const c of holes) stamp(mask, c.a, c.b, c.y0, c.y1, (s, y) => plaster(boxDistance(s, y, c.a, c.b, c.y0, c.y1), s, y));
27821
+ for (const d of discs) stamp(mask, d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => plaster(Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r), s, y));
27822
+ for (const f of flakes) {
27823
+ const rough = (s, y) => Math.hypot(s - f.s, y - f.y) - f.r * (0.65 + 0.7 * ragged(s * 2.3, y * 2.3));
27824
+ stamp(mask, f.s - 2 * f.r, f.s + 2 * f.r, f.y - 2 * f.r, f.y + 2 * f.r, (s, y) => rough(s, y) <= 0);
27825
+ }
27826
+ for (const p of pits) {
27827
+ stamp(mask, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => plaster(Math.max(0, ellipseDistance(s, y, p.s, p.y, p.rs, p.ry)), s, y));
27828
+ stamp(core, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => ellipseDistance(s, y, p.s, p.y, p.rs, p.ry) <= -0.35 * Math.min(p.rs, p.ry) * noise2(s * 1.7, y * 1.3, 0.012));
27829
+ }
27361
27830
  mask.texture.needsUpdate = true;
27362
- const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach);
27831
+ core.texture.needsUpdate = true;
27832
+ const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach) || pits.some((p) => boxDistance(p.s, p.y, c.a, c.b, c.y0, c.y1) <= Math.max(p.rs, p.ry) + reach) || flakes.some((f) => boxDistance(f.s, f.y, c.a, c.b, c.y0, c.y1) <= 2 * f.r);
27363
27833
  const list2 = [];
27364
27834
  const tone = (hex3, k) => tmp.c.setHex(hex3).multiplyScalar(k).getHex();
27365
27835
  for (const { c, part, units: units2, thick, mid } of shown) {
@@ -27369,7 +27839,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27369
27839
  const base = fragmentMaterial(part.material).color.getHex();
27370
27840
  const n = noise2(centre * 3.1, y * 2.7, 0.07);
27371
27841
  if (units2.mortar === "concrete") {
27372
- at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
27842
+ at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
27373
27843
  } else {
27374
27844
  at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
27375
27845
  at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
@@ -27379,7 +27849,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27379
27849
  view.units.removeFromParent();
27380
27850
  view.units.dispose();
27381
27851
  }
27382
- const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
27852
+ const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
27383
27853
  units.name = "breach:" + view.host;
27384
27854
  list2.forEach((u, i) => {
27385
27855
  tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
@@ -27395,6 +27865,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27395
27865
  house.add(units);
27396
27866
  view.units = units;
27397
27867
  view.gone = holes.length;
27868
+ view.pits = pits.length;
27869
+ view.flakes = flakes.length;
27870
+ }
27871
+ function chipBox(chip, alongU) {
27872
+ house.updateMatrixWorld(true);
27873
+ const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
27874
+ let a = 0, u = 0;
27875
+ for (let k = 0; k < 3; k++) {
27876
+ const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
27877
+ a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
27878
+ u += Math.abs(axis.y) * chip.size[k] / 2;
27879
+ }
27880
+ return { center: local(chip.center), along: a, up: u };
27398
27881
  }
27399
27882
  function draw2(partId, change) {
27400
27883
  if (lod !== 0) return false;
@@ -27406,23 +27889,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27406
27889
  if (!items.length) return false;
27407
27890
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
27408
27891
  if (!parts.includes(partId)) return false;
27409
- const mask = maskOf(hostId, parts);
27410
- view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
27892
+ const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
27893
+ view = {
27894
+ host: hostId,
27895
+ parts,
27896
+ items,
27897
+ mask,
27898
+ core,
27899
+ unitMaterial: unitsMaterial(core),
27900
+ shells: [],
27901
+ saved: null,
27902
+ units: null,
27903
+ gone: 0,
27904
+ pits: 0,
27905
+ chips: new Map(parts.map((id) => [id, []])),
27906
+ spalls: new Map(parts.map((id) => [id, []])),
27907
+ openings: /* @__PURE__ */ new Map(),
27908
+ flakes: 0
27909
+ };
27411
27910
  }
27412
27911
  if (!view.chips.has(partId)) return false;
27413
27912
  if (change.opening) {
27414
27913
  if (view.openings.has(partId)) return false;
27415
27914
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
27416
27915
  }
27417
- if (change.chip) view.chips.get(partId).push(local(change.chip));
27916
+ if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
27917
+ if (change.spall) {
27918
+ const list2 = view.spalls.get(partId);
27919
+ list2.push({ center: local(change.spall.center), radius: change.spall.radius });
27920
+ if (list2.length > MAX_FLAKES) list2.shift();
27921
+ }
27418
27922
  if (!view.saved) {
27419
27923
  view.shells = view.items.map((it) => shell(it, view.mask));
27924
+ for (const m of view.shells) m.userData.behind = () => view.units;
27420
27925
  view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27421
27926
  }
27422
- paint(view);
27423
27927
  views.set(hostId, view);
27424
- invalidate();
27425
- invalidateShadow();
27928
+ if (opts.deferPaint && change.spall) dirty.add(view);
27929
+ else {
27930
+ dirty.delete(view);
27931
+ paint(view);
27932
+ invalidate();
27933
+ invalidateShadow();
27934
+ }
27426
27935
  return true;
27427
27936
  }
27428
27937
  function plain2(view, shown) {
@@ -27431,24 +27940,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27431
27940
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
27432
27941
  }
27433
27942
  }
27943
+ function flush(budgetMs = paintBudgetMs) {
27944
+ if (dirty.size === 0) return 0;
27945
+ const deadline = performance.now() + budgetMs;
27946
+ for (const view of dirty) {
27947
+ dirty.delete(view);
27948
+ paint(view);
27949
+ if (performance.now() >= deadline) break;
27950
+ }
27951
+ invalidate();
27952
+ invalidateShadow();
27953
+ return dirty.size;
27954
+ }
27434
27955
  function dispose(view) {
27435
- view.units.removeFromParent();
27436
- view.units.dispose();
27956
+ dirty.delete(view);
27957
+ if (view.units) {
27958
+ view.units.removeFromParent();
27959
+ view.units.dispose();
27960
+ }
27961
+ view.unitMaterial.dispose();
27437
27962
  for (const m of view.shells) {
27438
27963
  m.removeFromParent();
27439
27964
  m.geometry.dispose();
27440
27965
  m.material.dispose();
27441
27966
  }
27442
27967
  view.mask.texture.dispose();
27968
+ view.core.texture.dispose();
27443
27969
  }
27444
27970
  return {
27445
27971
  /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
27446
27972
  stage(event) {
27447
27973
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
27448
27974
  },
27449
- /** Handles a `chip` event: the unit that came out leaves a hole in the wall where it was. Returns true when the wall was cut. */
27975
+ /**
27976
+ * Handles a `spall` of a wall of units: the coat comes off in a ragged patch and the units show under it, drawn by the house (the destruction
27977
+ * renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
27978
+ */
27979
+ spall(event) {
27980
+ const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
27981
+ return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
27982
+ },
27983
+ /**
27984
+ * Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
27985
+ * throws the bits and leaves its patch out. Call it once the spec is known.
27986
+ */
27987
+ claim() {
27988
+ var _a;
27989
+ for (const q of opts.spec()?.parts ?? []) {
27990
+ if (!q.units) continue;
27991
+ const host = hostOf(q.id);
27992
+ if (!host) continue;
27993
+ for (const it of structure.meshesOf(host)) {
27994
+ if (!it.mesh) continue;
27995
+ const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
27996
+ if (!list2.includes(q.id)) list2.push(q.id);
27997
+ }
27998
+ }
27999
+ },
28000
+ /**
28001
+ * Handles a `chip` event: what came out (a unit, or a crater a shot broke through one) leaves a hole in the wall where it was; a `torn` chip (what a blow
28002
+ * tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
28003
+ */
27450
28004
  chip(event) {
27451
- return draw2(event.part, { chip: event.center });
28005
+ if (event.torn) return false;
28006
+ return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
27452
28007
  },
27453
28008
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
27454
28009
  drop(partId) {
@@ -27456,9 +28011,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27456
28011
  if (!v) return;
27457
28012
  dispose(v);
27458
28013
  views.delete(partId);
28014
+ if (v.saved) dropped.push(v);
27459
28015
  },
27460
28016
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
27461
28017
  setLod(level) {
28018
+ flush(Infinity);
27462
28019
  const was = lod;
27463
28020
  lod = level;
27464
28021
  if (was === 0 === (level === 0)) return;
@@ -27477,21 +28034,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27477
28034
  if (lod === 0) plain2(v, true);
27478
28035
  }
27479
28036
  views.clear();
28037
+ for (const v of dropped) plain2(v, true);
28038
+ dropped.length = 0;
27480
28039
  },
28040
+ flush,
27481
28041
  /** The wall has damage drawn. */
27482
28042
  has(partId) {
27483
28043
  return views.has(partId);
27484
28044
  },
27485
- /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
27486
- statistics: () => ({
28045
+ /** Walls with damage drawn, units out of them, craters shots broke through units that stay, and units laid bare at the edge of the cuts. */
28046
+ statistics: () => (flush(Infinity), {
27487
28047
  walls: views.size,
27488
28048
  gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
28049
+ craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
28050
+ flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
27489
28051
  units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
27490
28052
  })
27491
28053
  };
27492
28054
  }
27493
28055
  Object.assign(K, { createBreachView });
27494
28056
  }
28057
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
27495
28058
  function installDestructionWorld(K, shared) {
27496
28059
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
27497
28060
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -27500,7 +28063,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27500
28063
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
27501
28064
  D.renderer = R;
27502
28065
  D.spec = null;
27503
- const breach = createBreachView({ house, structure, spec: () => D.spec });
28066
+ const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
27504
28067
  D.breach = breach;
27505
28068
  D.onEvent = (fn) => {
27506
28069
  listeners.add(fn);
@@ -27685,6 +28248,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27685
28248
  case "chip":
27686
28249
  breach.chip(e);
27687
28250
  break;
28251
+ case "spall":
28252
+ breach.spall(e);
28253
+ break;
27688
28254
  case "collapse":
27689
28255
  for (const id of e.parts) {
27690
28256
  breach.drop(id);
@@ -27726,7 +28292,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27726
28292
  D.specOptions = specOptions || null;
27727
28293
  D.spec = structure.spec(specOptions);
27728
28294
  D.id = D.world.add(D.spec, { id: "house3d:" + ++shared.destructionSeq, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
27729
- if (R) R.bind(D.id, house, { parts: rangesOf() });
28295
+ breach.claim();
28296
+ if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
27730
28297
  off = D.world.on("*", handleEvent);
27731
28298
  D.active = true;
27732
28299
  return D;
@@ -27741,6 +28308,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27741
28308
  };
27742
28309
  D.follow = (world, id, spec) => {
27743
28310
  D.spec = spec;
28311
+ breach.claim();
28312
+ let fallback = null;
27744
28313
  const offFollow = world.on("*", (e) => {
27745
28314
  if (e.object !== id) return;
27746
28315
  switch (e.type) {
@@ -27750,6 +28319,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27750
28319
  case "chip":
27751
28320
  breach.chip(e);
27752
28321
  break;
28322
+ case "spall":
28323
+ breach.spall(e);
28324
+ break;
27753
28325
  case "fracture":
27754
28326
  breach.drop(e.part);
27755
28327
  break;
@@ -27759,10 +28331,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27759
28331
  default:
27760
28332
  break;
27761
28333
  }
28334
+ if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
28335
+ fallback = null;
28336
+ breach.flush(Infinity);
28337
+ }, FOLLOW_FLUSH_FALLBACK_MS);
27762
28338
  });
27763
28339
  return { restore: () => breach.restore(), dispose: () => {
27764
28340
  offFollow();
27765
28341
  breach.restore();
28342
+ }, update: () => {
28343
+ breach.flush();
27766
28344
  } };
27767
28345
  };
27768
28346
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -27795,6 +28373,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27795
28373
  return r;
27796
28374
  };
27797
28375
  D.animate = (dt) => {
28376
+ breach.flush();
27798
28377
  if (R) return;
27799
28378
  let moved = false;
27800
28379
  for (const [id, list2] of bodies) {
@@ -29290,7 +29869,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29290
29869
  const provider = options.provider ?? createFindsProvider(catalog);
29291
29870
  const items = catalog.list();
29292
29871
  const categories = [...ITEM_CATEGORIES.map((c) => c.id), ...[...catalog.categories.keys()].filter((id) => !ITEM_CATEGORIES.some((c) => c.id === id))].filter((id) => items.some((item) => item.category === id));
29293
- const shelves = shelvesLayout(items, categories);
29872
+ const openerOf = (item) => {
29873
+ const id = item.seal?.openWith?.find((tool2) => catalog.has(tool2));
29874
+ return id === void 0 ? void 0 : catalog.get(id);
29875
+ };
29876
+ const laid = items.flatMap((item) => {
29877
+ const opener = openerOf(item);
29878
+ return opener ? [item, { id: opener.id, category: item.category, size: opener.size }] : [item];
29879
+ });
29880
+ const shelves = shelvesLayout(laid, categories);
29294
29881
  const showcase = listItemModels().map((id) => ({ id, size: sizeOfModel(id), def: catalog.has(id) ? catalog.get(id) : void 0 }));
29295
29882
  const state = {
29296
29883
  category: CATEGORY_ALL,
@@ -29341,12 +29928,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29341
29928
  const contents = itemContents(subject);
29342
29929
  return contents ? { fluid: { container: contents.container, part: SHELL_PART_ID } } : {};
29343
29930
  };
29931
+ const sealOf = (def, fluid) => {
29932
+ const model = def.model;
29933
+ if (!def.seal || model === void 0 || !itemOpening(model)) return fluid;
29934
+ const openWith = def.seal.openWith ? [...def.seal.openWith, ...def.seal.openWith.map((id) => `${ITEM_PREFIX}${id}`)] : void 0;
29935
+ const seal = {
29936
+ ...def.seal,
29937
+ ...openWith ? { openWith } : {},
29938
+ open: (root) => void (latestTHREE && openItem(latestTHREE, root, model)),
29939
+ contents: (root, share) => setOpeningContents(root, share),
29940
+ // a screw cap is turned off by the other hand before the first bite or sip
29941
+ ...itemOpening(model)?.style === "screw_cap" ? { openUse: "unscrew", turn: (root, state2) => void turnItem(root, model, state2) } : {}
29942
+ };
29943
+ const eaten = def.category === "food" && fluid.fluid ? { fluid: { ...fluid.fluid, uses: ["eat", "pour_out"] } } : fluid;
29944
+ return { ...eaten, seal };
29945
+ };
29344
29946
  const itemOf = (def) => ({
29345
29947
  id: `${ITEM_PREFIX}${def.id}`,
29346
29948
  name: nameOf(def),
29347
29949
  movable: true,
29348
29950
  thumbnail: itemThumbnailSvg(drawable(def)),
29349
- ...fluidOf({ ...def.model === void 0 ? {} : { model: def.model }, size: def.size, weight: def.weight }),
29951
+ ...sealOf(def, fluidOf({ ...def.model === void 0 ? {} : { model: def.model }, size: def.size, weight: def.weight })),
29350
29952
  build: (THREE) => {
29351
29953
  const T2 = THREE;
29352
29954
  latestTHREE = T2;
@@ -29710,7 +30312,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29710
30312
  id: "modeled",
29711
30313
  name: text.metrics.modeled,
29712
30314
  read: () => {
29713
- const shown = visibleEntries().filter((entry2) => entry2.group === "shelf");
30315
+ const shown = [...new Map(visibleEntries().filter((entry2) => entry2.group === "shelf").map((entry2) => [entry2.itemId, entry2])).values()];
29714
30316
  return `${shown.filter((entry2) => entry2.kind === "model").length}/${shown.length}`;
29715
30317
  }
29716
30318
  },