@adatechnology/game-items-three 0.4.0 → 0.5.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.
@@ -2640,8 +2640,8 @@ var GameItemsThree = (() => {
2640
2640
  bedHeight = roadY * 1e3 + z * 14 - (x - params.roadSlopeAnchorX) * 5;
2641
2641
  roughness = 0.55;
2642
2642
  }
2643
- const noise2 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
2644
- bed[index] = bedHeight + (noise2 - 0.5) * roughness * 2;
2643
+ const noise3 = valueNoise(column * 0.35, row * 0.35) * 0.6 + valueNoise(column * 1.3, row * 1.3) * 0.4;
2644
+ bed[index] = bedHeight + (noise3 - 0.5) * roughness * 2;
2645
2645
  vertical[index] = region === 0 || region === 2 ? 0.95 : 1;
2646
2646
  pin[index] = 0.62 + 0.76 * (valueNoise(column * 0.21 + 40, row * 0.21) * 0.5 + hashCell(column, row) * 0.5) * (region === 3 ? 1.25 : 1);
2647
2647
  }
@@ -2812,6 +2812,55 @@ var GameItemsThree = (() => {
2812
2812
  ends: 22e-5
2813
2813
  }
2814
2814
  }
2815
+ },
2816
+ plastic_bottle: {
2817
+ models: [
2818
+ "water_bottle",
2819
+ "water_jug_5l",
2820
+ "dirty_water",
2821
+ "sports_drink",
2822
+ "coconut_water",
2823
+ "cooking_oil",
2824
+ "alcohol_70",
2825
+ "fuel_jerrycan"
2826
+ ],
2827
+ rule: {
2828
+ hollow: true,
2829
+ welded: true,
2830
+ skinTags: [
2831
+ "paper",
2832
+ "food"
2833
+ ],
2834
+ materials: {
2835
+ glass: "plastic"
2836
+ }
2837
+ }
2838
+ },
2839
+ glass_bottle: {
2840
+ models: [
2841
+ "cachaca",
2842
+ "communion_wine",
2843
+ "honey_jar",
2844
+ "home_preserves"
2845
+ ],
2846
+ rule: {
2847
+ hollow: true,
2848
+ welded: true,
2849
+ skinTags: [
2850
+ "paper",
2851
+ "food"
2852
+ ]
2853
+ }
2854
+ },
2855
+ drink_carton: {
2856
+ models: [
2857
+ "juice_box",
2858
+ "milk_carton"
2859
+ ],
2860
+ rule: {
2861
+ hollow: true,
2862
+ welded: true
2863
+ }
2815
2864
  }
2816
2865
  },
