@adatechnology/game-floorplan-three 0.6.0 → 0.7.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.
@@ -12693,6 +12693,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12693
12693
  },
12694
12694
  hitDebris: "chips",
12695
12695
  breakDebris: "dust",
12696
+ visual: { innerColor: 10197652 },
12696
12697
  thermal: {
12697
12698
  conductivity: 0.15,
12698
12699
  heatCapacity: 2.5
@@ -12723,6 +12724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12723
12724
  ],
12724
12725
  hitDebris: "chips",
12725
12726
  breakDebris: "dust",
12727
+ visual: { innerColor: 12085567 },
12726
12728
  thermal: {
12727
12729
  conductivity: 0.15,
12728
12730
  heatCapacity: 2.5
@@ -12753,6 +12755,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12753
12755
  ],
12754
12756
  hitDebris: "dust",
12755
12757
  breakDebris: "dust",
12758
+ visual: { innerColor: 14999507 },
12756
12759
  thermal: {
12757
12760
  conductivity: 0.1,
12758
12761
  heatCapacity: 1.5
@@ -12778,6 +12781,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
12778
12781
  ],
12779
12782
  hitDebris: "chips",
12780
12783
  breakDebris: "dust",
12784
+ visual: { innerColor: 9407364 },
12781
12785
  thermal: {
12782
12786
  conductivity: 0.2,
12783
12787
  heatCapacity: 2.5
@@ -13535,6 +13539,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
13535
13539
  var CHIP_ENERGY = 200;
13536
13540
  var CHIP_SPLASH = 0.5;
13537
13541
  var BAR_BOW = 0.08;
13542
+ var BULLET_JOINT_SPAN = 0.3;
13538
13543
  var CUT_SLICE = 1;
13539
13544
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
13540
13545
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -14661,7 +14666,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14661
14666
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
14662
14667
  setStage(o, p, 1, {}, src);
14663
14668
  };
14664
- 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 });
14669
+ 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 } : {} });
14665
14670
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
14666
14671
  const detachEvents = (o, opened, by, src) => {
14667
14672
  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]) });
@@ -14702,7 +14707,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14702
14707
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
14703
14708
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
14704
14709
  };
14705
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
14710
+ const jointLength = (o, j) => {
14711
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
14712
+ let len = 0;
14713
+ 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]));
14714
+ return len;
14715
+ };
14716
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
14706
14717
  if (amount <= 0) return;
14707
14718
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
14708
14719
  if (split && mine.length) amount /= mine.length;
@@ -14710,7 +14721,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14710
14721
  if (j.st.open) continue;
14711
14722
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
14712
14723
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
14713
- j.st.hp -= amount;
14724
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
14714
14725
  if (j.st.hp <= 0) {
14715
14726
  j.st.hp = 0;
14716
14727
  j.st.open = true;
@@ -14739,7 +14750,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14739
14750
  if (spalls.length > MAX_SPALLS) spalls.shift();
14740
14751
  markDamaged(p);
14741
14752
  emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
14742
- debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
14753
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
14743
14754
  setStage(o, p, 1, {}, src);
14744
14755
  };
14745
14756
  const crater = (o, p, wp, normal, radius, src) => {
@@ -15226,7 +15237,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15226
15237
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15227
15238
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
15228
15239
  debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
15229
- wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
15240
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
15230
15241
  const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
15231
15242
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
15232
15243
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
@@ -19593,6 +19604,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19593
19604
  pos.array.set(saved, v0 * 3);
19594
19605
  pos.needsUpdate = true;
19595
19606
  }
