@adatechnology/game-items-three 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  }
@@ -14207,6 +14207,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14207
14207
  },
14208
14208
  hitDebris: "chips",
14209
14209
  breakDebris: "dust",
14210
+ visual: { innerColor: 10197652 },
14210
14211
  thermal: {
14211
14212
  conductivity: 0.15,
14212
14213
  heatCapacity: 2.5
@@ -14237,6 +14238,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14237
14238
  ],
14238
14239
  hitDebris: "chips",
14239
14240
  breakDebris: "dust",
14241
+ visual: { innerColor: 12085567 },
14240
14242
  thermal: {
14241
14243
  conductivity: 0.15,
14242
14244
  heatCapacity: 2.5
@@ -14267,6 +14269,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14267
14269
  ],
14268
14270
  hitDebris: "dust",
14269
14271
  breakDebris: "dust",
14272
+ visual: { innerColor: 14999507 },
14270
14273
  thermal: {
14271
14274
  conductivity: 0.1,
14272
14275
  heatCapacity: 1.5
@@ -14292,6 +14295,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
14292
14295
  ],
14293
14296
  hitDebris: "chips",
14294
14297
  breakDebris: "dust",
14298
+ visual: { innerColor: 9407364 },
14295
14299
  thermal: {
14296
14300
  conductivity: 0.2,
14297
14301
  heatCapacity: 2.5
@@ -15049,6 +15053,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
15049
15053
  var CHIP_ENERGY = 200;
15050
15054
  var CHIP_SPLASH = 0.5;
15051
15055
  var BAR_BOW = 0.08;
15056
+ var BULLET_JOINT_SPAN = 0.3;
15052
15057
  var CUT_SLICE = 1;
15053
15058
  var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
15054
15059
  var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
@@ -16175,7 +16180,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16175
16180
  emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
16176
16181
  setStage(o, p, 1, {}, src);
16177
16182
  };
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 });
16183
+ 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
16184
  const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
16180
16185
  const detachEvents = (o, opened, by, src) => {
16181
16186
  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 +16221,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16216
16221
  const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
16217
16222
  return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
16218
16223
  };
16219
- const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false) => {
16224
+ const jointLength = (o, j) => {
16225
+ const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
16226
+ let len = 0;
16227
+ 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]));
16228
+ return len;
16229
+ };
16230
+ const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
16220
16231
  if (amount <= 0) return;
16221
16232
  const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
16222
16233
  if (split && mine.length) amount /= mine.length;
@@ -16224,7 +16235,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16224
16235
  if (j.st.open) continue;
16225
16236
  if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
16226
16237
  if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
16227
- j.st.hp -= amount;
16238
+ j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
16228
16239
  if (j.st.hp <= 0) {
16229
16240
  j.st.hp = 0;
16230
16241
  j.st.open = true;
@@ -16253,7 +16264,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16253
16264
  if (spalls.length > MAX_SPALLS) spalls.shift();
16254
16265
  markDamaged(p);
16255
16266
  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);
16267
+ debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
16257
16268
  setStage(o, p, 1, {}, src);
16258
16269
  };
16259
16270
  const crater = (o, p, wp, normal, radius, src) => {
@@ -16740,7 +16751,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
16740
16751
  if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16741
16752
  } else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
16742
16753
  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);
16754
+ wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
16744
16755
  const absorbed = penetrated && mat.ductility >= 0.5 && mat.fracture !== "tear" ? Math.min(E, thr) : E;
16745
16756
  const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
16746
16757
  if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
@@ -27196,6 +27207,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27196
27207
  pos.array.set(saved, v0 * 3);
27197
27208
  pos.needsUpdate = true;
27198
27209
  }
27210
+ function noise2(x, y, cell) {
27211
+ const h = (i2, j2) => {
27212
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
27213
+ return s - Math.floor(s);
27214
+ };
27215
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
27216
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
27217
+ 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;
27218
+ }
27219
+ var MASK_TEXEL = 0.02;
27220
+ var SPALL_MIN = 0.025;
27221
+ var SPALL_RAGGED = 0.045;
27222
+ var COVER2 = 0.015;
27199
27223
  function installDestructionBreach(K, _shared) {
27200
27224
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
27201
27225
  function createBreachView(opts) {
@@ -27205,55 +27229,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27205
27229
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
27206
27230
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
27207
27231
  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
27232
  const local = (p) => {
27220
27233
  house.updateMatrixWorld(true);
27221
27234
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
27222
27235
  return [v.x, v.y, v.z];
27223
27236
  };
27224
- function courses(part, units, opening, chips, paint) {
27237
+ function cells(part, units) {
27225
27238
  const [u0, v0, u1, v1] = part.aabb;
27226
27239
  const alongU = u1 - u0 >= v1 - v0;
27227
27240
  const len = alongU ? u1 - u0 : v1 - v0;
27228
- const thick = alongU ? v1 - v0 : u1 - u0;
27229
27241
  const [ul, uh] = units.size;
27230
27242
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
27231
27243
  const rowH = (part.y1 - part.y0) / rows;
27232
27244
  const per = Math.max(1, Math.round(len / ul));
27233
27245
  const w = len / per;
27234
- const edge = fragmentMaterial(part.material).color.getHex();
27235
- const list2 = [];
27246
+ const out = [];
27236
27247
  for (let r = 0; r < rows; r++) {
27237
27248
  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 });
27249
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
27250
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
27251
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
27254
27252
  }
27255
27253
  }
27256
- return list2;
27254
+ return out;
27257
27255
  }
