@adatechnology/game-items-three 0.5.0 → 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
  });
@@ -2958,6 +2965,12 @@ var GameItemsThree = (() => {
2958
2965
  liquid: "honey",
2959
2966
  fill: 0.9
2960
2967
  }
2968
+ },
2969
+ shapes: {
2970
+ box: {
2971
+ hollow: true,
2972
+ welded: true
2973
+ }
2961
2974
  }
2962
2975
  };
2963
2976
 
@@ -2973,6 +2986,8 @@ var GameItemsThree = (() => {
2973
2986
  };
2974
2987
  var FAMILIES = destruction_default.families;
2975
2988
  var CONTENTS = destruction_default.contents;
2989
+ var SHAPES2 = destruction_default.shapes;
2990
+ var shapeDestruction = (shapeId) => SHAPES2[shapeId];
2976
2991
  var FAMILY_OF_MODEL = new Map(Object.entries(FAMILIES).flatMap(([family, entry2]) => entry2.models.map((model) => [model, family])));
2977
2992
  var itemFamily = (modelId) => FAMILY_OF_MODEL.get(modelId);
2978
2993
  var itemDestruction = (modelId) => {
@@ -3088,9 +3103,9 @@ var GameItemsThree = (() => {
3088
3103
  root.userData.tessellationTriangles = added;
3089
3104
  }
3090
3105
  function tagForDestruction(root, options = {}) {
3091
- const { modelId, weight, size, three } = options;
3106
+ const { modelId, shapeId, weight, size, powder, three } = options;
3092
3107
  if (weight !== void 0 && weight > 0) root.userData.destructionMass = weight;
3093
- const rule = modelId === void 0 ? void 0 : itemDestruction(modelId);
3108
+ const rule = modelId !== void 0 ? itemDestruction(modelId) : shapeId !== void 0 ? shapeDestruction(shapeId) : void 0;
3094
3109
  if (rule?.tessellate) tessellateShapes(root, rule.tessellate, three);
3095
3110
  const solid = [];
3096
3111
  root.traverse((object2) => {
@@ -3105,6 +3120,7 @@ var GameItemsThree = (() => {
3105
3120
  return;
3106
3121
  }
3107
3122
  if (rule?.hollow) mesh2.userData.hollow = true;
3123
+ if (powder) mesh2.userData.destructionPowder = { color: powder.color };
3108
3124
  solid.push(mesh2);
3109
3125
  });
3110
3126
  if (!rule?.welded || solid.length < 2) return;
@@ -5587,8 +5603,168 @@ var GameItemsThree = (() => {
5587
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]));
5588
5604
  };
5589
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
+
5590
5766
  // src/models/water-bottle.ts
5591
- 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]));
5592
5768
 
5593
5769
  // src/models/rice.ts
5594
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]));
@@ -5597,7 +5773,7 @@ var GameItemsThree = (() => {
5597
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]));
5598
5774
 
5599
5775
  // src/models/cachaca.ts
5600
- 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]));
5601
5777
 
5602
5778
  // src/models/first-aid-kit.ts
5603
5779
  var firstAidKit = ({ mesh: mesh2, bx, torus: torus2, G, plastic }) => {
@@ -5813,7 +5989,7 @@ var GameItemsThree = (() => {
5813
5989
  mesh2(cyl(0.41, 0.41, 0.86, 12), food(9314106), [0, -0.2, 0]),
5814
5990
  mesh2(cyl(0.34, 0.445, 0.12, 12), glass(13625070, 0.5), [0, 0.39, 0]),
5815
5991
  mesh2(cyl(0.34, 0.34, 0.05, 12), glass(13625070, 0.5), [0, 0.475, 0]),
5816
- 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])),
5817
5993
  mesh2(cyl(0.45, 0.45, 0.4, 12), paper2(15327172), [0, -0.2, 0]),
5818
5994
  mesh2(cyl(0.452, 0.452, 0.08, 12), cloth2(10172986), [0, -0.2, 0])