19607
+ function noise2(x, y, cell) {
19608
+ const h = (i2, j2) => {
19609
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
19610
+ return s - Math.floor(s);
19611
+ };
19612
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
19613
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
19614
+ 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;
19615
+ }
19616
+ var MASK_TEXEL = 0.02;
19617
+ var SPALL_MIN = 0.025;
19618
+ var SPALL_RAGGED = 0.045;
19619
+ var COVER2 = 0.015;
19596
19620
  function installDestructionBreach(K, _shared) {
19597
19621
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
19598
19622
  function createBreachView(opts) {
@@ -19602,55 +19626,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19602
19626
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
19603
19627
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
19604
19628
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
19605
- const wallColour = (items) => {
19606
- for (const it of items) {
19607
- if (!it.mesh) continue;
19608
- const c = it.mesh.geometry.attributes.color;
19609
- if (!c) continue;
19610
- const a = c.array;
19611
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
19612
- return tmp.c.getHex();
19613
- }
19614
- return null;
19615
- };
19616
19629
  const local = (p) => {
19617
19630
  house.updateMatrixWorld(true);
19618
19631
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
19619
19632
  return [v.x, v.y, v.z];
19620
19633
  };
19621
- function courses(part, units, opening, chips, paint) {
19634
+ function cells(part, units) {
19622
19635
  const [u0, v0, u1, v1] = part.aabb;
19623
19636
  const alongU = u1 - u0 >= v1 - v0;
19624
19637
  const len = alongU ? u1 - u0 : v1 - v0;
19625
- const thick = alongU ? v1 - v0 : u1 - u0;
19626
19638
  const [ul, uh] = units.size;
19627
19639
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
19628
19640
  const rowH = (part.y1 - part.y0) / rows;
19629
19641
  const per = Math.max(1, Math.round(len / ul));
19630
19642
  const w = len / per;
19631
- const edge = fragmentMaterial(part.material).color.getHex();
19632
- const list2 = [];
19643
+ const out = [];
19633
19644
  for (let r = 0; r < rows; r++) {
19634
19645
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
19635
- const cells = per + (shift ? 1 : 0);
19636
- for (let i = 0; i < cells; i++) {
19637
- let a = i * w - shift, b = a + w;
19638
- a = Math.max(0, a);
19639
- b = Math.min(len, b);
19640
- if (b - a < 1e-3) continue;
19641
- const mid = (a + b) / 2;
19642
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
19643
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
19644
- const y = part.y0 + (r + 0.5) * rowH;
19645
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
19646
- if (opening && d < opening.radius) continue;
19647
- 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);
19648
- if (chips.some(gone)) continue;
19649
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
19650
- list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
19646
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
19647
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
19648
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
19651
19649
  }
19652
19650
  }
19653
- return list2;
19651
+ return out;
19654
19652
  }
19655
19653
  const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
19656
19654
  const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
@@ -19664,6 +19662,137 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19664
19662
  return null;
19665
19663
  }
19666
19664
  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);