27258
27256
  const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
27259
27257
  const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
@@ -27267,6 +27265,137 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27267
27265
  return null;
27268
27266
  }
27269
27267
  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);
27268
+ function shell(it, mask) {
27269
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
27270
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
27271
+ for (const [name, attr] of Object.entries(g0.attributes)) {
27272
+ const k = attr.itemSize;
27273
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
27274
+ }
27275
+ if (g0.index) {
27276
+ const ix = g0.index.array.slice(it.i0, it.i1);
27277
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
27278
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
27279
+ }
27280
+ g.computeBoundingBox();
27281
+ g.computeBoundingSphere();
27282
+ const base = it.mesh.material;
27283
+ const material = base.clone();
27284
+ const toHouse = { value: new THREE.Matrix4() };
27285
+ material.onBeforeCompile = (sh) => {
27286
+ base.onBeforeCompile?.(sh);
27287
+ sh.uniforms.uToHouse = toHouse;
27288
+ sh.uniforms.uMask = { value: mask.texture };
27289
+ sh.uniforms.uMaskRect = { value: mask.rect };
27290
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
27291
+ 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;");
27292
+ 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;");
27293
+ };
27294
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
27295
+ const mesh2 = new THREE.Mesh(g, material);
27296
+ mesh2.name = "breach-shell";
27297
+ mesh2.userData.breachMask = mask;
27298
+ mesh2.position.copy(it.mesh.position);
27299
+ mesh2.quaternion.copy(it.mesh.quaternion);
27300
+ mesh2.scale.copy(it.mesh.scale);
27301
+ mesh2.castShadow = it.mesh.castShadow;
27302
+ mesh2.receiveShadow = it.mesh.receiveShadow;
27303
+ mesh2.renderOrder = it.mesh.renderOrder;
27304
+ mesh2.onBeforeRender = () => {
27305
+ house.updateMatrixWorld();
27306
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27307
+ };
27308
+ it.mesh.parent.add(mesh2);
27309
+ mesh2.updateMatrixWorld(true);
27310
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
27311
+ return mesh2;
27312
+ }
27313
+ function maskOf(hostId, parts) {
27314
+ const host = structure.byId.get(hostId);
27315
+ const [u0, v0, u1, v1] = host.aabb;
27316
+ const alongU = u1 - u0 >= v1 - v0;
27317
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
27318
+ const len = alongU ? u1 - u0 : v1 - v0;
27319
+ 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)));
27320
+ const data = new Uint8Array(w * h);
27321
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
27322
+ texture.magFilter = THREE.LinearFilter;
27323
+ texture.minFilter = THREE.LinearFilter;
27324
+ texture.needsUpdate = true;
27325
+ return { data, texture, w, h, alongU, start: alongU ? u0 : v0, len, y0, height: y1 - y0, rect: new THREE.Vector4(alongU ? u0 : v0, y0, len, y1 - y0) };
27326
+ }
27327
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
27328
+ function paint(view) {
27329
+ const { mask } = view;
27330
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
27331
+ const holes = [];
27332
+ const discs = [];
27333
+ const shown = [];
27334
+ for (const id of view.parts) {
27335
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
27336
+ const all = cells(part, units2);
27337
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
27338
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
27339
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
27340
+ const gone = (c) => chips.some((p) => p[1] >= c.y0 && p[1] <= c.y1 && along(p) >= c.a && along(p) <= c.b) || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
27341
+ const kept = [];
27342
+ for (const c of all) (gone(c) ? holes : kept).push(c);
27343
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
27344
+ }
27345
+ const reach = SPALL_MIN + SPALL_RAGGED;
27346
+ mask.data.fill(0);
27347
+ const stamp = (s0, s1, y0, y1, inside2) => {
27348
+ const i0 = Math.max(0, Math.floor((s0 - reach) / mask.len * mask.w)), i1 = Math.min(mask.w - 1, Math.ceil((s1 + reach) / mask.len * mask.w));
27349
+ const j0 = Math.max(0, Math.floor((y0 - reach - mask.y0) / mask.height * mask.h)), j1 = Math.min(mask.h - 1, Math.ceil((y1 + reach - mask.y0) / mask.height * mask.h));
27350
+ for (let j = j0; j <= j1; j++) {
27351
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
27352
+ for (let i = i0; i <= i1; i++) {
27353
+ const s = (i + 0.5) / mask.w * mask.len;
27354
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
27355
+ if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
27356
+ }
27357
+ }
27358
+ };
27359
+ for (const c of holes) stamp(c.a, c.b, c.y0, c.y1, (s, y) => boxDistance(s, y, c.a, c.b, c.y0, c.y1));
27360
+ for (const d of discs) stamp(d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r));
27361
+ mask.texture.needsUpdate = true;
27362
+ const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach);
27363
+ const list2 = [];
27364
+ const tone = (hex3, k) => tmp.c.setHex(hex3).multiplyScalar(k).getHex();
27365
+ for (const { c, part, units: units2, thick, mid } of shown) {
27366
+ if (!near(c)) continue;
27367
+ 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;
27368
+ 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 });
27369
+ const base = fragmentMaterial(part.material).color.getHex();
27370
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
27371
+ if (units2.mortar === "concrete") {
27372
+ at(len, h, thick - 2 * COVER2 - 0.03 * n, tone(base, 0.86 + 0.2 * n));
27373
+ } else {
27374
+ at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
27375
+ at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
27376
+ }
27377
+ }
27378
+ if (view.units) {
27379
+ view.units.removeFromParent();
27380
+ view.units.dispose();
27381
+ }
27382
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
27383
+ units.name = "breach:" + view.host;
27384
+ list2.forEach((u, i) => {
27385
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
27386
+ units.setMatrixAt(i, tmp.m);
27387
+ units.setColorAt(i, tmp.c.setHex(u.colour));
27388
+ });
27389
+ units.count = list2.length;
27390
+ units.instanceMatrix.needsUpdate = true;
27391
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
27392
+ units.castShadow = true;
27393
+ units.receiveShadow = true;
27394
+ units.visible = lod === 0;
27395
+ house.add(units);
27396
+ view.units = units;
27397
+ view.gone = holes.length;
27398
+ }
27270
27399
  function draw2(partId, change) {
27271
27400
  if (lod !== 0) return false;
27272
27401
  const hostId = hostOf(partId);
@@ -27276,37 +27405,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27276
27405
  const items = structure.meshesOf(hostId).filter((it) => it.mesh);
27277
27406
  if (!items.length) return false;
27278
27407
  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() };