5819
5995
  );
@@ -6018,7 +6194,7 @@ var GameItemsThree = (() => {
6018
6194
  mesh2(cyl(0.395, 0.395, 0.68, 12), glass(14925936, 0.55), [0, -0.16, 0]),
6019
6195
  mesh2(cyl(0.37, 0.37, 0.6, 12), food(14260764), [0, -0.2, 0]),
6020
6196
  mesh2(cyl(0.3, 0.395, 0.14, 12), glass(14925936, 0.55), [0, 0.25, 0]),
6021
- 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])),
6022
6198
  mesh2(cyl(0.4, 0.4, 0.34, 12), paper2(15853512), [0, -0.2, 0]),
6023
6199
  mesh2(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.1, 0]),
6024
6200
  mesh2(cyl(0.402, 0.402, 0.05, 12), paper2(9067028), [0, -0.3, 0])
@@ -6122,7 +6298,7 @@ var GameItemsThree = (() => {
6122
6298
  mesh2(cyl(0.345, 0.345, 1.7, 12), glass(14923824, 0.65), [0, -0.45, 0]),
6123
6299
  mesh2(cyl(0.12, 0.345, 0.5, 12), glass(14923824, 0.65), [0, 0.65, 0]),
6124
6300
  mesh2(cyl(0.12, 0.12, 0.25, 12), glass(14923824, 0.65), [0, 1.025, 0]),
6125
- 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])),
6126
6302
  mesh2(cyl(0.35, 0.35, 0.8, 12), paper2(15853512), [0, -0.5, 0]),
6127
6303
  mesh2(cyl(0.352, 0.352, 0.2, 12), paper2(4160042), [0, -0.5, 0])
6128
6304
  );
@@ -6160,7 +6336,7 @@ var GameItemsThree = (() => {
6160
6336
  const body = mesh2(bx(1.5, 2.3, 1.5), glass(10473710, 0.75), [0, -0.5, 0]);
6161
6337
  const shoulder = mesh2(bx(1.2, 0.45, 1.2), glass(10473710, 0.75), [0, 0.875, 0]);
6162
6338
  const neck = mesh2(cyl(0.34, 0.34, 0.3, 12), dark, [0, 1.25, 0]);
6163
- 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]));
6164
6340
  const grip = mesh2(bx(0.16, 0.9, 0.3), dark, [1.07, 0.6, 0]);
6165
6341
  const upperBar = mesh2(bx(0.45, 0.16, 0.3), dark, [0.925, 0.97, 0]);
6166
6342
  const lowerBar = mesh2(bx(0.45, 0.16, 0.3), dark, [0.925, 0.23, 0]);
@@ -6175,7 +6351,7 @@ var GameItemsThree = (() => {
6175
6351
  const label = mesh2(cyl(0.332, 0.332, 0.7, 12), paper2(1448738), [0, -0.12, 0]);
6176
6352
  const stripe = mesh2(cyl(0.332, 0.332, 0.1, 12), paper2(15753232), [0, 0.28, 0]);
6177
6353
  const collar = mesh2(cyl(0.2, 0.2, 0.04, 12), plastic(15263976), [0, 0.705, 0]);
6178
- 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]));
6179
6355
  return G(bottle2, label, stripe, collar, cap);
6180
6356
  };
6181
6357
 
@@ -6222,7 +6398,7 @@ var GameItemsThree = (() => {
6222
6398
  const goldBand = mesh2(cyl(0.382, 0.382, 0.05, 12), metal(11896880), [0, 0.075, 0]);
6223
6399
  const crossUpright = mesh2(bx(0.06, 0.26, 0.02), paper2(8003371), [0, -0.35, 0.375]);
6224
6400
  const crossArm = mesh2(bx(0.18, 0.06, 0.02), paper2(8003371), [0, -0.3, 0.375]);
6225
- 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]));
6226
6402
  return G(bottle2, label, goldBand, crossUpright, crossArm, foil);