2817
2866
  contents: {
@@ -2827,6 +2876,67 @@ var GameItemsThree = (() => {
2827
2876
  liquid: "soda",
2828
2877
  fill: 0.95
2829
2878
  },
2879
+ water_bottle: {
2880
+ liquid: "water",
2881
+ fill: 0.95
2882
+ },
2883
+ water_jug_5l: {
2884
+ liquid: "water",
2885
+ fill: 0.95,
2886
+ box: true
2887
+ },
2888
+ dirty_water: {
2889
+ liquid: "water",
2890
+ fill: 0.9
2891
+ },
2892
+ sports_drink: {
2893
+ liquid: "juice",
2894
+ fill: 0.95
2895
+ },
2896
+ coconut_water: {
2897
+ liquid: "water",
2898
+ fill: 0.9,
2899
+ box: true
2900
+ },
2901
+ juice_box: {
2902
+ liquid: "juice",
2903
+ fill: 0.95,
2904
+ box: true
2905
+ },
2906
+ milk_carton: {
2907
+ liquid: "milk",
2908
+ fill: 0.95,
2909
+ box: true
2910
+ },
2911
+ cachaca: {
2912
+ liquid: "spirits",
2913
+ fill: 0.9
2914
+ },
2915
+ communion_wine: {
2916
+ liquid: "wine",
2917
+ fill: 0.9
2918
+ },
2919
+ cooking_oil: {
2920
+ liquid: "cooking_oil",
2921
+ fill: 0.9
2922
+ },
2923
+ alcohol_70: {
2924
+ liquid: "spirits",
2925
+ fill: 0.9
2926
+ },
2927
+ honey_jar: {
2928
+ liquid: "honey",
2929
+ fill: 0.85
2930
+ },
2931
+ home_preserves: {
2932
+ liquid: "water",
2933
+ fill: 0.5
2934
+ },
2935
+ fuel_jerrycan: {
2936
+ liquid: "fuel",
2937
+ fill: 0.85,
2938
+ box: true
2939
+ },
2830
2940
  canned_beans: {
2831
2941
  liquid: "juice",
2832
2942
  fill: 0.4
@@ -14207,6 +14317,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14207
14317
  },
14208
14318
  hitDebris: "chips",
14209
14319
  breakDebris: "dust",
14320
+ visual: { innerColor: 10197652 },
14210
14321
  thermal: {
14211
14322
  conductivity: 0.15,
14212
14323
  heatCapacity: 2.5
@@ -14237,6 +14348,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14237
14348
  ],
14238
14349
  hitDebris: "chips",
14239
14350
  breakDebris: "dust",
14351
+ visual: { innerColor: 12085567 },
14240
14352
  thermal: {
14241
14353
  conductivity: 0.15,
14242
14354
  heatCapacity: 2.5
@@ -14267,6 +14379,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14267
14379
  ],
14268
14380
  hitDebris: "dust",
14269
14381
  breakDebris: "dust",
14382
+ visual: { innerColor: 14999507 },
14270
14383
  thermal: {
14271
14384
  conductivity: 0.1,
14272
14385
  heatCapacity: 1.5
@@ -14292,6 +14405,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14292
14405
  ],
14293
14406
  hitDebris: "chips",
14294
14407
  breakDebris: "dust",
14408
+ visual: { innerColor: 9407364 },
14295
14409
  thermal: {
14296
14410
  conductivity: 0.2,
14297
14411
  heatCapacity: 2.5
@@ -15049,6 +15163,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15049
15163
  var CHIP_ENERGY = 200;
15050
15164
  var CHIP_SPLASH = 0.5;
15051
15165
  var BAR_BOW = 0.08;
15166
+ var BULLET_JOINT_SPAN = 0.3;
15052
15167
  var CUT_SLICE = 1;
15053
15168
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
15054
15169
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -16175,7 +16290,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16175
16290
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
16176
16291
  setStage(o, p, 1, {}, src);
16177
16292
  };
16178
- const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
16293
+ const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
16179
16294
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
16180
16295
  const detachEvents = (o, opened, by, src) => {
16181
16296
  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]) });
@@ -16216,7 +16331,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16216
16331
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
16217
16332
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
16218
16333
  };
16219
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
16334
+ const jointLength = (o, j) => {
16335
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
16336
+ let len = 0;
16337
+ for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
16338
+ return len;
16339
+ };
16340
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
16220
16341
  if (amount <= 0) return;
16221
16342
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
16222
16343
  if (split && mine.length) amount /= mine.length;
@@ -16224,7 +16345,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16224
16345
  if (j.st.open) continue;
16225
16346
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
16226
16347
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
16227
- j.st.hp -= amount;
16348
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
16228
16349
  if (j.st.hp <= 0) {
16229
16350
  j.st.hp = 0;
16230
16351
  j.st.open = true;
@@ -16253,7 +16374,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16253
16374
  if (spalls.length > MAX_SPALLS) spalls.shift();
16254
16375
  markDamaged(p);
16255
16376
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
16256
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
16377
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
16257
16378
  setStage(o, p, 1, {}, src);
16258
16379
  };
16259
16380
  const crater = (o, p, wp, normal, radius, src) => {
@@ -16740,7 +16861,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16740
16861
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16741
16862
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16742
16863
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
16743
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
16864
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
16744
16865
  const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
16745
16866
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16746
16867
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
@@ -27196,6 +27317,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27196
27317
  pos.array.set(saved, v0 * 3);
27197
27318
  pos.needsUpdate = true;
27198
27319
  }
