@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.
package/dist/index.js CHANGED
@@ -6251,6 +6251,19 @@ function restoreRange(mesh, v0, saved) {
6251
6251
  pos.array.set(saved, v0 * 3);
6252
6252
  pos.needsUpdate = true;
6253
6253
  }
6254
+ function noise2(x, y, cell) {
6255
+ const h = (i2, j2) => {
6256
+ const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
6257
+ return s - Math.floor(s);
6258
+ };
6259
+ const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
6260
+ const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
6261
+ 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;
6262
+ }
6263
+ var MASK_TEXEL = 0.02;
6264
+ var SPALL_MIN = 0.025;
6265
+ var SPALL_RAGGED = 0.045;
6266
+ var COVER = 0.015;
6254
6267
  function installDestructionBreach(K, _shared) {
6255
6268
  const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
6256
6269
  function createBreachView(opts) {
@@ -6260,55 +6273,29 @@ function installDestructionBreach(K, _shared) {
6260
6273
  const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
6261
6274
  const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
6262
6275
  const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
6263
- const wallColour = (items) => {
6264
- for (const it of items) {
6265
- if (!it.mesh) continue;
6266
- const c = it.mesh.geometry.attributes.color;
6267
- if (!c) continue;
6268
- const a = c.array;
6269
- tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
6270
- return tmp.c.getHex();
6271
- }
6272
- return null;
6273
- };
6274
6276
  const local = (p) => {
6275
6277
  house.updateMatrixWorld(true);
6276
6278
  const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
6277
6279
  return [v.x, v.y, v.z];
6278
6280
  };
6279
- function courses(part, units, opening, chips, paint) {
6281
+ function cells(part, units) {
6280
6282
  const [u0, v0, u1, v1] = part.aabb;
6281
6283
  const alongU = u1 - u0 >= v1 - v0;
6282
6284
  const len = alongU ? u1 - u0 : v1 - v0;
6283
- const thick = alongU ? v1 - v0 : u1 - u0;
6284
6285
  const [ul, uh] = units.size;
6285
6286
  const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
6286
6287
  const rowH = (part.y1 - part.y0) / rows;
6287
6288
  const per = Math.max(1, Math.round(len / ul));
6288
6289
  const w = len / per;
6289
- const edge = fragmentMaterial(part.material).color.getHex();
6290
- const list = [];
6290
+ const out = [];
6291
6291
  for (let r = 0; r < rows; r++) {
6292
6292
  const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
6293
- const cells = per + (shift ? 1 : 0);
6294
- for (let i = 0; i < cells; i++) {
6295
- let a = i * w - shift, b = a + w;
6296
- a = Math.max(0, a);
6297
- b = Math.min(len, b);
6298
- if (b - a < 1e-3) continue;
6299
- const mid = (a + b) / 2;
6300
- const x = alongU ? u0 + mid : (u0 + u1) / 2;
6301
- const z = alongU ? (v0 + v1) / 2 : v0 + mid;
6302
- const y = part.y0 + (r + 0.5) * rowH;
6303
- const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
6304
- if (opening && d < opening.radius) continue;
6305
- 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);
6306
- if (chips.some(gone)) continue;
6307
- const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
6308
- list.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
6293
+ for (let i = 0; i < per + (shift ? 1 : 0); i++) {
6294
+ const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
6295
+ if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
6309
6296
  }
6310
6297
  }
6311
- return list;
6298
+ return out;
6312
6299
  }
6313
6300
  const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
6314
6301
  const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
@@ -6322,6 +6309,137 @@ function installDestructionBreach(K, _shared) {
6322
6309
  return null;
6323
6310
  }
6324
6311
  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);
6312
+ function shell(it, mask) {
6313
+ const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
6314
+ const src = it.mesh.userData._p0 || g0.attributes.position.array;
6315
+ for (const [name, attr] of Object.entries(g0.attributes)) {
6316
+ const k = attr.itemSize;
6317
+ g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
6318
+ }
6319
+ if (g0.index) {
6320
+ const ix = g0.index.array.slice(it.i0, it.i1);
6321
+ for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
6322
+ g.setIndex(new THREE.BufferAttribute(ix, 1));
6323
+ }
6324
+ g.computeBoundingBox();
6325
+ g.computeBoundingSphere();
6326
+ const base = it.mesh.material;
6327
+ const material = base.clone();
6328
+ const toHouse = { value: new THREE.Matrix4() };
6329
+ material.onBeforeCompile = (sh) => {
6330
+ base.onBeforeCompile?.(sh);
6331
+ sh.uniforms.uToHouse = toHouse;
6332
+ sh.uniforms.uMask = { value: mask.texture };
6333
+ sh.uniforms.uMaskRect = { value: mask.rect };
6334
+ sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
6335
+ 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;");
6336
+ 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;");
6337
+ };
6338
+ material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
6339
+ const mesh = new THREE.Mesh(g, material);
6340
+ mesh.name = "breach-shell";
6341
+ mesh.userData.breachMask = mask;
6342
+ mesh.position.copy(it.mesh.position);
6343
+ mesh.quaternion.copy(it.mesh.quaternion);
6344
+ mesh.scale.copy(it.mesh.scale);
6345
+ mesh.castShadow = it.mesh.castShadow;
6346
+ mesh.receiveShadow = it.mesh.receiveShadow;
6347
+ mesh.renderOrder = it.mesh.renderOrder;
6348
+ mesh.onBeforeRender = () => {
6349
+ house.updateMatrixWorld();
6350
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh.matrixWorld);
6351
+ };
6352
+ it.mesh.parent.add(mesh);
6353
+ mesh.updateMatrixWorld(true);
6354
+ toHouse.value.copy(house.matrixWorld).invert().multiply(mesh.matrixWorld);
6355
+ return mesh;
6356
+ }
6357
+ function maskOf(hostId, parts) {
6358
+ const host = structure.byId.get(hostId);
6359
+ const [u0, v0, u1, v1] = host.aabb;
6360
+ const alongU = u1 - u0 >= v1 - v0;
6361
+ const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
6362
+ const len = alongU ? u1 - u0 : v1 - v0;
6363
+ 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)));
6364
+ const data = new Uint8Array(w * h);
6365
+ const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
6366
+ texture.magFilter = THREE.LinearFilter;
6367
+ texture.minFilter = THREE.LinearFilter;
6368
+ texture.needsUpdate = true;
6369
+ 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) };
6370
+ }
6371
+ const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
6372
+ function paint(view) {
6373
+ const { mask } = view;
6374
+ const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
6375
+ const holes = [];
6376
+ const discs = [];
6377
+ const shown = [];
6378
+ for (const id of view.parts) {
6379
+ const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id);
6380
+ const all = cells(part, units2);
6381
+ const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
6382
+ const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
6383
+ if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
6384
+ 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;
6385
+ const kept = [];
6386
+ for (const c of all) (gone(c) ? holes : kept).push(c);
6387
+ for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
6388
+ }
6389
+ const reach = SPALL_MIN + SPALL_RAGGED;
6390
+ mask.data.fill(0);
6391
+ const stamp = (s0, s1, y0, y1, inside) => {
6392
+ 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));
6393
+ 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));
6394
+ for (let j = j0; j <= j1; j++) {
6395
+ const y = mask.y0 + (j + 0.5) / mask.h * mask.height;
6396
+ for (let i = i0; i <= i1; i++) {
6397
+ const s = (i + 0.5) / mask.w * mask.len;
6398
+ const edge = SPALL_MIN + SPALL_RAGGED * (0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011));
6399
+ if (inside(s, y) <= edge) mask.data[j * mask.w + i] = 255;
6400
+ }
6401
+ }
6402
+ };
6403
+ 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));
6404
+ 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));
6405
+ mask.texture.needsUpdate = true;
6406
+ 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);
6407
+ const list = [];
6408
+ const tone = (hex, k) => tmp.c.setHex(hex).multiplyScalar(k).getHex();
6409
+ for (const { c, part, units: units2, thick, mid } of shown) {
6410
+ if (!near(c)) continue;
6411
+ 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;
6412
+ const at = (s, dy, d, colour) => list.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 });
6413
+ const base = fragmentMaterial(part.material).color.getHex();
6414
+ const n = noise2(centre * 3.1, y * 2.7, 0.07);
6415
+ if (units2.mortar === "concrete") {
6416
+ at(len, h, thick - 2 * COVER - 0.03 * n, tone(base, 0.86 + 0.2 * n));
6417
+ } else {
6418
+ at(len, h, thick - 2 * COVER - 0.012, 10327948);
6419
+ at(len - 0.012, h - 0.012, thick - 2 * COVER, tone(base, 0.8 + 0.35 * n));
6420
+ }
6421
+ }
6422
+ if (view.units) {
6423
+ view.units.removeFromParent();
6424
+ view.units.dispose();
6425
+ }
6426
+ const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list.length));
6427
+ units.name = "breach:" + view.host;
6428
+ list.forEach((u, i) => {
6429
+ tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
6430
+ units.setMatrixAt(i, tmp.m);
6431
+ units.setColorAt(i, tmp.c.setHex(u.colour));
6432
+ });
6433
+ units.count = list.length;
6434
+ units.instanceMatrix.needsUpdate = true;
6435
+ if (units.instanceColor) units.instanceColor.needsUpdate = true;
6436
+ units.castShadow = true;
6437
+ units.receiveShadow = true;
6438
+ units.visible = lod === 0;
6439
+ house.add(units);
6440
+ view.units = units;
6441
+ view.gone = holes.length;
6442
+ }
6325
6443
  function draw2(partId, change) {
6326
6444
  if (lod !== 0) return false;
6327
6445
  const hostId = hostOf(partId);
@@ -6331,37 +6449,21 @@ function installDestructionBreach(K, _shared) {
6331
6449
  const items = structure.meshesOf(hostId).filter((it) => it.mesh);
6332
6450
  if (!items.length) return false;
6333
6451
  const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
6334
- view = { mesh: null, saved: null, items, paint: wallColour(items), units: 0, opening: null, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
6452
+ if (!parts.includes(partId)) return false;
6453
+ const mask = maskOf(hostId, parts);
6454
+ view = { host: hostId, parts, items, mask, shells: [], saved: null, units: null, gone: 0, chips: new Map(parts.map((id) => [id, []])), openings: /* @__PURE__ */ new Map() };
6335
6455
  }
6336
6456
  if (!view.chips.has(partId)) return false;
6337
6457
  if (change.opening) {
6338
6458
  if (view.openings.has(partId)) return false;
6339
6459
  view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
6340
- if (partId === hostId) view.opening = view.openings.get(partId);
6341
6460
  }
6342
6461
  if (change.chip) view.chips.get(partId).push(local(change.chip));
6343
- const list = [];
6344
- for (const [id, chips] of view.chips) list.push(...courses(structure.byId.get(id), specOf(id).units, view.openings.get(id) ?? null, chips, view.paint));
6345
- if (view.mesh) {
6346
- view.mesh.removeFromParent();
6347
- view.mesh.dispose();
6462
+ if (!view.saved) {
6463
+ view.shells = view.items.map((it) => shell(it, view.mask));
6464
+ view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
6348
6465
  }
6349
- const mesh = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list.length));
6350
- mesh.name = "breach:" + hostId;
6351
- list.forEach((u, i) => {
6352
- tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
6353
- mesh.setMatrixAt(i, tmp.m);
6354
- mesh.setColorAt(i, tmp.c.setHex(u.colour));
6355
- });
6356
- mesh.count = list.length;
6357
- mesh.instanceMatrix.needsUpdate = true;
6358
- if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
6359
- mesh.castShadow = true;
6360
- mesh.receiveShadow = true;
6361
- house.add(mesh);
6362
- if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
6363
- view.mesh = mesh;
6364
- view.units = list.length;
6466
+ paint(view);
6365
6467
  views.set(hostId, view);
6366
6468
  invalidate();
6367
6469
  invalidateShadow();
@@ -6373,30 +6475,40 @@ function installDestructionBreach(K, _shared) {
6373
6475
  else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
6374
6476
  }
6375
6477
  }