6227
6403
  };
6228
6404
 
@@ -6233,7 +6409,7 @@ var GameItemsThree = (() => {
6233
6409
  const nut = mesh2(cyl(0.11, 0.11, 0.03, 10), paper2(7031339), [0, -0.12, 0.285], [Math.PI / 2, 0, 0]);
6234
6410
  const leafLeft = mesh2(bx(0.2, 0.05, 0.01), paper2(15135448), [-0.12, 0.08, 0.292], [0, 0, 0.5]);
6235
6411
  const leafRight = mesh2(bx(0.2, 0.05, 0.01), paper2(15135448), [0.12, 0.08, 0.292], [0, 0, -0.5]);
6236
- 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]));
6237
6413
  return G(bottle2, label, nut, leafLeft, leafRight, cap);
6238
6414
  };
6239
6415
 
@@ -6243,7 +6419,7 @@ var GameItemsThree = (() => {
6243
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));
6244
6420
  const sediment = mesh2(cyl(0.31, 0.27, 0.12, 8), food(4865314), [0, -0.82, 0]);
6245
6421
  const label = mesh2(cyl(0.332, 0.332, 0.22, 12), paper2(13617588), [0, 0.38, 0]);
6246
- 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]));
6247
6423
  return G(bottle2, murk, sediment, label, cap);
6248
6424
  };
6249
6425
 
@@ -9232,7 +9408,7 @@ var GameItemsThree = (() => {
9232
9408
  const materials = kit.materials.slice(first);
9233
9409
  if (options.wear) setWear(materials, options.wear);
9234
9410
  root.userData.wear = options.wear ?? 0;
9235
- 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 });
9236
9412
  markKitOrigin(root, { owner: "game-items-three", kind: "item", id: item.id, ...kind === "shape" ? { standIn: true } : {} });
9237
9413
  return { root, kind, ...shape ? { shape } : {}, size: item.size, materials };
9238
9414
  }
@@ -15147,6 +15323,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15147
15323
  var FLAMMABLE_FLUIDS = ["gas", "fuel"];
15148
15324
  var VENT_DIAMETER = 8e-3;
15149
15325
  var FLAME_LIGHTS_LEAK_C = 470;
15326
+ var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
15150
15327
  var BREACH_RADIUS = 0.55;
15151
15328
  var BREACH_BLAST_FACTOR = 6;
15152
15329
  var BREACH_AREA = 0.25;
@@ -15162,6 +15339,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15162
15339
  var BREACH_GRID_CAP = 4096;
15163
15340
  var CHIP_ENERGY = 200;
15164
15341
  var CHIP_SPLASH = 0.5;
15342
+ var CRATER_SPENT = 0.6;
15165
15343
  var BAR_BOW = 0.08;
15166
15344
  var BULLET_JOINT_SPAN = 0.3;
15167
15345
  var CUT_SLICE = 1;
@@ -15182,12 +15360,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15182
15360
  const cells = p.st.chipped;
15183
15361
  if (!cells?.length) return 0;
15184
15362
  const [u, v] = planeAxes(p.thin);
15185
- 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;
15186
15367
  const across2 = along === u ? v : u;
15187
15368
  let worst = 0;
15188
15369
  for (const c of cells) {
15189
- let gone = 0;
15190
- 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
+ }
15191
15379
  worst = Math.max(worst, gone / Math.max(p.dims[across2], 1e-6));
15192
15380
  }
15193
15381
  return Math.min(1, worst);
@@ -15483,7 +15671,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15483
15671
  if (k < 0 || j.spec.kind !== "tie") return k;
15484
15672
  return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
15485
15673
  };