27408
+ if (!parts.includes(partId)) return false;
27409
+ const mask = maskOf(hostId, parts);
27410
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
27280
27411
  }
27281
27412
  if (!view.chips.has(partId)) return false;
27282
27413
  if (change.opening) {
27283
27414
  if (view.openings.has(partId)) return false;
27284
27415
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
27285
- if (partId === hostId) view.opening = view.openings.get(partId);
27286
27416
  }
27287
27417
  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();
27418
+ if (!view.saved) {
27419
+ view.shells = view.items.map((it) => shell(it, view.mask));
27420
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
27293
27421
  }
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;
27422
+ paint(view);
27310
27423
  views.set(hostId, view);
27311
27424
  invalidate();
27312
27425
  invalidateShadow();
@@ -27318,30 +27431,40 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27318
27431
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
27319
27432
  }
27320
27433
  }
27434
+ function dispose(view) {
27435
+ view.units.removeFromParent();
27436
+ view.units.dispose();
27437
+ for (const m of view.shells) {
27438
+ m.removeFromParent();
27439
+ m.geometry.dispose();
27440
+ m.material.dispose();
27441
+ }
27442
+ view.mask.texture.dispose();
27443
+ }
27321
27444
  return {
27322
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
27445
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
27323
27446
  stage(event) {
27324
27447
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
27325
27448
  },
27326
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
27449
+ /** Handles a `chip` event: the unit that came out leaves a hole in the wall where it was. Returns true when the wall was cut. */
27327
27450
  chip(event) {
27328
27451
  return draw2(event.part, { chip: event.center });
27329
27452
  },
27330
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
27453
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
27331
27454
  drop(partId) {
27332
27455
  const v = views.get(partId);
27333
27456
  if (!v) return;
27334
- v.mesh.removeFromParent();
27335
- v.mesh.dispose();
27457
+ dispose(v);
27336
27458
  views.delete(partId);
27337
27459
  },
27338
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
27460
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
27339
27461
  setLod(level) {
27340
27462
  const was = lod;
27341
27463
  lod = level;
27342
27464
  if (was === 0 === (level === 0)) return;
27343
27465
  for (const v of views.values()) {
27344
- v.mesh.visible = level === 0;
27466
+ v.units.visible = level === 0;
27467
+ for (const m of v.shells) m.visible = level === 0;
27345
27468
  plain2(v, level !== 0);
27346
27469
  }
27347
27470
  invalidate();
@@ -27350,17 +27473,21 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
27350
27473
  /** Everything back to what it was (after `world.reset`). */
27351
27474
  restore() {
27352
27475
  for (const v of views.values()) {
27353
- v.mesh.removeFromParent();
27354
- v.mesh.dispose();
27476
+ dispose(v);
27355
27477
  if (lod === 0) plain2(v, true);
27356
27478
  }
27357
27479
  views.clear();
27358
27480
  },
27359
- /** The wall's opening, if it has courses. */
27481
+ /** The wall has damage drawn. */
27360
27482
  has(partId) {
27361
27483
  return views.has(partId);
27362
27484
  },
27363
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
27485
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
27486
+ statistics: () => ({
27487
+ walls: views.size,
27488
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
27489
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
27490
+ })
27364
27491
  };
27365
27492
  }
27366
27493
  Object.assign(K, { createBreachView });