6478
+ function dispose(view) {
6479
+ view.units.removeFromParent();
6480
+ view.units.dispose();
6481
+ for (const m of view.shells) {
6482
+ m.removeFromParent();
6483
+ m.geometry.dispose();
6484
+ m.material.dispose();
6485
+ }
6486
+ view.mask.texture.dispose();
6487
+ }
6376
6488
  return {
6377
- /** Handles a `stage` event of the house's object. Returns true when the wall was redrawn as courses. */
6489
+ /** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
6378
6490
  stage(event) {
6379
6491
  return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
6380
6492
  },
6381
- /** Handles a `chip` event: the unit that came out leaves a gap in the courses. Returns true when the wall was redrawn. */
6493
+ /** 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. */
6382
6494
  chip(event) {
6383
6495
  return draw2(event.part, { chip: event.center });
6384
6496
  },
6385
- /** The part broke or fell: the courses go (the plain range stays collapsed, the renderer hides the part). */
6497
+ /** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
6386
6498
  drop(partId) {
6387
6499
  const v = views.get(partId);
6388
6500
  if (!v) return;
6389
- v.mesh.removeFromParent();
6390
- v.mesh.dispose();
6501
+ dispose(v);
6391
6502
  views.delete(partId);
6392
6503
  },
6393
- /** LOD 0 draws the courses; LOD 1 and 2 give the wall its box back. */
6504
+ /** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
6394
6505
  setLod(level) {
6395
6506
  const was = lod;
6396
6507
  lod = level;
6397
6508
  if (was === 0 === (level === 0)) return;
6398
6509
  for (const v of views.values()) {
6399
- v.mesh.visible = level === 0;
6510
+ v.units.visible = level === 0;
6511
+ for (const m of v.shells) m.visible = level === 0;
6400
6512
  plain(v, level !== 0);
6401
6513
  }
6402
6514
  invalidate();
@@ -6405,17 +6517,21 @@ function installDestructionBreach(K, _shared) {
6405
6517
  /** Everything back to what it was (after `world.reset`). */
6406
6518
  restore() {
6407
6519
  for (const v of views.values()) {
6408
- v.mesh.removeFromParent();
6409
- v.mesh.dispose();
6520
+ dispose(v);
6410
6521
  if (lod === 0) plain(v, true);
6411
6522
  }
6412
6523
  views.clear();
6413
6524
  },
6414
- /** The wall's opening, if it has courses. */
6525
+ /** The wall has damage drawn. */
6415
6526
  has(partId) {
6416
6527
  return views.has(partId);
6417
6528
  },
6418
- statistics: () => ({ walls: views.size, units: [...views.values()].reduce((n, v) => n + v.units, 0) })
6529
+ /** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
6530
+ statistics: () => ({
6531
+ walls: views.size,
6532
+ gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
6533
+ units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
6534
+ })
6419
6535
  };
6420
6536
  }
6421
6537
  Object.assign(K, { createBreachView });