15674
+ let poseVersion = 0;
15675
+ const contactMemo = /* @__PURE__ */ new WeakMap();
15486
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) => {
15487
15686
  const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
15488
15687
  const lo = [0, 0, 0], hi = [0, 0, 0];
15489
15688
  for (let k = 0; k < 3; k++) {
@@ -16278,7 +16477,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16278
16477
  p.st.holes.push(hole);
16279
16478
  if (p.st.holes.length > maxHoles) p.st.holes.shift();
16280
16479
  markDamaged(p);
16281
- 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 } } : {} });
16282
16481
  setStage(o, p, 1, {}, src);
16283
16482
  return hole;
16284
16483
  };
@@ -16295,6 +16494,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16295
16494
  const detachEvents = (o, opened, by, src) => {
16296
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]) });
16297
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
+ };
16298
16513
  const checkDestroyed = (o, src) => {
16299
16514
  if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
16300
16515
  o.st.destroyed = true;
@@ -16395,7 +16610,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16395
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 }));
16396
16611
  const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
16397
16612
  const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
16398
- const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
16613
+ const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
16399
16614
  const { c, q } = partXf(o, p);
16400
16615
  const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
16401
16616
  const [u, v] = planeAxes(p.thin);
@@ -16419,7 +16634,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16419
16634
  velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
16420
16635
  angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
16421
16636
  mass: mass * faceArea(cell) / total,
16422
- ...marks.length > 0 ? { marks } : {}
16637
+ ...marks.length > 0 ? { marks } : {},
16638
+ ...inner ? { inner } : {}
16423
16639
  };
16424
16640
  });
16425
16641
  };
@@ -16460,7 +16676,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16460
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);
16461
16677
  };
16462
16678
  const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
16463
- var _a, _b;
16679
+ var _a, _b, _c;
16464
16680
  if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
16465
16681
  const rule = p.mat.hits?.[kind];
16466
16682
  const perKg = rule?.unitJoules ?? chipEnergy;
@@ -16470,7 +16686,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16470
16686
  const lp = toLocal(p.pos, p.q, objLocal(o, wp));
16471
16687
  const grid = gridOf(p);
16472
16688
  const worn = (_a = p.rt).chips ?? (_a.chips = {});
16689
+ const cut = (_b = p.rt).craters ?? (_b.craters = {});
16473
16690
  const out = [];
16691
+ const shot = kind === "ballistic";
16474
16692
  for (let i = 0; i < grid.length; i++) {
16475
16693
  const cell = grid[i];
16476
16694
  if (worn[i] === -1) continue;
@@ -16483,18 +16701,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16483
16701
  worn[i] = total;
16484
16702
  continue;
16485
16703
  }
16486
- worn[i] = -1;
16487
- 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 });
16488
16726
  }
16489
16727
  if (out.length === 0) return;
16490
16728
  const { c, q } = partXf(o, p);
16491
- const chipped = (_b = p.st).chipped ?? (_b.chipped = []);
16729
+ const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
16492
16730
  const holes = localHolesOf(p);
16493
16731
  for (const cell of out) {
16494
16732
  const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
16495
16733
  const center = toWorld(c, q, cell.center);
16496
16734
  const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
16497
- 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);
16498
16737
  p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
16499
16738
  chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
16500
16739
  p.st.mass = Math.max(0, p.st.mass - mass);
@@ -16511,6 +16750,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16511
16750
  collapseCheck(o, src);
16512
16751
  }
16513
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
+ };
16514
16791
  const exposeRebar = (o, p, src) => {
16515
16792
  const r = p.spec.reinforcement;
16516
16793
  if (!r) return;
@@ -16798,7 +17075,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16798
17075
  });
16799
17076
  }
16800
17077
  const patch = [scale * 4, scale * 4, scale * 4];
16801
- 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 });
16802
17079
  };
16803
17080
  const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
16804
17081
  const mat = p.mat;