27320
+ function noise2(x, y, cell) {
27321
+ const h = (i2, j2) => {
27322
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
27323
+ return s - Math.floor(s);
27324
+ };
27325
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
27326
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
27327
+ return (h(i, j) * (1 - su) + h(i + 1, j) * su) * (1 - sv) + (h(i, j + 1) * (1 - su) + h(i + 1, j + 1) * su) * sv;
27328
+ }
27329
+ var MASK_TEXEL = 0.02;
27330
+ var SPALL_MIN = 0.025;
27331
+ var SPALL_RAGGED = 0.045;
27332
+ var COVER2 = 0.015;
27199
27333
  function installDestructionBreach(K, _shared) {
27200
27334
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
27201
27335
  function createBreachView(opts) {
@@ -27205,55 +27339,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27205
27339
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
27206
27340
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
27207
27341
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
27208
- const wallColour = (items) => {
27209
- for (const it of items) {
27210
- if (!it.mesh) continue;
27211
- const c = it.mesh.geometry.attributes.color;
27212
- if (!c) continue;
27213
- const a = c.array;
27214
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
27215
- return tmp.c.getHex();
27216
- }
27217
- return null;
27218
- };
27219
27342
  const local = (p) => {
27220
27343
  house.updateMatrixWorld(true);
27221
27344
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
27222
27345
  return [v.x, v.y, v.z];
27223
27346
  };
27224
- function courses(part, units, opening, chips, paint) {
27347
+ function cells(part, units) {
27225
27348
  const [u0, v0, u1, v1] = part.aabb;
27226
27349
  const alongU = u1 - u0 >= v1 - v0;
27227
27350
  const len = alongU ? u1 - u0 : v1 - v0;
27228
- const thick = alongU ? v1 - v0 : u1 - u0;
27229
27351
  const [ul, uh] = units.size;
27230
27352
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
27231
27353
  const rowH = (part.y1 - part.y0) / rows;
27232
27354
  const per = Math.max(1, Math.round(len / ul));
27233
27355
  const w = len / per;
27234
- const edge = fragmentMaterial(part.material).color.getHex();
27235
- const list2 = [];
27356
+ const out = [];
27236
27357
  for (let r = 0; r < rows; r++) {
27237
27358
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
27238
- const cells = per + (shift ? 1 : 0);
27239
- for (let i = 0; i < cells; i++) {
27240
- let a = i * w - shift, b = a + w;
27241
- a = Math.max(0, a);
27242
- b = Math.min(len, b);
27243
- if (b - a < 1e-3) continue;
27244
- const mid = (a + b) / 2;
27245
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
27246
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
27247
- const y = part.y0 + (r + 0.5) * rowH;
27248
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
27249
- if (opening && d < opening.radius) continue;
27250
- const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
27251
- if (chips.some(gone)) continue;
27252
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
27253
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
27359
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
27360
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
27361
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
27254
27362
  }
27255
27363
  }
27256
- return list2;
27364
+ return out;
27257
27365
  }
27258
27366
  const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
27259
27367
  const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
@@ -27267,6 +27375,137 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27267
27375
  return null;
27268
27376
  }
27269
27377
  const riders = (hostId) => structure.parts.filter((q) => q.id !== hostId && specOf(q.id)?.units && hostOf(q.id) === hostId && !structure.meshesOf(q.id).some((it) => it.mesh)).map((q) => q.id);
27378
+ function shell(it, mask) {
27379
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
27380
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
27381
+ for (const [name, attr] of Object.entries(g0.attributes)) {
27382
+ const k = attr.itemSize;
27383
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
27384
+ }
27385
+ if (g0.index) {
27386
+ const ix = g0.index.array.slice(it.i0, it.i1);
27387
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
27388
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
27389
+ }
27390
+ g.computeBoundingBox();
27391
+ g.computeBoundingSphere();
27392
+ const base = it.mesh.material;
27393
+ const material = base.clone();
27394
+ const toHouse = { value: new THREE.Matrix4() };
27395
+ material.onBeforeCompile = (sh) => {
27396
+ base.onBeforeCompile?.(sh);
27397
+ sh.uniforms.uToHouse = toHouse;
27398
+ sh.uniforms.uMask = { value: mask.texture };
27399
+ sh.uniforms.uMaskRect = { value: mask.rect };
27400
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
27401
+ sh.vertexShader = sh.vertexShader.replace("void main() {", "uniform mat4 uToHouse;\nvarying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\nvHousePos = (uToHouse * vec4(transformed, 1.0)).xyz;");
27402
+ sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uMask;\nuniform vec4 uMaskRect;\nuniform float uMaskAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 mUv = vec2(((uMaskAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uMaskRect.x) / uMaskRect.z, (vHousePos.y - uMaskRect.y) / uMaskRect.w);\n if (mUv.x >= 0.0 && mUv.x <= 1.0 && mUv.y >= 0.0 && mUv.y <= 1.0 && texture2D(uMask, mUv).r > 0.5) discard;");
27403
+ };
27404
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
27405
+ const mesh2 = new THREE.Mesh(g, material);
27406
+ mesh2.name = "breach-shell";
27407
+ mesh2.userData.breachMask = mask;
27408
+ mesh2.position.copy(it.mesh.position);
27409
+ mesh2.quaternion.copy(it.mesh.quaternion);
27410
+ mesh2.scale.copy(it.mesh.scale);
27411
+ mesh2.castShadow = it.mesh.castShadow;
27412
+ mesh2.receiveShadow = it.mesh.receiveShadow;
27413
+ mesh2.renderOrder = it.mesh.renderOrder;
27414
+ mesh2.onBeforeRender = () => {
27415
+ house.updateMatrixWorld();
27416
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27417
+ };
27418
+ it.mesh.parent.add(mesh2);
27419
+ mesh2.updateMatrixWorld(true);
27420
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27421
+ return mesh2;
27422
+ }
27423
+ function maskOf(hostId, parts) {
27424
+ const host = structure.byId.get(hostId);
27425
+ const [u0, v0, u1, v1] = host.aabb;
27426
+ const alongU = u1 - u0 >= v1 - v0;
27427
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
27428
+ const len = alongU ? u1 - u0 : v1 - v0;
27429
+ const w = Math.max(2, Math.min(2048, Math.ceil(len / MASK_TEXEL))), h = Math.max(2, Math.min(1024, Math.ceil((y1 - y0) / MASK_TEXEL)));
27430
+ const data = new Uint8Array(w * h);
27431
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
27432
+ texture.magFilter = THREE.LinearFilter;
27433
+ texture.minFilter = THREE.LinearFilter;
27434
+ texture.needsUpdate = true;
27435
+ 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
+ }
27437
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
27438
+ function paint(view) {
27439
+ const { mask } = view;
27440
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
27441
+ const holes = [];
27442
+ const discs = [];
27443
+ const shown = [];
27444
+ 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);
27446
+ const all = cells(part, units2);
27447
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
27448
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
27449
+ 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;
27451
+ const kept = [];
27452
+ for (const c of all) (gone(c) ? holes : kept).push(c);
27453
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
27454
+ }
27455
+ const reach = SPALL_MIN + SPALL_RAGGED;
27456
+ 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));
27460
+ for (let j = j0; j <= j1; j++) {
27461
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
27462
+ 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;
27466
+ }
27467
+ }
27468
+ };
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));
27471
+ 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);
27473
+ const list2 = [];
27474
+ const tone = (hex3, k) => tmp.c.setHex(hex3).multiplyScalar(k).getHex();
27475
+ for (const { c, part, units: units2, thick, mid } of shown) {
27476
+ if (!near(c)) continue;
27477
+ const centre = mask.start + (c.a + c.b) / 2, y = (c.y0 + c.y1) / 2, len = c.b - c.a, h = c.y1 - c.y0;
27478
+ const at = (s, dy, d, colour) => list2.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
27479
+ const base = fragmentMaterial(part.material).color.getHex();
27480
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
27481
+ if (units2.mortar === "concrete") {
27482
+ at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
27483
+ } else {
27484
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
27485
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
27486
+ }
27487
+ }
27488
+ if (view.units) {
27489
+ view.units.removeFromParent();
27490
+ view.units.dispose();
27491
+ }
27492
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
27493
+ units.name = "breach:" + view.host;
27494
+ list2.forEach((u, i) => {
27495
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
27496
+ units.setMatrixAt(i, tmp.m);
27497
+ units.setColorAt(i, tmp.c.setHex(u.colour));
27498
+ });
27499
+ units.count = list2.length;
27500
+ units.instanceMatrix.needsUpdate = true;
27501
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
27502
+ units.castShadow = true;
27503
+ units.receiveShadow = true;
27504
+ units.visible = lod === 0;
27505
+ house.add(units);
27506
+ view.units = units;
27507
+ view.gone = holes.length;
27508
+ }
27270
27509
  function draw2(partId, change) {
27271
27510
  if (lod !== 0) return false;
27272
27511
  const hostId = hostOf(partId);
@@ -27276,37 +27515,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27276
27515
  const items = structure.meshesOf(hostId).filter((it) => it.mesh);
27277
27516
  if (!items.length) return false;
27278
27517
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
27279
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
27518
+ 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() };
27280
27521
  }