19665
+ function shell(it, mask) {
19666
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
19667
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
19668
+ for (const [name, attr] of Object.entries(g0.attributes)) {
19669
+ const k = attr.itemSize;
19670
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
19671
+ }
19672
+ if (g0.index) {
19673
+ const ix = g0.index.array.slice(it.i0, it.i1);
19674
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
19675
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
19676
+ }
19677
+ g.computeBoundingBox();
19678
+ g.computeBoundingSphere();
19679
+ const base = it.mesh.material;
19680
+ const material = base.clone();
19681
+ const toHouse = { value: new THREE.Matrix4() };
19682
+ material.onBeforeCompile = (sh) => {
19683
+ base.onBeforeCompile?.(sh);
19684
+ sh.uniforms.uToHouse = toHouse;
19685
+ sh.uniforms.uMask = { value: mask.texture };
19686
+ sh.uniforms.uMaskRect = { value: mask.rect };
19687
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
19688
+ 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;");
19689
+ 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;");
19690
+ };
19691
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
19692
+ const mesh2 = new THREE.Mesh(g, material);
19693
+ mesh2.name = "breach-shell";
19694
+ mesh2.userData.breachMask = mask;
19695
+ mesh2.position.copy(it.mesh.position);
19696
+ mesh2.quaternion.copy(it.mesh.quaternion);
19697
+ mesh2.scale.copy(it.mesh.scale);
19698
+ mesh2.castShadow = it.mesh.castShadow;
19699
+ mesh2.receiveShadow = it.mesh.receiveShadow;
19700
+ mesh2.renderOrder = it.mesh.renderOrder;
19701
+ mesh2.onBeforeRender = () => {
19702
+ house.updateMatrixWorld();
19703
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19704
+ };
19705
+ it.mesh.parent.add(mesh2);
19706
+ mesh2.updateMatrixWorld(true);
19707
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
19708
+ return mesh2;
19709
+ }
19710
+ function maskOf(hostId, parts) {
19711
+ const host = structure.byId.get(hostId);
19712
+ const [u0, v0, u1, v1] = host.aabb;
19713
+ const alongU = u1 - u0 >= v1 - v0;
19714
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
19715
+ const len = alongU ? u1 - u0 : v1 - v0;
19716
+ 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)));
19717
+ const data = new Uint8Array(w * h);
19718
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
19719
+ texture.magFilter = THREE.LinearFilter;
19720
+ texture.minFilter = THREE.LinearFilter;
19721
+ texture.needsUpdate = true;
19722
+ 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) };
19723
+ }
19724
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
19725
+ function paint(view) {
19726
+ const { mask } = view;
19727
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
19728
+ const holes = [];
19729
+ const discs = [];
19730
+ const shown = [];
19731
+ for (const id of view.parts) {
19732
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
19733
+ const all = cells(part, units2);
19734
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
19735
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
19736
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
19737
+ 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;
19738
+ const kept = [];
19739
+ for (const c of all) (gone(c) ? holes : kept).push(c);
19740
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
19741
+ }
19742
+ const reach = SPALL_MIN + SPALL_RAGGED;
19743
+ mask.data.fill(0);
19744
+ const stamp = (s0, s1, y0, y1, inside2) => {
19745
+ 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));
19746
+ 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));
19747
+ for (let j = j0; j <= j1; j++) {
19748
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
19749
+ for (let i = i0; i <= i1; i++) {
19750
+ const s = (i + 0.5) / mask.w * mask.len;
19751
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
19752
+ if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
19753
+ }
19754
+ }
19755
+ };
19756
+ 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));
19757
+ 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));
19758
+ mask.texture.needsUpdate = true;
19759
+ 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);
19760
+ const list2 = [];
19761
+ const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
19762
+ for (const { c, part, units: units2, thick, mid } of shown) {
19763
+ if (!near(c)) continue;
19764
+ 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;
19765
+ 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 });
19766
+ const base = fragmentMaterial(part.material).color.getHex();
19767
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
19768
+ if (units2.mortar === "concrete") {
19769
+ at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
19770
+ } else {
19771
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
19772
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
19773
+ }
19774
+ }
19775
+ if (view.units) {
19776
+ view.units.removeFromParent();
19777
+ view.units.dispose();
19778
+ }
19779
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19780
+ units.name = "breach:" + view.host;
19781
+ list2.forEach((u, i) => {
19782
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19783
+ units.setMatrixAt(i, tmp.m);
19784
+ units.setColorAt(i, tmp.c.setHex(u.colour));
19785
+ });
19786
+ units.count = list2.length;
19787
+ units.instanceMatrix.needsUpdate = true;
19788
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
19789
+ units.castShadow = true;
19790
+ units.receiveShadow = true;
19791
+ units.visible = lod === 0;
19792
+ house.add(units);
19793
+ view.units = units;
19794
+ view.gone = holes.length;
19795
+ }
19667
19796
  function draw2(partId, change) {
19668
19797
  if (lod !== 0) return false;
19669
19798
  const hostId = hostOf(partId);
@@ -19673,37 +19802,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19673
19802
  const items = structure.meshesOf(hostId).filter((it) => it.mesh);
19674
19803
  if (!items.length) return false;
19675
19804
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
19676
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
19805
+ if (!parts.includes(partId)) return false;
19806
+ const mask = maskOf(hostId, parts);
19807
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
19677
19808
  }