@@ -16857,12 +17134,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16857
17134
  } else if (mat.ductility >= 0.5) {
16858
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);
16859
17136
  } else if (brittle) {
16860
- addCrack(o, p, entry2, n, crackLen, src);
17137
+ if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
16861
17138
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16862
17139
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16863
17140
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16864
17141
  wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
16865
- 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;
16866
17144
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16867
17145
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
16868
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);
@@ -16947,10 +17225,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16947
17225
  if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
16948
17226
  return reach;
16949
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
+ };
16950
17243
  const blastObject = (o, point, energy, radius, src) => {
16951
17244
  const start = cur.length;
16952
17245
  const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
16953
- 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);
16954
17247
  res.events = cur.slice(start);
16955
17248
  return res;
16956
17249
  };
@@ -17074,7 +17367,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17074
17367
  let order = 0;
17075
17368
  for (const o of inRange) {
17076
17369
  if (only && only !== o) continue;
17077
- for (const p of o.parts) {
17370
+ for (const p of blastCandidates(o, point, radius)) {
17078
17371
  if (isLost(p)) continue;
17079
17372
  const dist = distToPart(o, p, point);
17080
17373
  if (dist < radius) items.push({ dist, order: order++, o, p });
@@ -17621,6 +17914,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17621
17914
  const p = getPart(o, partId);
17622
17915
  p.pos = [position[0], position[1], position[2]];
17623
17916
  p.q = q;
17917
+ poseVersion++;
17624
17918
  updateBound(o);
17625
17919
  },
17626
17920
  remove(id) {
@@ -17639,6 +17933,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17639
17933
  const o = getObj(id);
17640
17934
  o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
17641
17935
  applySoilPose(o);
17936
+ slideOffResting(o, rotation);
17642
17937
  },
17643
17938
  splitGroup(id, group, newId) {
17644
17939
  if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
@@ -17662,7 +17957,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17662
17957
  if (o.spec.name !== void 0) s.name = o.spec.name;
17663
17958
  if (o.spec.tags) s.tags = [...o.spec.tags];
17664
17959
  if (o.spec.soil !== void 0) s.soil = o.spec.soil;
17665
- return clone(s);
17960
+ return s;
17666
17961
  };
17667
17962
  const inNew = (pid) => moved.has(pid);
17668
17963
  const inOld = (pid) => !moved.has(pid);
@@ -17689,7 +17984,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17689
17984
  const p = getPart(o, partId);
17690
17985
  return whole(() => doHit(o, h, p));
17691
17986
  },
17987
+ blastProbe() {
17988
+ return { ...blastProbe };
17989
+ },
17692
17990
  explode(point, energy, radius, source, blast) {
17991
+ blastProbe.partsTested = 0;
17992
+ blastProbe.partsNear = 0;
17693
17993
  return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
17694
17994
  },
17695
17995
  raycast(origin, direction, maxDistance = Infinity) {
@@ -17699,7 +17999,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
17699
17999
  sweepBox(box4, direction, distance2, options2 = {}) {
17700
18000
  const d = vec.normalize(direction);
17701
18001
  const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
17702
- 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;
17703
18004
  let best = null;
17704
18005
  for (const origin of sweepOrigins(box4, d, options2.spacing ?? 0.2)) {
17705
18006
  const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
@@ -27302,6 +27603,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27302
27603
  function collapseRange(mesh2, v0, v1) {
27303
27604
  const pos = mesh2.geometry.attributes.position;
27304
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]);
27305
27610
  const saved = a.slice(v0 * 3, v1 * 3);
27306
27611
  const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
27307
27612
  for (let k = v0; k < v1; k++) {
@@ -27316,6 +27621,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27316
27621
  const pos = mesh2.geometry.attributes.position;
27317
27622
  pos.array.set(saved, v0 * 3);
27318
27623
  pos.needsUpdate = true;
27624
+ const list2 = mesh2.userData.collapsedRanges;
27625
+ if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
27319
27626
  }
27320
27627
  function noise2(x, y, cell) {
27321
27628
  const h = (i2, j2) => {
@@ -27330,13 +27637,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27330
27637
  var SPALL_MIN = 0.025;
27331
27638
  var SPALL_RAGGED = 0.045;
27332
27639
  var COVER2 = 0.015;
27640
+ var MAX_FLAKES = 64;
27333
27641
  function installDestructionBreach(K, _shared) {
27334
27642
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
27335
27643
  function createBreachView(opts) {
27336
27644
  const { house, structure } = opts;
27337
27645
  const views = /* @__PURE__ */ new Map();
27646
+ const dirty = /* @__PURE__ */ new Set();
27647
+ const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
27648
+ const dropped = [];
27338
27649
  let lod = 0;
27339
- const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
27340
27650
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
27341
27651
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
27342
27652
  const local = (p) => {
@@ -27405,6 +27715,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27405
27715
  const mesh2 = new THREE.Mesh(g, material);
27406
27716
  mesh2.name = "breach-shell";
27407
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
+ };
27408
27723
  mesh2.position.copy(it.mesh.position);
27409
27724
  mesh2.quaternion.copy(it.mesh.quaternion);
27410
27725
  mesh2.scale.copy(it.mesh.scale);
@@ -27420,6 +27735,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27420
27735
  toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27421
27736
  return mesh2;
27422
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
+ }
27423
27751
  function maskOf(hostId, parts) {
27424
27752
  const host = structure.byId.get(hostId);
27425
27753
  const [u0, v0, u1, v1] = host.aabb;
@@ -27435,41 +27763,73 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27435
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) };
27436
27764
  }
27437
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
+ }
27438
27779
  function paint(view) {
27439
- const { mask } = view;
27780
+ const { mask, core } = view;
27440
27781
  const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
27441
27782
  const holes = [];
27783
+ const pits = [];
27442
27784
  const discs = [];
27785
+ const flakes = [];
27443
27786
  const shown = [];
27444
27787
  for (const id of view.parts) {
27445
- 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);
27446
27789
  const all = cells(part, units2);
27447
27790
  const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
27448
27791
  const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
27449
27792
  if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
27450
- 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;
27451
27796
  const kept = [];
27452
27797
  for (const c of all) (gone(c) ? holes : kept).push(c);
27453
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
+ }
27454
27803
  }
27455
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);
27456
27806
  mask.data.fill(0);
27457
- const stamp = (s0, s1, y0, y1, inside2) => {
27458
- 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));
27459
- 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));
27460
27811
  for (let j = j0; j <= j1; j++) {
27461
- const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
27812
+ const y = m.y0 + (j + 0.5) / m.h * m.height;
27462
27813
  for (let i = i0; i <= i1; i++) {
27463
- const s = (i + 0.5) / mask.w * mask.len;
27464
- const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
27465
- 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;
27466
27816
  }
27467
27817
  }
