@adatechnology/game-floorplan-three 0.7.0 → 0.8.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.
- package/dist/index.d.ts +5 -1
- package/dist/index.global.js +411 -50
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +251 -32
- package/dist/index.js.map +1 -1
- package/package.json +20 -21
package/dist/index.js
CHANGED
|
@@ -6236,6 +6236,10 @@ function installDestructionPieces(K, shared) {
|
|
|
6236
6236
|
function collapseRange(mesh, v0, v1) {
|
|
6237
6237
|
const pos = mesh.geometry.attributes.position;
|
|
6238
6238
|
const a = pos.array;
|
|
6239
|
+
const u = mesh.userData;
|
|
6240
|
+
if (!u._p0) u._p0 = a.slice();
|
|
6241
|
+
const list = u.collapsedRanges ?? (u.collapsedRanges = []);
|
|
6242
|
+
if (!list.some((r) => r[0] === v0 && r[1] === v1)) list.push([v0, v1]);
|
|
6239
6243
|
const saved = a.slice(v0 * 3, v1 * 3);
|
|
6240
6244
|
const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
|
|
6241
6245
|
for (let k = v0; k < v1; k++) {
|
|
@@ -6250,6 +6254,8 @@ function restoreRange(mesh, v0, saved) {
|
|
|
6250
6254
|
const pos = mesh.geometry.attributes.position;
|
|
6251
6255
|
pos.array.set(saved, v0 * 3);
|
|
6252
6256
|
pos.needsUpdate = true;
|
|
6257
|
+
const list = mesh.userData.collapsedRanges;
|
|
6258
|
+
if (list) mesh.userData.collapsedRanges = list.filter((r) => r[0] !== v0);
|
|
6253
6259
|
}
|
|
6254
6260
|
function noise2(x, y, cell) {
|
|
6255
6261
|
const h = (i2, j2) => {
|
|
@@ -6264,13 +6270,22 @@ var MASK_TEXEL = 0.02;
|
|
|
6264
6270
|
var SPALL_MIN = 0.025;
|
|
6265
6271
|
var SPALL_RAGGED = 0.045;
|
|
6266
6272
|
var COVER = 0.015;
|
|
6273
|
+
var MAX_FLAKES = 64;
|
|
6274
|
+
function clipToBox(unit, box) {
|
|
6275
|
+
const lo = [Math.max(unit.x - unit.sx / 2, box[0]), Math.max(unit.y - unit.sy / 2, box[1]), Math.max(unit.z - unit.sz / 2, box[2])];
|
|
6276
|
+
const hi = [Math.min(unit.x + unit.sx / 2, box[3]), Math.min(unit.y + unit.sy / 2, box[4]), Math.min(unit.z + unit.sz / 2, box[5])];
|
|
6277
|
+
if (hi[0] - lo[0] <= 1e-4 || hi[1] - lo[1] <= 1e-4 || hi[2] - lo[2] <= 1e-4) return null;
|
|
6278
|
+
return { x: (lo[0] + hi[0]) / 2, y: (lo[1] + hi[1]) / 2, z: (lo[2] + hi[2]) / 2, sx: hi[0] - lo[0], sy: hi[1] - lo[1], sz: hi[2] - lo[2], colour: unit.colour };
|
|
6279
|
+
}
|
|
6267
6280
|
function installDestructionBreach(K, _shared) {
|
|
6268
6281
|
const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
|
|
6269
6282
|
function createBreachView(opts) {
|
|
6270
6283
|
const { house, structure } = opts;
|
|
6271
6284
|
const views = /* @__PURE__ */ new Map();
|
|
6285
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
6286
|
+
const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
|
|
6287
|
+
const dropped = [];
|
|
6272
6288
|
let lod = 0;
|
|
6273
|
-
const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
|
|
6274
6289
|
const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
6275
6290
|
const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
|
|
6276
6291
|
const local = (p) => {
|
|
@@ -6339,6 +6354,11 @@ function installDestructionBreach(K, _shared) {
|
|
|
6339
6354
|
const mesh = new THREE.Mesh(g, material);
|
|
6340
6355
|
mesh.name = "breach-shell";
|
|
6341
6356
|
mesh.userData.breachMask = mask;
|
|
6357
|
+
mesh.userData.cutAt = (p) => {
|
|
6358
|
+
const v = local([p.x, p.y, p.z]);
|
|
6359
|
+
const i = Math.floor(((mask.alongU ? v[0] : v[2]) - mask.start) / mask.len * mask.w), j = Math.floor((v[1] - mask.y0) / mask.height * mask.h);
|
|
6360
|
+
return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
|
|
6361
|
+
};
|
|
6342
6362
|
mesh.position.copy(it.mesh.position);
|
|
6343
6363
|
mesh.quaternion.copy(it.mesh.quaternion);
|
|
6344
6364
|
mesh.scale.copy(it.mesh.scale);
|
|
@@ -6354,6 +6374,29 @@ function installDestructionBreach(K, _shared) {
|
|
|
6354
6374
|
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh.matrixWorld);
|
|
6355
6375
|
return mesh;
|
|
6356
6376
|
}
|
|
6377
|
+
function unitsMaterial(core) {
|
|
6378
|
+
const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
|
|
6379
|
+
material.onBeforeCompile = (sh) => {
|
|
6380
|
+
sh.uniforms.uCore = { value: core.texture };
|
|
6381
|
+
sh.uniforms.uCoreRect = { value: core.rect };
|
|
6382
|
+
sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
|
|
6383
|
+
sh.vertexShader = sh.vertexShader.replace("void main() {", "varying vec3 vHousePos;\nvoid main() {").replace("#include <begin_vertex>", "#include <begin_vertex>\n#ifdef USE_INSTANCING\nvHousePos = (instanceMatrix * vec4(transformed, 1.0)).xyz;\n#else\nvHousePos = transformed;\n#endif");
|
|
6384
|
+
sh.fragmentShader = sh.fragmentShader.replace("void main() {", "uniform sampler2D uCore;\nuniform vec4 uCoreRect;\nuniform float uCoreAlongX;\nvarying vec3 vHousePos;\nvoid main() {\n vec2 cUv = vec2(((uCoreAlongX > 0.5 ? vHousePos.x : vHousePos.z) - uCoreRect.x) / uCoreRect.z, (vHousePos.y - uCoreRect.y) / uCoreRect.w);\n if (cUv.x >= 0.0 && cUv.x <= 1.0 && cUv.y >= 0.0 && cUv.y <= 1.0 && texture2D(uCore, cUv).r > 0.5) discard;").replace("#include <color_fragment>", "#include <color_fragment>\nif (!gl_FrontFacing) diffuseColor.rgb *= 0.55;");
|
|
6385
|
+
};
|
|
6386
|
+
material.customProgramCacheKey = () => "breachCore";
|
|
6387
|
+
material.userData.breachCore = core;
|
|
6388
|
+
return material;
|
|
6389
|
+
}
|
|
6390
|
+
function drawnBounds(items) {
|
|
6391
|
+
house.updateMatrixWorld(true);
|
|
6392
|
+
const box = new THREE.Box3(), toHouse = new THREE.Matrix4().copy(house.matrixWorld).invert();
|
|
6393
|
+
for (const it of items) {
|
|
6394
|
+
const a = it.mesh.userData._p0 || it.mesh.geometry.attributes.position.array;
|
|
6395
|
+
tmp.m.multiplyMatrices(toHouse, it.mesh.matrixWorld);
|
|
6396
|
+
for (let k = it.v0; k < it.v1; k++) box.expandByPoint(tmp.p.set(a[k * 3], a[k * 3 + 1], a[k * 3 + 2]).applyMatrix4(tmp.m));
|
|
6397
|
+
}
|
|
6398
|
+
return [box.min.x, box.min.y, box.min.z, box.max.x, box.max.y, box.max.z];
|
|
6399
|
+
}
|
|
6357
6400
|
function maskOf(hostId, parts) {
|
|
6358
6401
|
const host = structure.byId.get(hostId);
|
|
6359
6402
|
const [u0, v0, u1, v1] = host.aabb;
|
|
@@ -6369,51 +6412,86 @@ function installDestructionBreach(K, _shared) {
|
|
|
6369
6412
|
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
6413
|
}
|
|
6371
6414
|
const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
|
|
6415
|
+
const ellipseDistance = (s, y, cs, cy, rs, ry) => (Math.hypot((s - cs) / Math.max(rs, 1e-4), (y - cy) / Math.max(ry, 1e-4)) - 1) * Math.min(rs, ry);
|
|
6416
|
+
function covered(c, boxes) {
|
|
6417
|
+
const n = 8;
|
|
6418
|
+
let hit = 0;
|
|
6419
|
+
for (let j = 0; j < n; j++) {
|
|
6420
|
+
const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
|
|
6421
|
+
for (let i = 0; i < n; i++) {
|
|
6422
|
+
const s = c.a + (i + 0.5) / n * (c.b - c.a);
|
|
6423
|
+
if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
|
|
6424
|
+
}
|
|
6425
|
+
}
|
|
6426
|
+
return hit / (n * n);
|
|
6427
|
+
}
|
|
6372
6428
|
function paint(view) {
|
|
6373
|
-
const { mask } = view;
|
|
6429
|
+
const { mask, core } = view;
|
|
6374
6430
|
const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
|
|
6375
6431
|
const holes = [];
|
|
6432
|
+
const pits = [];
|
|
6376
6433
|
const discs = [];
|
|
6434
|
+
const flakes = [];
|
|
6377
6435
|
const shown = [];
|
|
6378
6436
|
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);
|
|
6437
|
+
const part = structure.byId.get(id), units2 = specOf(id).units, chips = view.chips.get(id), opening = view.openings.get(id), spalls = view.spalls.get(id);
|
|
6380
6438
|
const all = cells(part, units2);
|
|
6381
6439
|
const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
|
|
6382
6440
|
const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
|
|
6383
6441
|
if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
|
|
6384
|
-
|
|
6442
|
+
for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
|
|
6443
|
+
const boxes = chips.map((k) => ({ s0: along(k.center) - k.along, s1: along(k.center) + k.along, y0: k.center[1] - k.up, y1: k.center[1] + k.up }));
|
|
6444
|
+
const gone = (c) => covered(c, boxes.filter((k) => k.s1 > c.a && k.s0 < c.b && k.y1 > c.y0 && k.y0 < c.y1)) >= 0.9 || opening && Math.hypot((c.a + c.b) / 2 - along(opening.center), (c.y0 + c.y1) / 2 - opening.center[1]) < opening.radius;
|
|
6385
6445
|
const kept = [];
|
|
6386
6446
|
for (const c of all) (gone(c) ? holes : kept).push(c);
|
|
6387
6447
|
for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
|
|
6448
|
+
for (const k of boxes) {
|
|
6449
|
+
if (holes.some((h) => k.s0 >= h.a - 1e-4 && k.s1 <= h.b + 1e-4 && k.y0 >= h.y0 - 1e-4 && k.y1 <= h.y1 + 1e-4)) continue;
|
|
6450
|
+
pits.push({ s: (k.s0 + k.s1) / 2, y: (k.y0 + k.y1) / 2, rs: (k.s1 - k.s0) / 2, ry: (k.y1 - k.y0) / 2 });
|
|
6451
|
+
}
|
|
6388
6452
|
}
|
|
6389
6453
|
const reach = SPALL_MIN + SPALL_RAGGED;
|
|
6454
|
+
const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
|
|
6390
6455
|
mask.data.fill(0);
|
|
6391
|
-
|
|
6392
|
-
|
|
6393
|
-
const
|
|
6456
|
+
core.data.fill(0);
|
|
6457
|
+
const stamp = (m, s0, s1, y0, y1, inside) => {
|
|
6458
|
+
const i0 = Math.max(0, Math.floor((s0 - reach) / m.len * m.w)), i1 = Math.min(m.w - 1, Math.ceil((s1 + reach) / m.len * m.w));
|
|
6459
|
+
const j0 = Math.max(0, Math.floor((y0 - reach - m.y0) / m.height * m.h)), j1 = Math.min(m.h - 1, Math.ceil((y1 + reach - m.y0) / m.height * m.h));
|
|
6394
6460
|
for (let j = j0; j <= j1; j++) {
|
|
6395
|
-
const y =
|
|
6461
|
+
const y = m.y0 + (j + 0.5) / m.h * m.height;
|
|
6396
6462
|
for (let i = i0; i <= i1; i++) {
|
|
6397
|
-
const s = (i + 0.5) /
|
|
6398
|
-
|
|
6399
|
-
if (inside(s, y) <= edge) mask.data[j * mask.w + i] = 255;
|
|
6463
|
+
const s = (i + 0.5) / m.w * m.len;
|
|
6464
|
+
if (inside(s, y)) m.data[j * m.w + i] = 255;
|
|
6400
6465
|
}
|
|
6401
6466
|
}
|
|
6402
6467
|
};
|
|
6403
|
-
|
|
6404
|
-
for (const
|
|
6468
|
+
const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
|
|
6469
|
+
for (const c of holes) stamp(mask, c.a, c.b, c.y0, c.y1, (s, y) => plaster(boxDistance(s, y, c.a, c.b, c.y0, c.y1), s, y));
|
|
6470
|
+
for (const d of discs) stamp(mask, d.s - d.r, d.s + d.r, d.y - d.r, d.y + d.r, (s, y) => plaster(Math.max(0, Math.hypot(s - d.s, y - d.y) - d.r), s, y));
|
|
6471
|
+
for (const f of flakes) {
|
|
6472
|
+
const rough = (s, y) => Math.hypot(s - f.s, y - f.y) - f.r * (0.65 + 0.7 * ragged(s * 2.3, y * 2.3));
|
|
6473
|
+
stamp(mask, f.s - 2 * f.r, f.s + 2 * f.r, f.y - 2 * f.r, f.y + 2 * f.r, (s, y) => rough(s, y) <= 0);
|
|
6474
|
+
}
|
|
6475
|
+
for (const p of pits) {
|
|
6476
|
+
stamp(mask, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => plaster(Math.max(0, ellipseDistance(s, y, p.s, p.y, p.rs, p.ry)), s, y));
|
|
6477
|
+
stamp(core, p.s - p.rs, p.s + p.rs, p.y - p.ry, p.y + p.ry, (s, y) => ellipseDistance(s, y, p.s, p.y, p.rs, p.ry) <= -0.35 * Math.min(p.rs, p.ry) * noise2(s * 1.7, y * 1.3, 0.012));
|
|
6478
|
+
}
|
|
6405
6479
|
mask.texture.needsUpdate = true;
|
|
6406
|
-
|
|
6480
|
+
core.texture.needsUpdate = true;
|
|
6481
|
+
const near = (c) => holes.some((h) => boxDistance((c.a + c.b) / 2, (c.y0 + c.y1) / 2, h.a - (c.b - c.a) / 2, h.b + (c.b - c.a) / 2, h.y0 - (c.y1 - c.y0) / 2, h.y1 + (c.y1 - c.y0) / 2) <= reach) || discs.some((d) => boxDistance(d.s, d.y, c.a, c.b, c.y0, c.y1) <= d.r + reach) || pits.some((p) => boxDistance(p.s, p.y, c.a, c.b, c.y0, c.y1) <= Math.max(p.rs, p.ry) + reach) || flakes.some((f) => boxDistance(f.s, f.y, c.a, c.b, c.y0, c.y1) <= 2 * f.r);
|
|
6407
6482
|
const list = [];
|
|
6408
6483
|
const tone = (hex, k) => tmp.c.setHex(hex).multiplyScalar(k).getHex();
|
|
6409
6484
|
for (const { c, part, units: units2, thick, mid } of shown) {
|
|
6410
6485
|
if (!near(c)) continue;
|
|
6411
6486
|
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) =>
|
|
6487
|
+
const at = (s, dy, d, colour) => {
|
|
6488
|
+
const unit = clipToBox({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour }, view.bounds);
|
|
6489
|
+
if (unit) list.push(unit);
|
|
6490
|
+
};
|
|
6413
6491
|
const base = fragmentMaterial(part.material).color.getHex();
|
|
6414
6492
|
const n = noise2(centre * 3.1, y * 2.7, 0.07);
|
|
6415
6493
|
if (units2.mortar === "concrete") {
|
|
6416
|
-
at(len, h, thick - 2 * COVER
|
|
6494
|
+
at(len, h, thick - 2 * COVER, tone(base, 0.9 + 0.04 * n));
|
|
6417
6495
|
} else {
|
|
6418
6496
|
at(len, h, thick - 2 * COVER - 0.012, 10327948);
|
|
6419
6497
|
at(len - 0.012, h - 0.012, thick - 2 * COVER, tone(base, 0.8 + 0.35 * n));
|
|
@@ -6423,7 +6501,7 @@ function installDestructionBreach(K, _shared) {
|
|
|
6423
6501
|
view.units.removeFromParent();
|
|
6424
6502
|
view.units.dispose();
|
|
6425
6503
|
}
|
|
6426
|
-
const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list.length));
|
|
6504
|
+
const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list.length));
|
|
6427
6505
|
units.name = "breach:" + view.host;
|
|
6428
6506
|
list.forEach((u, i) => {
|
|
6429
6507
|
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
@@ -6439,6 +6517,19 @@ function installDestructionBreach(K, _shared) {
|
|
|
6439
6517
|
house.add(units);
|
|
6440
6518
|
view.units = units;
|
|
6441
6519
|
view.gone = holes.length;
|
|
6520
|
+
view.pits = pits.length;
|
|
6521
|
+
view.flakes = flakes.length;
|
|
6522
|
+
}
|
|
6523
|
+
function chipBox(chip, alongU) {
|
|
6524
|
+
house.updateMatrixWorld(true);
|
|
6525
|
+
const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
|
|
6526
|
+
let a = 0, u = 0;
|
|
6527
|
+
for (let k = 0; k < 3; k++) {
|
|
6528
|
+
const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
|
|
6529
|
+
a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
|
|
6530
|
+
u += Math.abs(axis.y) * chip.size[k] / 2;
|
|
6531
|
+
}
|
|
6532
|
+
return { center: local(chip.center), along: a, up: u };
|
|
6442
6533
|
}
|
|
6443
6534
|
function draw2(partId, change) {
|
|
6444
6535
|
if (lod !== 0) return false;
|
|
@@ -6450,23 +6541,50 @@ function installDestructionBreach(K, _shared) {
|
|
|
6450
6541
|
if (!items.length) return false;
|
|
6451
6542
|
const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
|
|
6452
6543
|
if (!parts.includes(partId)) return false;
|
|
6453
|
-
const mask = maskOf(hostId, parts);
|
|
6454
|
-
view = {
|
|
6544
|
+
const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
|
|
6545
|
+
view = {
|
|
6546
|
+
host: hostId,
|
|
6547
|
+
parts,
|
|
6548
|
+
items,
|
|
6549
|
+
mask,
|
|
6550
|
+
core,
|
|
6551
|
+
unitMaterial: unitsMaterial(core),
|
|
6552
|
+
shells: [],
|
|
6553
|
+
saved: null,
|
|
6554
|
+
units: null,
|
|
6555
|
+
gone: 0,
|
|
6556
|
+
pits: 0,
|
|
6557
|
+
bounds: drawnBounds(items),
|
|
6558
|
+
chips: new Map(parts.map((id) => [id, []])),
|
|
6559
|
+
spalls: new Map(parts.map((id) => [id, []])),
|
|
6560
|
+
openings: /* @__PURE__ */ new Map(),
|
|
6561
|
+
flakes: 0
|
|
6562
|
+
};
|
|
6455
6563
|
}
|
|
6456
6564
|
if (!view.chips.has(partId)) return false;
|
|
6457
6565
|
if (change.opening) {
|
|
6458
6566
|
if (view.openings.has(partId)) return false;
|
|
6459
6567
|
view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
|
|
6460
6568
|
}
|
|
6461
|
-
if (change.chip) view.chips.get(partId).push(
|
|
6569
|
+
if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
|
|
6570
|
+
if (change.spall) {
|
|
6571
|
+
const list = view.spalls.get(partId);
|
|
6572
|
+
list.push({ center: local(change.spall.center), radius: change.spall.radius });
|
|
6573
|
+
if (list.length > MAX_FLAKES) list.shift();
|
|
6574
|
+
}
|
|
6462
6575
|
if (!view.saved) {
|
|
6463
6576
|
view.shells = view.items.map((it) => shell(it, view.mask));
|
|
6577
|
+
for (const m of view.shells) m.userData.behind = () => view.units;
|
|
6464
6578
|
view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
6465
6579
|
}
|
|
6466
|
-
paint(view);
|
|
6467
6580
|
views.set(hostId, view);
|
|
6468
|
-
|
|
6469
|
-
|
|
6581
|
+
if (opts.deferPaint && change.spall) dirty.add(view);
|
|
6582
|
+
else {
|
|
6583
|
+
dirty.delete(view);
|
|
6584
|
+
paint(view);
|
|
6585
|
+
invalidate();
|
|
6586
|
+
invalidateShadow();
|
|
6587
|
+
}
|
|
6470
6588
|
return true;
|
|
6471
6589
|
}
|
|
6472
6590
|
function plain(view, shown) {
|
|
@@ -6475,24 +6593,70 @@ function installDestructionBreach(K, _shared) {
|
|
|
6475
6593
|
else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
|
|
6476
6594
|
}
|
|
6477
6595
|
}
|
|
6596
|
+
function flush(budgetMs = paintBudgetMs) {
|
|
6597
|
+
if (dirty.size === 0) return 0;
|
|
6598
|
+
const deadline = performance.now() + budgetMs;
|
|
6599
|
+
for (const view of dirty) {
|
|
6600
|
+
dirty.delete(view);
|
|
6601
|
+
paint(view);
|
|
6602
|
+
if (performance.now() >= deadline) break;
|
|
6603
|
+
}
|
|
6604
|
+
invalidate();
|
|
6605
|
+
invalidateShadow();
|
|
6606
|
+
return dirty.size;
|
|
6607
|
+
}
|
|
6478
6608
|
function dispose(view) {
|
|
6479
|
-
|
|
6480
|
-
view.units
|
|
6609
|
+
dirty.delete(view);
|
|
6610
|
+
if (view.units) {
|
|
6611
|
+
view.units.removeFromParent();
|
|
6612
|
+
view.units.dispose();
|
|
6613
|
+
}
|
|
6614
|
+
view.unitMaterial.dispose();
|
|
6481
6615
|
for (const m of view.shells) {
|
|
6482
6616
|
m.removeFromParent();
|
|
6483
6617
|
m.geometry.dispose();
|
|
6484
6618
|
m.material.dispose();
|
|
6485
6619
|
}
|
|
6486
6620
|
view.mask.texture.dispose();
|
|
6621
|
+
view.core.texture.dispose();
|
|
6487
6622
|
}
|
|
6488
6623
|
return {
|
|
6489
6624
|
/** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
|
|
6490
6625
|
stage(event) {
|
|
6491
6626
|
return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
|
|
6492
6627
|
},
|
|
6493
|
-
/**
|
|
6628
|
+
/**
|
|
6629
|
+
* Handles a `spall` of a wall of units: the coat comes off in a ragged patch and the units show under it, drawn by the house (the destruction
|
|
6630
|
+
* renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
|
|
6631
|
+
*/
|
|
6632
|
+
spall(event) {
|
|
6633
|
+
const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
|
|
6634
|
+
return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
|
|
6635
|
+
},
|
|
6636
|
+
/**
|
|
6637
|
+
* Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
|
|
6638
|
+
* throws the bits and leaves its patch out. Call it once the spec is known.
|
|
6639
|
+
*/
|
|
6640
|
+
claim() {
|
|
6641
|
+
var _a;
|
|
6642
|
+
for (const q of opts.spec()?.parts ?? []) {
|
|
6643
|
+
if (!q.units) continue;
|
|
6644
|
+
const host = hostOf(q.id);
|
|
6645
|
+
if (!host) continue;
|
|
6646
|
+
for (const it of structure.meshesOf(host)) {
|
|
6647
|
+
if (!it.mesh) continue;
|
|
6648
|
+
const list = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
|
|
6649
|
+
if (!list.includes(q.id)) list.push(q.id);
|
|
6650
|
+
}
|
|
6651
|
+
}
|
|
6652
|
+
},
|
|
6653
|
+
/**
|
|
6654
|
+
* Handles a `chip` event: what came out (a unit, or a crater a shot broke through one) leaves a hole in the wall where it was; a `torn` chip (what a blow
|
|
6655
|
+
* tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
|
|
6656
|
+
*/
|
|
6494
6657
|
chip(event) {
|
|
6495
|
-
|
|
6658
|
+
if (event.torn) return false;
|
|
6659
|
+
return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
|
|
6496
6660
|
},
|
|
6497
6661
|
/** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
|
|
6498
6662
|
drop(partId) {
|
|
@@ -6500,9 +6664,11 @@ function installDestructionBreach(K, _shared) {
|
|
|
6500
6664
|
if (!v) return;
|
|
6501
6665
|
dispose(v);
|
|
6502
6666
|
views.delete(partId);
|
|
6667
|
+
if (v.saved) dropped.push(v);
|
|
6503
6668
|
},
|
|
6504
6669
|
/** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
|
|
6505
6670
|
setLod(level) {
|
|
6671
|
+
flush(Infinity);
|
|
6506
6672
|
const was = lod;
|
|
6507
6673
|
lod = level;
|
|
6508
6674
|
if (was === 0 === (level === 0)) return;
|
|
@@ -6521,15 +6687,20 @@ function installDestructionBreach(K, _shared) {
|
|
|
6521
6687
|
if (lod === 0) plain(v, true);
|
|
6522
6688
|
}
|
|
6523
6689
|
views.clear();
|
|
6690
|
+
for (const v of dropped) plain(v, true);
|
|
6691
|
+
dropped.length = 0;
|
|
6524
6692
|
},
|
|
6693
|
+
flush,
|
|
6525
6694
|
/** The wall has damage drawn. */
|
|
6526
6695
|
has(partId) {
|
|
6527
6696
|
return views.has(partId);
|
|
6528
6697
|
},
|
|
6529
|
-
/** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
|
|
6530
|
-
statistics: () => ({
|
|
6698
|
+
/** Walls with damage drawn, units out of them, craters shots broke through units that stay, and units laid bare at the edge of the cuts. */
|
|
6699
|
+
statistics: () => (flush(Infinity), {
|
|
6531
6700
|
walls: views.size,
|
|
6532
6701
|
gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
|
|
6702
|
+
craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
|
|
6703
|
+
flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
|
|
6533
6704
|
units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
|
|
6534
6705
|
})
|
|
6535
6706
|
};
|
|
@@ -6538,6 +6709,7 @@ function installDestructionBreach(K, _shared) {
|
|
|
6538
6709
|
}
|
|
6539
6710
|
|
|
6540
6711
|
// src/destruction/world.ts
|
|
6712
|
+
var FOLLOW_FLUSH_FALLBACK_MS = 100;
|
|
6541
6713
|
function installDestructionWorld(K, shared) {
|
|
6542
6714
|
const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
|
|
6543
6715
|
function createHouseDestruction(root, house, structure, parts, colliders, water) {
|
|
@@ -6546,7 +6718,7 @@ function installDestructionWorld(K, shared) {
|
|
|
6546
6718
|
const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
|
|
6547
6719
|
D.renderer = R;
|
|
6548
6720
|
D.spec = null;
|
|
6549
|
-
const breach = createBreachView({ house, structure, spec: () => D.spec });
|
|
6721
|
+
const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
|
|
6550
6722
|
D.breach = breach;
|
|
6551
6723
|
D.onEvent = (fn) => {
|
|
6552
6724
|
listeners.add(fn);
|
|
@@ -6731,6 +6903,9 @@ function installDestructionWorld(K, shared) {
|
|
|
6731
6903
|
case "chip":
|
|
6732
6904
|
breach.chip(e);
|
|
6733
6905
|
break;
|
|
6906
|
+
case "spall":
|
|
6907
|
+
breach.spall(e);
|
|
6908
|
+
break;
|
|
6734
6909
|
case "collapse":
|
|
6735
6910
|
for (const id of e.parts) {
|
|
6736
6911
|
breach.drop(id);
|
|
@@ -6772,7 +6947,8 @@ function installDestructionWorld(K, shared) {
|
|
|
6772
6947
|
D.specOptions = specOptions || null;
|
|
6773
6948
|
D.spec = structure.spec(specOptions);
|
|
6774
6949
|
D.id = D.world.add(D.spec, { id: "house3d:" + ++shared.destructionSeq, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
|
|
6775
|
-
|
|
6950
|
+
breach.claim();
|
|
6951
|
+
if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
|
|
6776
6952
|
off = D.world.on("*", handleEvent);
|
|
6777
6953
|
D.active = true;
|
|
6778
6954
|
return D;
|
|
@@ -6787,6 +6963,8 @@ function installDestructionWorld(K, shared) {
|
|
|
6787
6963
|
};
|
|
6788
6964
|
D.follow = (world, id, spec) => {
|
|
6789
6965
|
D.spec = spec;
|
|
6966
|
+
breach.claim();
|
|
6967
|
+
let fallback = null;
|
|
6790
6968
|
const offFollow = world.on("*", (e) => {
|
|
6791
6969
|
if (e.object !== id) return;
|
|
6792
6970
|
switch (e.type) {
|
|
@@ -6796,6 +6974,9 @@ function installDestructionWorld(K, shared) {
|
|
|
6796
6974
|
case "chip":
|
|
6797
6975
|
breach.chip(e);
|
|
6798
6976
|
break;
|
|
6977
|
+
case "spall":
|
|
6978
|
+
breach.spall(e);
|
|
6979
|
+
break;
|
|
6799
6980
|
case "fracture":
|
|
6800
6981
|
breach.drop(e.part);
|
|
6801
6982
|
break;
|
|
@@ -6805,10 +6986,16 @@ function installDestructionWorld(K, shared) {
|
|
|
6805
6986
|
default:
|
|
6806
6987
|
break;
|
|
6807
6988
|
}
|
|
6989
|
+
if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
|
|
6990
|
+
fallback = null;
|
|
6991
|
+
breach.flush(Infinity);
|
|
6992
|
+
}, FOLLOW_FLUSH_FALLBACK_MS);
|
|
6808
6993
|
});
|
|
6809
6994
|
return { restore: () => breach.restore(), dispose: () => {
|
|
6810
6995
|
offFollow();
|
|
6811
6996
|
breach.restore();
|
|
6997
|
+
}, update: () => {
|
|
6998
|
+
breach.flush();
|
|
6812
6999
|
} };
|
|
6813
7000
|
};
|
|
6814
7001
|
D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
|
|
@@ -6841,6 +7028,7 @@ function installDestructionWorld(K, shared) {
|
|
|
6841
7028
|
return r;
|
|
6842
7029
|
};
|
|
6843
7030
|
D.animate = (dt) => {
|
|
7031
|
+
breach.flush();
|
|
6844
7032
|
if (R) return;
|
|
6845
7033
|
let moved = false;
|
|
6846
7034
|
for (const [id, list] of bodies) {
|
|
@@ -8307,6 +8495,7 @@ function createFloorplanTestModule(options = {}) {
|
|
|
8307
8495
|
houseRenderer = create(THREE, {
|
|
8308
8496
|
locale,
|
|
8309
8497
|
lod: { interior: 10, hide: 40 },
|
|
8498
|
+
breachPaintBudgetMs: 4,
|
|
8310
8499
|
...options.house,
|
|
8311
8500
|
quality: settings.quality,
|
|
8312
8501
|
...options.world ? { destruction: options.world } : {},
|
|
@@ -8462,9 +8651,39 @@ function createFloorplanTestModule(options = {}) {
|
|
|
8462
8651
|
host?.log(text.steps.no_door);
|
|
8463
8652
|
return;
|
|
8464
8653
|
}
|
|
8465
|
-
|
|
8654
|
+
const fo = target.target;
|
|
8655
|
+
const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
|
|
8656
|
+
if (reach && host?.reachFor) {
|
|
8657
|
+
host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
|
|
8658
|
+
} else p.house.interact(eyeOf(p));
|
|
8466
8659
|
host?.log(fill(text.steps.door, { text: String(target.text) }));
|
|
8467
8660
|
};
|
|
8661
|
+
const doorReach = (p, fo) => {
|
|
8662
|
+
const g = fo.gs[0];
|
|
8663
|
+
const pivot = fo.handles[0].pivot;
|
|
8664
|
+
const at = (angle) => {
|
|
8665
|
+
const c = Math.cos(angle);
|
|
8666
|
+
const s = Math.sin(angle);
|
|
8667
|
+
const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
|
|
8668
|
+
return g.parent ? g.parent.localToWorld(v) : v;
|
|
8669
|
+
};
|
|
8670
|
+
const camera = host?.camera;
|
|
8671
|
+
const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
|
|
8672
|
+
const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
|
|
8673
|
+
const closed = at(0);
|
|
8674
|
+
const open = at(fo.rot);
|
|
8675
|
+
const opening = !fo.open;
|
|
8676
|
+
return {
|
|
8677
|
+
handle: () => {
|
|
8678
|
+
g.updateMatrixWorld(true);
|
|
8679
|
+
return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
|
|
8680
|
+
},
|
|
8681
|
+
hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
|
|
8682
|
+
seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
|
|
8683
|
+
pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
|
|
8684
|
+
locked: !!(fo.locked || fo.blockedFlag)
|
|
8685
|
+
};
|
|
8686
|
+
};
|
|
8468
8687
|
const strike = () => {
|
|
8469
8688
|
const world = options.world;
|
|
8470
8689
|
if (!world || !host) return;
|