27281
27522
  if (!view.chips.has(partId)) return false;
27282
27523
  if (change.opening) {
27283
27524
  if (view.openings.has(partId)) return false;
27284
27525
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
27285
- if (partId === hostId) view.opening = view.openings.get(partId);
27286
27526
  }
27287
27527
  if (change.chip) view.chips.get(partId).push(local(change.chip));
27288
- const list2 = [];
27289
- for (const [id, chips] of view.chips) list2.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
27290
- if (view.mesh) {
27291
- view.mesh.removeFromParent();
27292
- view.mesh.dispose();
27528
+ if (!view.saved) {
27529
+ view.shells = view.items.map((it) => shell(it, view.mask));
27530
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27293
27531
  }
27294
- const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
27295
- mesh2.name = "breach:" + hostId;
27296
- list2.forEach((u, i) => {
27297
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
27298
- mesh2.setMatrixAt(i, tmp.m);
27299
- mesh2.setColorAt(i, tmp.c.setHex(u.colour));
27300
- });
27301
- mesh2.count = list2.length;
27302
- mesh2.instanceMatrix.needsUpdate = true;
27303
- if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
27304
- mesh2.castShadow = true;
27305
- mesh2.receiveShadow = true;
27306
- house.add(mesh2);
27307
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27308
- view.mesh = mesh2;
27309
- view.units = list2.length;
27532
+ paint(view);
27310
27533
  views.set(hostId, view);
27311
27534
  invalidate();
27312
27535
  invalidateShadow();
@@ -27318,30 +27541,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27318
27541
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
27319
27542
  }