27468
27818
  };
27469
- 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));
27470
- 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
+ }
27471
27830
  mask.texture.needsUpdate = true;
27472
- 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);
27473
27833
  const list2 = [];
27474
27834
  const tone = (hex3, k) => tmp.c.setHex(hex3).multiplyScalar(k).getHex();
27475
27835
  for (const { c, part, units: units2, thick, mid } of shown) {
@@ -27479,7 +27839,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27479
27839
  const base = fragmentMaterial(part.material).color.getHex();
27480
27840
  const n = noise2(centre * 3.1, y * 2.7, 0.07);
27481
27841
  if (units2.mortar === "concrete") {
27482
- 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));
27483
27843
  } else {
27484
27844
  at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
27485
27845
  at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
@@ -27489,7 +27849,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27489
27849
  view.units.removeFromParent();
27490
27850
  view.units.dispose();
27491
27851
  }
27492
- 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));
27493
27853
  units.name = "breach:" + view.host;
27494
27854
  list2.forEach((u, i) => {
27495
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));
@@ -27505,6 +27865,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27505
27865
  house.add(units);
27506
27866
  view.units = units;
27507
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 };
27508
27881
  }
27509
27882
  function draw2(partId, change) {
27510
27883
  if (lod !== 0) return false;
@@ -27516,23 +27889,49 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27516
27889
  if (!items.length) return false;
27517
27890
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
27518
27891
  if (!parts.includes(partId)) return false;
27519
- const mask = maskOf(hostId, parts);
27520
- 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
+ };
27521
27910
  }