19678
19809
  if (!view.chips.has(partId)) return false;
19679
19810
  if (change.opening) {
19680
19811
  if (view.openings.has(partId)) return false;
19681
19812
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
19682
- if (partId === hostId) view.opening = view.openings.get(partId);
19683
19813
  }
19684
19814
  if (change.chip) view.chips.get(partId).push(local(change.chip));
19685
- const list2 = [];
19686
- 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));
19687
- if (view.mesh) {
19688
- view.mesh.removeFromParent();
19689
- view.mesh.dispose();
19815
+ if (!view.saved) {
19816
+ view.shells = view.items.map((it) => shell(it, view.mask));
19817
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19690
19818
  }
19691
- const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
19692
- mesh2.name = "breach:" + hostId;
19693
- list2.forEach((u, i) => {
19694
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
19695
- mesh2.setMatrixAt(i, tmp.m);
19696
- mesh2.setColorAt(i, tmp.c.setHex(u.colour));
19697
- });
19698
- mesh2.count = list2.length;
19699
- mesh2.instanceMatrix.needsUpdate = true;
19700
- if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
19701
- mesh2.castShadow = true;
19702
- mesh2.receiveShadow = true;
19703
- house.add(mesh2);
19704
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
19705
- view.mesh = mesh2;
19706
- view.units = list2.length;
19819
+ paint(view);
19707
19820
  views.set(hostId, view);
19708
19821
  invalidate();
19709
19822
  invalidateShadow();
@@ -19715,30 +19828,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19715
19828
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
19716
19829
  }
19717
19830
  }
19831
+ function dispose(view) {
19832
+ view.units.removeFromParent();
19833
+ view.units.dispose();
19834
+ for (const m of view.shells) {
19835
+ m.removeFromParent();
19836
+ m.geometry.dispose();
19837
+ m.material.dispose();
19838
+ }
19839
+ view.mask.texture.dispose();
19840
+ }
19718
19841
  return {
19719
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
19842
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
19720
19843
  stage(event) {
19721
19844
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
19722
19845
  },
19723
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
19846
+ /** 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. */
19724
19847
  chip(event) {
19725
19848
  return draw2(event.part, { chip: event.center });
19726
19849
  },
19727
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
19850
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
19728
19851
  drop(partId) {
19729
19852
  const v = views.get(partId);
19730
19853
  if (!v) return;
19731
- v.mesh.removeFromParent();
19732
- v.mesh.dispose();
19854
+ dispose(v);
19733
19855
  views.delete(partId);
19734
19856
  },
19735
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
19857
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
19736
19858
  setLod(level) {
19737
19859
  const was = lod;
19738
19860
  lod = level;
19739
19861
  if (was === 0 === (level === 0)) return;
19740
19862
  for (const v of views.values()) {
19741
- v.mesh.visible = level === 0;
19863
+ v.units.visible = level === 0;
19864
+ for (const m of v.shells) m.visible = level === 0;
19742
19865
  plain2(v, level !== 0);
19743
19866
  }
19744
19867
  invalidate();
@@ -19747,17 +19870,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
19747
19870
  /** Everything back to what it was (after `world.reset`). */
19748
19871
  restore() {
19749
19872
  for (const v of views.values()) {
19750
- v.mesh.removeFromParent();
19751
- v.mesh.dispose();
19873
+ dispose(v);
19752
19874
  if (lod === 0) plain2(v, true);
19753
19875
  }
19754
19876
  views.clear();
19755
19877
  },
19756
- /** The wall's opening, if it has courses. */
19878
+ /** The wall has damage drawn. */
19757
19879
  has(partId) {
19758
19880
  return views.has(partId);
19759
19881
  },
19760
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
19882
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
19883
+ statistics: () => ({
19884
+ walls: views.size,
19885
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
19886
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
19887
+ })
19761
19888
  };
19762
19889
  }
19763
19890
  Object.assign(K, { createBreachView });