27320
27543
  }
27544
+ function dispose(view) {
27545
+ view.units.removeFromParent();
27546
+ view.units.dispose();
27547
+ for (const m of view.shells) {
27548
+ m.removeFromParent();
27549
+ m.geometry.dispose();
27550
+ m.material.dispose();
27551
+ }
27552
+ view.mask.texture.dispose();
27553
+ }
27321
27554
  return {
27322
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
27555
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
27323
27556
  stage(event) {
27324
27557
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
27325
27558
  },
27326
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
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. */
27327
27560
  chip(event) {
27328
27561
  return draw2(event.part, { chip: event.center });
27329
27562
  },
27330
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
27563
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
27331
27564
  drop(partId) {
27332
27565
  const v = views.get(partId);
27333
27566
  if (!v) return;
27334
- v.mesh.removeFromParent();
27335
- v.mesh.dispose();
27567
+ dispose(v);
27336
27568
  views.delete(partId);
27337
27569
  },
27338
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
27570
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
27339
27571
  setLod(level) {
27340
27572
  const was = lod;
27341
27573
  lod = level;
27342
27574
  if (was === 0 === (level === 0)) return;
27343
27575
  for (const v of views.values()) {
27344
- v.mesh.visible = level === 0;
27576
+ v.units.visible = level === 0;
27577
+ for (const m of v.shells) m.visible = level === 0;
27345
27578
  plain2(v, level !== 0);
27346
27579
  }
27347
27580
  invalidate();
@@ -27350,17 +27583,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27350
27583
  /** Everything back to what it was (after `world.reset`). */
27351
27584
  restore() {
27352
27585
  for (const v of views.values()) {
27353
- v.mesh.removeFromParent();
27354
- v.mesh.dispose();
27586
+ dispose(v);
27355
27587
  if (lod === 0) plain2(v, true);
27356
27588
  }
27357
27589
  views.clear();
27358
27590
  },
27359
- /** The wall's opening, if it has courses. */
27591
+ /** The wall has damage drawn. */
27360
27592
  has(partId) {
27361
27593
  return views.has(partId);
27362
27594
  },
27363
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
27595
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
27596
+ statistics: () => ({
27597
+ walls: views.size,
27598
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
27599
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
27600
+ })
27364
27601
  };
27365
27602
  }
27366
27603
  Object.assign(K, { createBreachView });