27522
27911
  if (!view.chips.has(partId)) return false;
27523
27912
  if (change.opening) {
27524
27913
  if (view.openings.has(partId)) return false;
27525
27914
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
27526
27915
  }
27527
- 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
+ }
27528
27922
  if (!view.saved) {
27529
27923
  view.shells = view.items.map((it) => shell(it, view.mask));
27924
+ for (const m of view.shells) m.userData.behind = () => view.units;
27530
27925
  view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27531
27926
  }
27532
- paint(view);
27533
27927
  views.set(hostId, view);
27534
- invalidate();
27535
- invalidateShadow();
27928
+ if (opts.deferPaint && change.spall) dirty.add(view);
27929
+ else {
27930
+ dirty.delete(view);
27931
+ paint(view);
27932
+ invalidate();
27933
+ invalidateShadow();
27934
+ }
27536
27935
  return true;
27537
27936
  }
27538
27937
  function plain2(view, shown) {
@@ -27541,24 +27940,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27541
27940
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
27542
27941
  }
27543
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
+ }
27544
27955
  function dispose(view) {
27545
- view.units.removeFromParent();
27546
- view.units.dispose();
27956
+ dirty.delete(view);
27957
+ if (view.units) {
27958
+ view.units.removeFromParent();
27959
+ view.units.dispose();
27960
+ }
27961
+ view.unitMaterial.dispose();
27547
27962
  for (const m of view.shells) {
27548
27963
  m.removeFromParent();
27549
27964
  m.geometry.dispose();
27550
27965
  m.material.dispose();
27551
27966
  }
27552
27967
  view.mask.texture.dispose();
27968
+ view.core.texture.dispose();
27553
27969
  }
27554
27970
  return {
27555
27971
  /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
27556
27972
  stage(event) {
27557
27973
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
27558
27974
  },
27559
- /** 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
+ */
27560
28004
  chip(event) {
27561
- 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] } });
27562
28007
  },
27563
28008
  /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
27564
28009
  drop(partId) {
@@ -27566,9 +28011,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27566
28011
  if (!v) return;
27567
28012
  dispose(v);
27568
28013
  views.delete(partId);
28014
+ if (v.saved) dropped.push(v);
27569
28015
  },
27570
28016
  /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
27571
28017
  setLod(level) {
28018
+ flush(Infinity);
27572
28019
  const was = lod;
27573
28020
  lod = level;
27574
28021
  if (was === 0 === (level === 0)) return;
@@ -27587,21 +28034,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27587
28034
  if (lod === 0) plain2(v, true);
27588
28035
  }
27589
28036
  views.clear();
28037
+ for (const v of dropped) plain2(v, true);
28038
+ dropped.length = 0;
27590
28039
  },
28040
+ flush,
27591
28041
  /** The wall has damage drawn. */
27592
28042
  has(partId) {
27593
28043
  return views.has(partId);
27594
28044
  },
27595
- /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
27596
- 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), {
27597
28047
  walls: views.size,
27598
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),
27599
28051
  units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
27600
28052
  })
27601
28053
  };
27602
28054
  }
27603
28055
  Object.assign(K, { createBreachView });
27604
28056
  }
28057
+ var FOLLOW_FLUSH_FALLBACK_MS = 100;
27605
28058
  function installDestructionWorld(K, shared) {
27606
28059
  const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
27607
28060
  function createHouseDestruction(root, house, structure, parts, colliders, water) {
@@ -27610,7 +28063,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27610
28063
  const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
27611
28064
  D.renderer = R;
27612
28065
  D.spec = null;
27613
- 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 } : {} });
27614
28067
  D.breach = breach;
27615
28068
  D.onEvent = (fn) => {
27616
28069
  listeners.add(fn);
@@ -27795,6 +28248,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27795
28248
  case "chip":
27796
28249
  breach.chip(e);
27797
28250
  break;
28251
+ case "spall":
28252
+ breach.spall(e);
28253
+ break;
27798
28254
  case "collapse":
27799
28255
  for (const id of e.parts) {
27800
28256
  breach.drop(id);
@@ -27836,7 +28292,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27836
28292
  D.specOptions = specOptions || null;
27837
28293
  D.spec = structure.spec(specOptions);
27838
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] });
27839
- 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() });
27840
28297
  off = D.world.on("*", handleEvent);
27841
28298
  D.active = true;
27842
28299
  return D;
@@ -27851,6 +28308,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27851
28308
  };
27852
28309
  D.follow = (world, id, spec) => {
27853
28310
  D.spec = spec;
28311
+ breach.claim();
28312
+ let fallback = null;
27854
28313
  const offFollow = world.on("*", (e) => {
27855
28314
  if (e.object !== id) return;
27856
28315
  switch (e.type) {
@@ -27860,6 +28319,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27860
28319
  case "chip":
27861
28320
  breach.chip(e);
27862
28321
  break;
28322
+ case "spall":
28323
+ breach.spall(e);
28324
+ break;
27863
28325
  case "fracture":
27864
28326
  breach.drop(e.part);
27865
28327
  break;
@@ -27869,10 +28331,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27869
28331
  default:
27870
28332
  break;
27871
28333
  }
28334
+ if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
28335
+ fallback = null;
28336
+ breach.flush(Infinity);
28337
+ }, FOLLOW_FLUSH_FALLBACK_MS);
27872
28338
  });
27873
28339
  return { restore: () => breach.restore(), dispose: () => {
27874
28340
  offFollow();
27875
28341
  breach.restore();
28342
+ }, update: () => {
28343
+ breach.flush();
27876
28344
  } };
27877
28345
  };
27878
28346
  D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
@@ -27905,6 +28373,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27905
28373
  return r;
27906
28374
  };
27907
28375
  D.animate = (dt) => {
28376
+ breach.flush();
27908
28377
  if (R) return;
27909
28378
  let moved = false;
27910
28379
  for (const [id, list2] of bodies) {
@@ -29400,7 +29869,15 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29400
29869
  const provider = options.provider ?? createFindsProvider(catalog);
29401
29870
  const items = catalog.list();
29402
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));
29403
- 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);
29404
29881
  const showcase = listItemModels().map((id) => ({ id, size: sizeOfModel(id), def: catalog.has(id) ? catalog.get(id) : void 0 }));
29405
29882
  const state = {
29406
29883
  category: CATEGORY_ALL,
@@ -29451,12 +29928,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29451
29928
  const contents = itemContents(subject);
29452
29929
  return contents ? { fluid: { container: contents.container, part: SHELL_PART_ID } } : {};
29453
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
+ };
29454
29946
  const itemOf = (def) => ({
29455
29947
  id: `${ITEM_PREFIX}${def.id}`,
29456
29948
  name: nameOf(def),
29457
29949
  movable: true,
29458
29950
  thumbnail: itemThumbnailSvg(drawable(def)),
29459
- ...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 })),
29460
29952
  build: (THREE) => {
29461
29953
  const T2 = THREE;
29462
29954
  latestTHREE = T2;
@@ -29820,7 +30312,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
29820
30312
  id: "modeled",
29821
30313
  name: text.metrics.modeled,
29822
30314
  read: () => {
29823
- 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()];
29824
30316
  return `${shown.filter((entry2) => entry2.kind === "model").length}/${shown.length}`;
29825
30317
  }
29826
30318
  },