@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.global.js
CHANGED
|
@@ -13523,6 +13523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13523
13523
|
var FLAMMABLE_FLUIDS = ["gas", "fuel"];
|
|
13524
13524
|
var VENT_DIAMETER = 8e-3;
|
|
13525
13525
|
var FLAME_LIGHTS_LEAK_C = 470;
|
|
13526
|
+
var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
|
|
13526
13527
|
var BREACH_RADIUS = 0.55;
|
|
13527
13528
|
var BREACH_BLAST_FACTOR = 6;
|
|
13528
13529
|
var BREACH_AREA = 0.25;
|
|
@@ -13538,6 +13539,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13538
13539
|
var BREACH_GRID_CAP = 4096;
|
|
13539
13540
|
var CHIP_ENERGY = 200;
|
|
13540
13541
|
var CHIP_SPLASH = 0.5;
|
|
13542
|
+
var CRATER_SPENT = 0.6;
|
|
13543
|
+
var EDGE_REACH = 1.5;
|
|
13541
13544
|
var BAR_BOW = 0.08;
|
|
13542
13545
|
var BULLET_JOINT_SPAN = 0.3;
|
|
13543
13546
|
var CUT_SLICE = 1;
|
|
@@ -13558,12 +13561,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13558
13561
|
const cells = p.st.chipped;
|
|
13559
13562
|
if (!cells?.length) return 0;
|
|
13560
13563
|
const [u, v] = planeAxes(p.thin);
|
|
13561
|
-
const
|
|
13564
|
+
const role = p.spec.structure?.role;
|
|
13565
|
+
const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
|
|
13566
|
+
const upright = Math.abs(up[1]) > 0.7 ? v : Math.abs(rotateVec(p.q, [0, 0, 0].map((_, k) => k === u ? 1 : 0))[1]) > 0.7 ? u : -1;
|
|
13567
|
+
const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
|
|
13562
13568
|
const across = along === u ? v : u;
|
|
13563
13569
|
let worst = 0;
|
|
13564
13570
|
for (const c of cells) {
|
|
13565
|
-
|
|
13566
|
-
for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6)
|
|
13571
|
+
const spans = [];
|
|
13572
|
+
for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6) spans.push([d.center[across] - d.size[across] / 2, d.center[across] + d.size[across] / 2]);
|
|
13573
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
13574
|
+
let gone = 0, end = -Infinity;
|
|
13575
|
+
for (const [a, b] of spans) {
|
|
13576
|
+
if (b <= end) continue;
|
|
13577
|
+
gone += b - Math.max(a, end);
|
|
13578
|
+
end = b;
|
|
13579
|
+
}
|
|
13567
13580
|
worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
|
|
13568
13581
|
}
|
|
13569
13582
|
return Math.min(1, worst);
|
|
@@ -13859,7 +13872,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13859
13872
|
if (k < 0 || j.spec.kind !== "tie") return k;
|
|
13860
13873
|
return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
|
|
13861
13874
|
};
|
|
13875
|
+
let poseVersion = 0;
|
|
13876
|
+
const contactMemo = /* @__PURE__ */ new WeakMap();
|
|
13862
13877
|
const contactBox = (p, support) => {
|
|
13878
|
+
let byTouching = contactMemo.get(p);
|
|
13879
|
+
if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
|
|
13880
|
+
const known = byTouching.get(support);
|
|
13881
|
+
if (known && known.version === poseVersion) return known.box;
|
|
13882
|
+
const box3 = measureContact(p, support);
|
|
13883
|
+
byTouching.set(support, { version: poseVersion, box: box3 });
|
|
13884
|
+
return box3;
|
|
13885
|
+
};
|
|
13886
|
+
const measureContact = (p, support) => {
|
|
13863
13887
|
const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
|
|
13864
13888
|
const lo = [0, 0, 0], hi = [0, 0, 0];
|
|
13865
13889
|
for (let k = 0; k < 3; k++) {
|
|
@@ -14654,7 +14678,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14654
14678
|
p.st.holes.push(hole);
|
|
14655
14679
|
if (p.st.holes.length > maxHoles) p.st.holes.shift();
|
|
14656
14680
|
markDamaged(p);
|
|
14657
|
-
emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
|
|
14681
|
+
emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
|
|
14658
14682
|
setStage(o, p, 1, {}, src);
|
|
14659
14683
|
return hole;
|
|
14660
14684
|
};
|
|
@@ -14671,6 +14695,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14671
14695
|
const detachEvents = (o, opened, by, src) => {
|
|
14672
14696
|
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]) });
|
|
14673
14697
|
};
|
|
14698
|
+
const slideOffResting = (o, rotation) => {
|
|
14699
|
+
const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
|
|
14700
|
+
if (upY >= RESTING_HOLD_COS) return;
|
|
14701
|
+
const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
|
|
14702
|
+
if (opened.length === 0) return;
|
|
14703
|
+
run(() => {
|
|
14704
|
+
for (const j of opened) {
|
|
14705
|
+
j.st.open = true;
|
|
14706
|
+
j.st.openedBy = "break";
|
|
14707
|
+
}
|
|
14708
|
+
solveStructure(o);
|
|
14709
|
+
recomputeGroups(o);
|
|
14710
|
+
detachEvents(o, opened, "break");
|
|
14711
|
+
collapseCheck(o);
|
|
14712
|
+
});
|
|
14713
|
+
};
|
|
14674
14714
|
const checkDestroyed = (o, src) => {
|
|
14675
14715
|
if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
|
|
14676
14716
|
o.st.destroyed = true;
|
|
@@ -14771,7 +14811,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14771
14811
|
const localHolesOf = (p) => p.st.holes.map((h) => ({ point: toLocal(p.pos, p.q, h.point), normal: rotateVec(quat.conjugate(p.q), h.normal), diameter: h.diameter }));
|
|
14772
14812
|
const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
|
|
14773
14813
|
const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
|
|
14774
|
-
const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
|
|
14814
|
+
const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
|
|
14775
14815
|
const { c, q } = partXf(o, p);
|
|
14776
14816
|
const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
|
|
14777
14817
|
const [u, v] = planeAxes(p.thin);
|
|
@@ -14795,7 +14835,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14795
14835
|
velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
|
|
14796
14836
|
angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
|
|
14797
14837
|
mass: mass * faceArea(cell) / total,
|
|
14798
|
-
...marks.length > 0 ? { marks } : {}
|
|
14838
|
+
...marks.length > 0 ? { marks } : {},
|
|
14839
|
+
...inner ? { inner } : {}
|
|
14799
14840
|
};
|
|
14800
14841
|
});
|
|
14801
14842
|
};
|
|
@@ -14836,7 +14877,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14836
14877
|
return cells.some((c) => Math.abs(lp[u] - c.center[u]) <= c.size[u] / 2 && Math.abs(lp[v] - c.center[v]) <= c.size[v] / 2);
|
|
14837
14878
|
};
|
|
14838
14879
|
const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
|
|
14839
|
-
var _a, _b;
|
|
14880
|
+
var _a, _b, _c;
|
|
14840
14881
|
if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
|
|
14841
14882
|
const rule = p.mat.hits?.[kind];
|
|
14842
14883
|
const perKg = rule?.unitJoules ?? chipEnergy;
|
|
@@ -14846,7 +14887,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14846
14887
|
const lp = toLocal(p.pos, p.q, objLocal(o, wp));
|
|
14847
14888
|
const grid = gridOf(p);
|
|
14848
14889
|
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
14890
|
+
const cut = (_b = p.rt).craters ?? (_b.craters = {});
|
|
14849
14891
|
const out = [];
|
|
14892
|
+
const shot = kind === "ballistic";
|
|
14850
14893
|
for (let i = 0; i < grid.length; i++) {
|
|
14851
14894
|
const cell = grid[i];
|
|
14852
14895
|
if (worn[i] === -1) continue;
|
|
@@ -14859,18 +14902,42 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14859
14902
|
worn[i] = total;
|
|
14860
14903
|
continue;
|
|
14861
14904
|
}
|
|
14862
|
-
|
|
14863
|
-
|
|
14905
|
+
if (!shot) {
|
|
14906
|
+
worn[i] = -1;
|
|
14907
|
+
out.push(cell);
|
|
14908
|
+
continue;
|
|
14909
|
+
}
|
|
14910
|
+
const size = [cell.size[0], cell.size[1], cell.size[2]];
|
|
14911
|
+
const center = [cell.center[0], cell.center[1], cell.center[2]];
|
|
14912
|
+
for (const a of [u, v]) {
|
|
14913
|
+
const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
|
|
14914
|
+
const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
|
|
14915
|
+
size[a] = w;
|
|
14916
|
+
center[a] = clamp2(lp[a], lo, hi);
|
|
14917
|
+
const [from, to] = tornToEdge({ p, cell, a, b: a === u ? v : u, at: lp[a], crater: [center[a] - w / 2, center[a] + w / 2], reach: w * EDGE_REACH, gone: (k) => worn[k] === -1 });
|
|
14918
|
+
size[a] = to - from;
|
|
14919
|
+
center[a] = (from + to) / 2;
|
|
14920
|
+
}
|
|
14921
|
+
const taken2 = (cut[i] ?? 0) + size[u] * size[v];
|
|
14922
|
+
if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
|
|
14923
|
+
worn[i] = -1;
|
|
14924
|
+
out.push({ ...cell, crater: true });
|
|
14925
|
+
continue;
|
|
14926
|
+
}
|
|
14927
|
+
worn[i] = 0;
|
|
14928
|
+
cut[i] = taken2;
|
|
14929
|
+
out.push({ center, size, crater: true });
|
|
14864
14930
|
}
|
|
14865
14931
|
if (out.length === 0) return;
|
|
14866
14932
|
const { c, q } = partXf(o, p);
|
|
14867
|
-
const chipped = (
|
|
14933
|
+
const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
|
|
14868
14934
|
const holes = localHolesOf(p);
|
|
14869
14935
|
for (const cell of out) {
|
|
14870
14936
|
const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
|
|
14871
14937
|
const center = toWorld(c, q, cell.center);
|
|
14872
14938
|
const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
|
|
14873
|
-
const
|
|
14939
|
+
const tag = `c${chipped.length}_`;
|
|
14940
|
+
const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
|
|
14874
14941
|
p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
|
|
14875
14942
|
chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
|
|
14876
14943
|
p.st.mass = Math.max(0, p.st.mass - mass);
|
|
@@ -14887,6 +14954,57 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14887
14954
|
collapseCheck(o, src);
|
|
14888
14955
|
}
|
|
14889
14956
|
};
|
|
14957
|
+
const tornToEdge = (o) => {
|
|
14958
|
+
const { p, cell, a, b, at, crater: crater2, reach, gone } = o;
|
|
14959
|
+
const half = p.dims[a] / 2;
|
|
14960
|
+
const lo = cell.center[a] - cell.size[a] / 2, hi = cell.center[a] + cell.size[a] / 2;
|
|
14961
|
+
const free = (side, sign2) => {
|
|
14962
|
+
if (sign2 > 0 ? side >= half - 1e-6 : side <= -half + 1e-6) return sign2 > 0 || a !== planeAxes(p.thin)[1];
|
|
14963
|
+
const probe = side + sign2 * 1e-3;
|
|
14964
|
+
return gridOf(p).some((c, k) => gone(k) && Math.abs(probe - c.center[a]) <= c.size[a] / 2 && Math.abs(cell.center[b] - c.center[b]) <= c.size[b] / 2);
|
|
14965
|
+
};
|
|
14966
|
+
if (at - lo <= reach && free(lo, -1)) return [lo, Math.max(crater2[1], lo)];
|
|
14967
|
+
if (hi - at <= reach && free(hi, 1)) return [Math.min(crater2[0], hi), hi];
|
|
14968
|
+
return crater2;
|
|
14969
|
+
};
|
|
14970
|
+
const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
|
|
14971
|
+
const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
|
|
14972
|
+
const { c, q } = partXf(o, p);
|
|
14973
|
+
const [u, v] = planeAxes(p.thin);
|
|
14974
|
+
const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
|
|
14975
|
+
const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
|
|
14976
|
+
const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
|
|
14977
|
+
const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
|
|
14978
|
+
const out = [];
|
|
14979
|
+
for (let i = 0; i < n; i++) {
|
|
14980
|
+
const back = i % 3 === 2;
|
|
14981
|
+
const lc = [cell.center[0], cell.center[1], cell.center[2]];
|
|
14982
|
+
lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
|
|
14983
|
+
lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
|
|
14984
|
+
lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
|
|
14985
|
+
const size = [0, 0, 0];
|
|
14986
|
+
size[u] = bit * (0.6 + 0.8 * rnd());
|
|
14987
|
+
size[v] = bit * (0.5 + 0.7 * rnd());
|
|
14988
|
+
size[p.thin] = bit * (0.4 + 0.6 * rnd());
|
|
14989
|
+
const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
|
|
14990
|
+
const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
|
|
14991
|
+
const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
|
|
14992
|
+
out.push({
|
|
14993
|
+
id: `${p.spec.id}#${tag}${i}`,
|
|
14994
|
+
part: p.spec.id,
|
|
14995
|
+
material: p.spec.material,
|
|
14996
|
+
shape: "chunk",
|
|
14997
|
+
center: toWorld(c, q, lc),
|
|
14998
|
+
size,
|
|
14999
|
+
rotation: q,
|
|
15000
|
+
velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
|
|
15001
|
+
angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
|
|
15002
|
+
mass: mass / n,
|
|
15003
|
+
inner: true
|
|
15004
|
+
});
|
|
15005
|
+
}
|
|
15006
|
+
return out;
|
|
15007
|
+
};
|
|
14890
15008
|
const exposeRebar = (o, p, src) => {
|
|
14891
15009
|
const r = p.spec.reinforcement;
|
|
14892
15010
|
if (!r) return;
|
|
@@ -15174,7 +15292,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15174
15292
|
});
|
|
15175
15293
|
}
|
|
15176
15294
|
const patch = [scale * 4, scale * 4, scale * 4];
|
|
15177
|
-
emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
|
|
15295
|
+
emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
|
|
15178
15296
|
};
|
|
15179
15297
|
const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
|
|
15180
15298
|
const mat = p.mat;
|
|
@@ -15233,12 +15351,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15233
15351
|
} else if (mat.ductility >= 0.5) {
|
|
15234
15352
|
addDent(o, p, entry2, n, Math.max(caliber * 3 / 1e3, 0.012), clamp2(Math.min(Math.max(p.thickness * 3, 3e-3), ratio * 0.01), 1e-3, 0.05), src);
|
|
15235
15353
|
} else if (brittle) {
|
|
15236
|
-
addCrack(o, p, entry2, n, crackLen, src);
|
|
15354
|
+
if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
|
|
15237
15355
|
if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
|
|
15238
15356
|
} else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
|
|
15239
15357
|
debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
|
|
15240
15358
|
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
|
|
15241
|
-
const
|
|
15359
|
+
const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
|
|
15360
|
+
const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
|
|
15242
15361
|
const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
|
|
15243
15362
|
if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
|
|
15244
15363
|
chipUnits(o, p, "ballistic", entry2, penetrated ? Math.min(E, Math.max(thr, E * 0.5)) : E, d, Math.max(0.05, caliber / 1e3 * 8), src);
|
|
@@ -15323,10 +15442,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15323
15442
|
if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
|
|
15324
15443
|
return reach;
|
|
15325
15444
|
};
|
|
15445
|
+
const blastProbe = { partsTested: 0, partsNear: 0 };
|
|
15446
|
+
const blastCandidates = (o, point, radius) => {
|
|
15447
|
+
const local = toLocal(o.st.position, o.st.rotation, point);
|
|
15448
|
+
const near = [];
|
|
15449
|
+
for (const p of o.parts) {
|
|
15450
|
+
const reach = radius + p.bound;
|
|
15451
|
+
const dx = local[0] - p.pos[0];
|
|
15452
|
+
const dy = local[1] - p.pos[1];
|
|
15453
|
+
const dz = local[2] - p.pos[2];
|
|
15454
|
+
if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
|
|
15455
|
+
}
|
|
15456
|
+
blastProbe.partsTested += o.parts.length;
|
|
15457
|
+
blastProbe.partsNear += near.length;
|
|
15458
|
+
return near;
|
|
15459
|
+
};
|
|
15326
15460
|
const blastObject = (o, point, energy, radius, src) => {
|
|
15327
15461
|
const start = cur.length;
|
|
15328
15462
|
const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
|
|
15329
|
-
for (const p of o
|
|
15463
|
+
for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
|
|
15330
15464
|
res.events = cur.slice(start);
|
|
15331
15465
|
return res;
|
|
15332
15466
|
};
|
|
@@ -15450,7 +15584,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15450
15584
|
let order = 0;
|
|
15451
15585
|
for (const o of inRange) {
|
|
15452
15586
|
if (only && only !== o) continue;
|
|
15453
|
-
for (const p of o
|
|
15587
|
+
for (const p of blastCandidates(o, point, radius)) {
|
|
15454
15588
|
if (isLost(p)) continue;
|
|
15455
15589
|
const dist = distToPart(o, p, point);
|
|
15456
15590
|
if (dist < radius) items.push({ dist, order: order++, o, p });
|
|
@@ -15997,6 +16131,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15997
16131
|
const p = getPart(o, partId);
|
|
15998
16132
|
p.pos = [position[0], position[1], position[2]];
|
|
15999
16133
|
p.q = q;
|
|
16134
|
+
poseVersion++;
|
|
16000
16135
|
updateBound(o);
|
|
16001
16136
|
},
|
|
16002
16137
|
remove(id) {
|
|
@@ -16015,6 +16150,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16015
16150
|
const o = getObj(id);
|
|
16016
16151
|
o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
|
|
16017
16152
|
applySoilPose(o);
|
|
16153
|
+
slideOffResting(o, rotation);
|
|
16018
16154
|
},
|
|
16019
16155
|
splitGroup(id, group, newId) {
|
|
16020
16156
|
if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
|
|
@@ -16038,7 +16174,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16038
16174
|
if (o.spec.name !== void 0) s.name = o.spec.name;
|
|
16039
16175
|
if (o.spec.tags) s.tags = [...o.spec.tags];
|
|
16040
16176
|
if (o.spec.soil !== void 0) s.soil = o.spec.soil;
|
|
16041
|
-
return
|
|
16177
|
+
return s;
|
|
16042
16178
|
};
|
|
16043
16179
|
const inNew = (pid) => moved.has(pid);
|
|
16044
16180
|
const inOld = (pid) => !moved.has(pid);
|
|
@@ -16065,7 +16201,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16065
16201
|
const p = getPart(o, partId);
|
|
16066
16202
|
return whole(() => doHit(o, h, p));
|
|
16067
16203
|
},
|
|
16204
|
+
blastProbe() {
|
|
16205
|
+
return { ...blastProbe };
|
|
16206
|
+
},
|
|
16068
16207
|
explode(point, energy, radius, source, blast) {
|
|
16208
|
+
blastProbe.partsTested = 0;
|
|
16209
|
+
blastProbe.partsNear = 0;
|
|
16069
16210
|
return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
|
|
16070
16211
|
},
|
|
16071
16212
|
raycast(origin, direction, maxDistance = Infinity) {
|
|
@@ -16075,7 +16216,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16075
16216
|
sweepBox(box3, direction, distance2, options2 = {}) {
|
|
16076
16217
|
const d = vec.normalize(direction);
|
|
16077
16218
|
const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
|
|
16078
|
-
const
|
|
16219
|
+
const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
|
|
16220
|
+
const ignore = ignored ? new Set(ignored.parts.map((p) => `${ignored.id}\0${p.spec.id}`)) : hosted ? new Set(hosted.impl.describe().parts.map((p) => `${hosted.impl.id}\0${p.id}`)) : void 0;
|
|
16079
16221
|
let best = null;
|
|
16080
16222
|
for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
|
|
16081
16223
|
const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
|
|
@@ -19589,6 +19731,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19589
19731
|
function collapseRange(mesh2, v0, v1) {
|
|
19590
19732
|
const pos = mesh2.geometry.attributes.position;
|
|
19591
19733
|
const a = pos.array;
|
|
19734
|
+
const u = mesh2.userData;
|
|
19735
|
+
if (!u._p0) u._p0 = a.slice();
|
|
19736
|
+
const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
|
|
19737
|
+
if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
|
|
19592
19738
|
const saved = a.slice(v0 * 3, v1 * 3);
|
|
19593
19739
|
const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
|
|
19594
19740
|
for (let k = v0; k < v1; k++) {
|
|
@@ -19603,6 +19749,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19603
19749
|
const pos = mesh2.geometry.attributes.position;
|
|
19604
19750
|
pos.array.set(saved, v0 * 3);
|
|
19605
19751
|
pos.needsUpdate = true;
|
|
19752
|
+
const list2 = mesh2.userData.collapsedRanges;
|
|
19753
|
+
if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
|
|
19606
19754
|
}
|
|
19607
19755
|
function noise2(x, y, cell) {
|
|
19608
19756
|
const h = (i2, j2) => {
|
|
@@ -19617,13 +19765,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19617
19765
|
var SPALL_MIN = 0.025;
|
|
19618
19766
|
var SPALL_RAGGED = 0.045;
|
|
19619
19767
|
var COVER2 = 0.015;
|
|
19768
|
+
var MAX_FLAKES = 64;
|
|
19769
|
+
function clipToBox(unit, box3) {
|
|
19770
|
+
const lo = [Math.max(unit.x - unit.sx / 2, box3[0]), Math.max(unit.y - unit.sy / 2, box3[1]), Math.max(unit.z - unit.sz / 2, box3[2])];
|
|
19771
|
+
const hi = [Math.min(unit.x + unit.sx / 2, box3[3]), Math.min(unit.y + unit.sy / 2, box3[4]), Math.min(unit.z + unit.sz / 2, box3[5])];
|
|
19772
|
+
if (hi[0] - lo[0] <= 1e-4 || hi[1] - lo[1] <= 1e-4 || hi[2] - lo[2] <= 1e-4) return null;
|
|
19773
|
+
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 };
|
|
19774
|
+
}
|
|
19620
19775
|
function installDestructionBreach(K, _shared) {
|
|
19621
19776
|
const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
|
|
19622
19777
|
function createBreachView(opts) {
|
|
19623
19778
|
const { house, structure } = opts;
|
|
19624
19779
|
const views = /* @__PURE__ */ new Map();
|
|
19780
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
19781
|
+
const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
|
|
19782
|
+
const dropped = [];
|
|
19625
19783
|
let lod = 0;
|
|
19626
|
-
const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
|
|
19627
19784
|
const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
19628
19785
|
const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
|
|
19629
19786
|
const local = (p) => {
|
|
@@ -19692,6 +19849,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19692
19849
|
const mesh2 = new THREE.Mesh(g, material);
|
|
19693
19850
|
mesh2.name = "breach-shell";
|
|
19694
19851
|
mesh2.userData.breachMask = mask;
|
|
19852
|
+
mesh2.userData.cutAt = (p) => {
|
|
19853
|
+
const v = local([p.x, p.y, p.z]);
|
|
19854
|
+
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);
|
|
19855
|
+
return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
|
|
19856
|
+
};
|
|
19695
19857
|
mesh2.position.copy(it.mesh.position);
|
|
19696
19858
|
mesh2.quaternion.copy(it.mesh.quaternion);
|
|
19697
19859
|
mesh2.scale.copy(it.mesh.scale);
|
|
@@ -19707,6 +19869,29 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19707
19869
|
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
|
|
19708
19870
|
return mesh2;
|
|
19709
19871
|
}
|
|
19872
|
+
function unitsMaterial(core) {
|
|
19873
|
+
const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
|
|
19874
|
+
material.onBeforeCompile = (sh) => {
|
|
19875
|
+
sh.uniforms.uCore = { value: core.texture };
|
|
19876
|
+
sh.uniforms.uCoreRect = { value: core.rect };
|
|
19877
|
+
sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
|
|
19878
|
+
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");
|
|
19879
|
+
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;");
|
|
19880
|
+
};
|
|
19881
|
+
material.customProgramCacheKey = () => "breachCore";
|
|
19882
|
+
material.userData.breachCore = core;
|
|
19883
|
+
return material;
|
|
19884
|
+
}
|
|
19885
|
+
function drawnBounds(items) {
|
|
19886
|
+
house.updateMatrixWorld(true);
|
|
19887
|
+
const box3 = new THREE.Box3(), toHouse = new THREE.Matrix4().copy(house.matrixWorld).invert();
|
|
19888
|
+
for (const it of items) {
|
|
19889
|
+
const a = it.mesh.userData._p0 || it.mesh.geometry.attributes.position.array;
|
|
19890
|
+
tmp.m.multiplyMatrices(toHouse, it.mesh.matrixWorld);
|
|
19891
|
+
for (let k = it.v0; k < it.v1; k++) box3.expandByPoint(tmp.p.set(a[k * 3], a[k * 3 + 1], a[k * 3 + 2]).applyMatrix4(tmp.m));
|
|
19892
|
+
}
|
|
19893
|
+
return [box3.min.x, box3.min.y, box3.min.z, box3.max.x, box3.max.y, box3.max.z];
|
|
19894
|
+
}
|
|
19710
19895
|
function maskOf(hostId, parts) {
|
|
19711
19896
|
const host = structure.byId.get(hostId);
|
|
19712
19897
|
const [u0, v0, u1, v1] = host.aabb;
|
|
@@ -19722,51 +19907,86 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19722
19907
|
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
19908
|
}
|
|
19724
19909
|
const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
|
|
19910
|
+
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);
|
|
19911
|
+
function covered(c, boxes) {
|
|
19912
|
+
const n = 8;
|
|
19913
|
+
let hit = 0;
|
|
19914
|
+
for (let j = 0; j < n; j++) {
|
|
19915
|
+
const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
|
|
19916
|
+
for (let i = 0; i < n; i++) {
|
|
19917
|
+
const s = c.a + (i + 0.5) / n * (c.b - c.a);
|
|
19918
|
+
if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
|
|
19919
|
+
}
|
|
19920
|
+
}
|
|
19921
|
+
return hit / (n * n);
|
|
19922
|
+
}
|
|
19725
19923
|
function paint(view) {
|
|
19726
|
-
const { mask } = view;
|
|
19924
|
+
const { mask, core } = view;
|
|
19727
19925
|
const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
|
|
19728
19926
|
const holes = [];
|
|
19927
|
+
const pits = [];
|
|
19729
19928
|
const discs = [];
|
|
19929
|
+
const flakes = [];
|
|
19730
19930
|
const shown = [];
|
|
19731
19931
|
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);
|
|
19932
|
+
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);
|
|
19733
19933
|
const all = cells(part, units2);
|
|
19734
19934
|
const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
|
|
19735
19935
|
const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
|
|
19736
19936
|
if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
|
|
19737
|
-
|
|
19937
|
+
for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
|
|
19938
|
+
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 }));
|
|
19939
|
+
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;
|
|
19738
19940
|
const kept = [];
|
|
19739
19941
|
for (const c of all) (gone(c) ? holes : kept).push(c);
|
|
19740
19942
|
for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
|
|
19943
|
+
for (const k of boxes) {
|
|
19944
|
+
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;
|
|
19945
|
+
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 });
|
|
19946
|
+
}
|
|
19741
19947
|
}
|
|
19742
19948
|
const reach = SPALL_MIN + SPALL_RAGGED;
|
|
19949
|
+
const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
|
|
19743
19950
|
mask.data.fill(0);
|
|
19744
|
-
|
|
19745
|
-
|
|
19746
|
-
const
|
|
19951
|
+
core.data.fill(0);
|
|
19952
|
+
const stamp = (m, s0, s1, y0, y1, inside2) => {
|
|
19953
|
+
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));
|
|
19954
|
+
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));
|
|
19747
19955
|
for (let j = j0; j <= j1; j++) {
|
|
19748
|
-
const y =
|
|
19956
|
+
const y = m.y0 + (j + 0.5) / m.h * m.height;
|
|
19749
19957
|
for (let i = i0; i <= i1; i++) {
|
|
19750
|
-
const s = (i + 0.5) /
|
|
19751
|
-
|
|
19752
|
-
if (inside2(s, y) <= edge) mask.data[j * mask.w + i] = 255;
|
|
19958
|
+
const s = (i + 0.5) / m.w * m.len;
|
|
19959
|
+
if (inside2(s, y)) m.data[j * m.w + i] = 255;
|
|
19753
19960
|
}
|
|
19754
19961
|
}
|
|
19755
19962
|
};
|
|
19756
|
-
|
|
19757
|
-
for (const
|
|
19963
|
+
const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
|
|
19964
|
+
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));
|
|
19965
|
+
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));
|
|
19966
|
+
for (const f of flakes) {
|
|
19967
|
+
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));
|
|
19968
|
+
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);
|
|
19969
|
+
}
|
|
19970
|
+
for (const p of pits) {
|
|
19971
|
+
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));
|
|
19972
|
+
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));
|
|
19973
|
+
}
|
|
19758
19974
|
mask.texture.needsUpdate = true;
|
|
19759
|
-
|
|
19975
|
+
core.texture.needsUpdate = true;
|
|
19976
|
+
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);
|
|
19760
19977
|
const list2 = [];
|
|
19761
19978
|
const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
|
|
19762
19979
|
for (const { c, part, units: units2, thick, mid } of shown) {
|
|
19763
19980
|
if (!near(c)) continue;
|
|
19764
19981
|
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) =>
|
|
19982
|
+
const at = (s, dy, d, colour) => {
|
|
19983
|
+
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);
|
|
19984
|
+
if (unit) list2.push(unit);
|
|
19985
|
+
};
|
|
19766
19986
|
const base = fragmentMaterial(part.material).color.getHex();
|
|
19767
19987
|
const n = noise2(centre * 3.1, y * 2.7, 0.07);
|
|
19768
19988
|
if (units2.mortar === "concrete") {
|
|
19769
|
-
at(len, h, thick - 2 * COVER2
|
|
19989
|
+
at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
|
|
19770
19990
|
} else {
|
|
19771
19991
|
at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
|
|
19772
19992
|
at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
|
|
@@ -19776,7 +19996,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19776
19996
|
view.units.removeFromParent();
|
|
19777
19997
|
view.units.dispose();
|
|
19778
19998
|
}
|
|
19779
|
-
const units = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
|
|
19999
|
+
const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
|
|
19780
20000
|
units.name = "breach:" + view.host;
|
|
19781
20001
|
list2.forEach((u, i) => {
|
|
19782
20002
|
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
@@ -19792,6 +20012,19 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19792
20012
|
house.add(units);
|
|
19793
20013
|
view.units = units;
|
|
19794
20014
|
view.gone = holes.length;
|
|
20015
|
+
view.pits = pits.length;
|
|
20016
|
+
view.flakes = flakes.length;
|
|
20017
|
+
}
|
|
20018
|
+
function chipBox(chip, alongU) {
|
|
20019
|
+
house.updateMatrixWorld(true);
|
|
20020
|
+
const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
|
|
20021
|
+
let a = 0, u = 0;
|
|
20022
|
+
for (let k = 0; k < 3; k++) {
|
|
20023
|
+
const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
|
|
20024
|
+
a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
|
|
20025
|
+
u += Math.abs(axis.y) * chip.size[k] / 2;
|
|
20026
|
+
}
|
|
20027
|
+
return { center: local(chip.center), along: a, up: u };
|
|
19795
20028
|
}
|
|
19796
20029
|
function draw2(partId, change) {
|
|
19797
20030
|
if (lod !== 0) return false;
|
|
@@ -19803,23 +20036,50 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19803
20036
|
if (!items.length) return false;
|
|
19804
20037
|
const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
|
|
19805
20038
|
if (!parts.includes(partId)) return false;
|
|
19806
|
-
const mask = maskOf(hostId, parts);
|
|
19807
|
-
view = {
|
|
20039
|
+
const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
|
|
20040
|
+
view = {
|
|
20041
|
+
host: hostId,
|
|
20042
|
+
parts,
|
|
20043
|
+
items,
|
|
20044
|
+
mask,
|
|
20045
|
+
core,
|
|
20046
|
+
unitMaterial: unitsMaterial(core),
|
|
20047
|
+
shells: [],
|
|
20048
|
+
saved: null,
|
|
20049
|
+
units: null,
|
|
20050
|
+
gone: 0,
|
|
20051
|
+
pits: 0,
|
|
20052
|
+
bounds: drawnBounds(items),
|
|
20053
|
+
chips: new Map(parts.map((id) => [id, []])),
|
|
20054
|
+
spalls: new Map(parts.map((id) => [id, []])),
|
|
20055
|
+
openings: /* @__PURE__ */ new Map(),
|
|
20056
|
+
flakes: 0
|
|
20057
|
+
};
|
|
19808
20058
|
}
|
|
19809
20059
|
if (!view.chips.has(partId)) return false;
|
|
19810
20060
|
if (change.opening) {
|
|
19811
20061
|
if (view.openings.has(partId)) return false;
|
|
19812
20062
|
view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
|
|
19813
20063
|
}
|
|
19814
|
-
if (change.chip) view.chips.get(partId).push(
|
|
20064
|
+
if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
|
|
20065
|
+
if (change.spall) {
|
|
20066
|
+
const list2 = view.spalls.get(partId);
|
|
20067
|
+
list2.push({ center: local(change.spall.center), radius: change.spall.radius });
|
|
20068
|
+
if (list2.length > MAX_FLAKES) list2.shift();
|
|
20069
|
+
}
|
|
19815
20070
|
if (!view.saved) {
|
|
19816
20071
|
view.shells = view.items.map((it) => shell(it, view.mask));
|
|
20072
|
+
for (const m of view.shells) m.userData.behind = () => view.units;
|
|
19817
20073
|
view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
19818
20074
|
}
|
|
19819
|
-
paint(view);
|
|
19820
20075
|
views.set(hostId, view);
|
|
19821
|
-
|
|
19822
|
-
|
|
20076
|
+
if (opts.deferPaint && change.spall) dirty.add(view);
|
|
20077
|
+
else {
|
|
20078
|
+
dirty.delete(view);
|
|
20079
|
+
paint(view);
|
|
20080
|
+
invalidate();
|
|
20081
|
+
invalidateShadow();
|
|
20082
|
+
}
|
|
19823
20083
|
return true;
|
|
19824
20084
|
}
|
|
19825
20085
|
function plain2(view, shown) {
|
|
@@ -19828,24 +20088,70 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19828
20088
|
else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
|
|
19829
20089
|
}
|
|
19830
20090
|
}
|
|
20091
|
+
function flush(budgetMs = paintBudgetMs) {
|
|
20092
|
+
if (dirty.size === 0) return 0;
|
|
20093
|
+
const deadline = performance.now() + budgetMs;
|
|
20094
|
+
for (const view of dirty) {
|
|
20095
|
+
dirty.delete(view);
|
|
20096
|
+
paint(view);
|
|
20097
|
+
if (performance.now() >= deadline) break;
|
|
20098
|
+
}
|
|
20099
|
+
invalidate();
|
|
20100
|
+
invalidateShadow();
|
|
20101
|
+
return dirty.size;
|
|
20102
|
+
}
|
|
19831
20103
|
function dispose(view) {
|
|
19832
|
-
|
|
19833
|
-
view.units
|
|
20104
|
+
dirty.delete(view);
|
|
20105
|
+
if (view.units) {
|
|
20106
|
+
view.units.removeFromParent();
|
|
20107
|
+
view.units.dispose();
|
|
20108
|
+
}
|
|
20109
|
+
view.unitMaterial.dispose();
|
|
19834
20110
|
for (const m of view.shells) {
|
|
19835
20111
|
m.removeFromParent();
|
|
19836
20112
|
m.geometry.dispose();
|
|
19837
20113
|
m.material.dispose();
|
|
19838
20114
|
}
|
|
19839
20115
|
view.mask.texture.dispose();
|
|
20116
|
+
view.core.texture.dispose();
|
|
19840
20117
|
}
|
|
19841
20118
|
return {
|
|
19842
20119
|
/** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
|
|
19843
20120
|
stage(event) {
|
|
19844
20121
|
return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
|
|
19845
20122
|
},
|
|
19846
|
-
/**
|
|
20123
|
+
/**
|
|
20124
|
+
* 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
|
|
20125
|
+
* renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
|
|
20126
|
+
*/
|
|
20127
|
+
spall(event) {
|
|
20128
|
+
const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
|
|
20129
|
+
return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
|
|
20130
|
+
},
|
|
20131
|
+
/**
|
|
20132
|
+
* Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
|
|
20133
|
+
* throws the bits and leaves its patch out. Call it once the spec is known.
|
|
20134
|
+
*/
|
|
20135
|
+
claim() {
|
|
20136
|
+
var _a;
|
|
20137
|
+
for (const q of opts.spec()?.parts ?? []) {
|
|
20138
|
+
if (!q.units) continue;
|
|
20139
|
+
const host = hostOf(q.id);
|
|
20140
|
+
if (!host) continue;
|
|
20141
|
+
for (const it of structure.meshesOf(host)) {
|
|
20142
|
+
if (!it.mesh) continue;
|
|
20143
|
+
const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
|
|
20144
|
+
if (!list2.includes(q.id)) list2.push(q.id);
|
|
20145
|
+
}
|
|
20146
|
+
}
|
|
20147
|
+
},
|
|
20148
|
+
/**
|
|
20149
|
+
* 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
|
|
20150
|
+
* tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
|
|
20151
|
+
*/
|
|
19847
20152
|
chip(event) {
|
|
19848
|
-
|
|
20153
|
+
if (event.torn) return false;
|
|
20154
|
+
return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
|
|
19849
20155
|
},
|
|
19850
20156
|
/** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
|
|
19851
20157
|
drop(partId) {
|
|
@@ -19853,9 +20159,11 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19853
20159
|
if (!v) return;
|
|
19854
20160
|
dispose(v);
|
|
19855
20161
|
views.delete(partId);
|
|
20162
|
+
if (v.saved) dropped.push(v);
|
|
19856
20163
|
},
|
|
19857
20164
|
/** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
|
|
19858
20165
|
setLod(level) {
|
|
20166
|
+
flush(Infinity);
|
|
19859
20167
|
const was = lod;
|
|
19860
20168
|
lod = level;
|
|
19861
20169
|
if (was === 0 === (level === 0)) return;
|
|
@@ -19874,15 +20182,20 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19874
20182
|
if (lod === 0) plain2(v, true);
|
|
19875
20183
|
}
|
|
19876
20184
|
views.clear();
|
|
20185
|
+
for (const v of dropped) plain2(v, true);
|
|
20186
|
+
dropped.length = 0;
|
|
19877
20187
|
},
|
|
20188
|
+
flush,
|
|
19878
20189
|
/** The wall has damage drawn. */
|
|
19879
20190
|
has(partId) {
|
|
19880
20191
|
return views.has(partId);
|
|
19881
20192
|
},
|
|
19882
|
-
/** Walls with damage drawn, units out of them, and units laid bare at the edge of the cuts. */
|
|
19883
|
-
statistics: () => ({
|
|
20193
|
+
/** 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. */
|
|
20194
|
+
statistics: () => (flush(Infinity), {
|
|
19884
20195
|
walls: views.size,
|
|
19885
20196
|
gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
|
|
20197
|
+
craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
|
|
20198
|
+
flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
|
|
19886
20199
|
units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
|
|
19887
20200
|
})
|
|
19888
20201
|
};
|
|
@@ -19891,6 +20204,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19891
20204
|
}
|
|
19892
20205
|
|
|
19893
20206
|
// src/destruction/world.ts
|
|
20207
|
+
var FOLLOW_FLUSH_FALLBACK_MS = 100;
|
|
19894
20208
|
function installDestructionWorld(K, shared) {
|
|
19895
20209
|
const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
|
|
19896
20210
|
function createHouseDestruction(root, house, structure, parts, colliders, water) {
|
|
@@ -19899,7 +20213,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19899
20213
|
const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
|
|
19900
20214
|
D.renderer = R;
|
|
19901
20215
|
D.spec = null;
|
|
19902
|
-
const breach = createBreachView({ house, structure, spec: () => D.spec });
|
|
20216
|
+
const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
|
|
19903
20217
|
D.breach = breach;
|
|
19904
20218
|
D.onEvent = (fn) => {
|
|
19905
20219
|
listeners.add(fn);
|
|
@@ -20084,6 +20398,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20084
20398
|
case "chip":
|
|
20085
20399
|
breach.chip(e);
|
|
20086
20400
|
break;
|
|
20401
|
+
case "spall":
|
|
20402
|
+
breach.spall(e);
|
|
20403
|
+
break;
|
|
20087
20404
|
case "collapse":
|
|
20088
20405
|
for (const id of e.parts) {
|
|
20089
20406
|
breach.drop(id);
|
|
@@ -20125,7 +20442,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20125
20442
|
D.specOptions = specOptions || null;
|
|
20126
20443
|
D.spec = structure.spec(specOptions);
|
|
20127
20444
|
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] });
|
|
20128
|
-
|
|
20445
|
+
breach.claim();
|
|
20446
|
+
if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
|
|
20129
20447
|
off = D.world.on("*", handleEvent);
|
|
20130
20448
|
D.active = true;
|
|
20131
20449
|
return D;
|
|
@@ -20140,6 +20458,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20140
20458
|
};
|
|
20141
20459
|
D.follow = (world, id, spec) => {
|
|
20142
20460
|
D.spec = spec;
|
|
20461
|
+
breach.claim();
|
|
20462
|
+
let fallback = null;
|
|
20143
20463
|
const offFollow = world.on("*", (e) => {
|
|
20144
20464
|
if (e.object !== id) return;
|
|
20145
20465
|
switch (e.type) {
|
|
@@ -20149,6 +20469,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20149
20469
|
case "chip":
|
|
20150
20470
|
breach.chip(e);
|
|
20151
20471
|
break;
|
|
20472
|
+
case "spall":
|
|
20473
|
+
breach.spall(e);
|
|
20474
|
+
break;
|
|
20152
20475
|
case "fracture":
|
|
20153
20476
|
breach.drop(e.part);
|
|
20154
20477
|
break;
|
|
@@ -20158,10 +20481,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20158
20481
|
default:
|
|
20159
20482
|
break;
|
|
20160
20483
|
}
|
|
20484
|
+
if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
|
|
20485
|
+
fallback = null;
|
|
20486
|
+
breach.flush(Infinity);
|
|
20487
|
+
}, FOLLOW_FLUSH_FALLBACK_MS);
|
|
20161
20488
|
});
|
|
20162
20489
|
return { restore: () => breach.restore(), dispose: () => {
|
|
20163
20490
|
offFollow();
|
|
20164
20491
|
breach.restore();
|
|
20492
|
+
}, update: () => {
|
|
20493
|
+
breach.flush();
|
|
20165
20494
|
} };
|
|
20166
20495
|
};
|
|
20167
20496
|
D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
|
|
@@ -20194,6 +20523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20194
20523
|
return r;
|
|
20195
20524
|
};
|
|
20196
20525
|
D.animate = (dt) => {
|
|
20526
|
+
breach.flush();
|
|
20197
20527
|
if (R) return;
|
|
20198
20528
|
let moved = false;
|
|
20199
20529
|
for (const [id, list2] of bodies) {
|
|
@@ -21658,6 +21988,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
21658
21988
|
houseRenderer = create3(THREE, {
|
|
21659
21989
|
locale,
|
|
21660
21990
|
lod: { interior: 10, hide: 40 },
|
|
21991
|
+
breachPaintBudgetMs: 4,
|
|
21661
21992
|
...options.house,
|
|
21662
21993
|
quality: settings.quality,
|
|
21663
21994
|
...options.world ? { destruction: options.world } : {},
|
|
@@ -21813,9 +22144,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
21813
22144
|
host?.log(text.steps.no_door);
|
|
21814
22145
|
return;
|
|
21815
22146
|
}
|
|
21816
|
-
|
|
22147
|
+
const fo = target.target;
|
|
22148
|
+
const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
|
|
22149
|
+
if (reach && host?.reachFor) {
|
|
22150
|
+
host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
|
|
22151
|
+
} else p.house.interact(eyeOf(p));
|
|
21817
22152
|
host?.log(fill(text.steps.door, { text: String(target.text) }));
|
|
21818
22153
|
};
|
|
22154
|
+
const doorReach = (p, fo) => {
|
|
22155
|
+
const g = fo.gs[0];
|
|
22156
|
+
const pivot = fo.handles[0].pivot;
|
|
22157
|
+
const at = (angle) => {
|
|
22158
|
+
const c = Math.cos(angle);
|
|
22159
|
+
const s = Math.sin(angle);
|
|
22160
|
+
const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
|
|
22161
|
+
return g.parent ? g.parent.localToWorld(v) : v;
|
|
22162
|
+
};
|
|
22163
|
+
const camera = host?.camera;
|
|
22164
|
+
const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
|
|
22165
|
+
const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
|
|
22166
|
+
const closed = at(0);
|
|
22167
|
+
const open = at(fo.rot);
|
|
22168
|
+
const opening = !fo.open;
|
|
22169
|
+
return {
|
|
22170
|
+
handle: () => {
|
|
22171
|
+
g.updateMatrixWorld(true);
|
|
22172
|
+
return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
|
|
22173
|
+
},
|
|
22174
|
+
hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
|
|
22175
|
+
seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
|
|
22176
|
+
pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
|
|
22177
|
+
locked: !!(fo.locked || fo.blockedFlag)
|
|
22178
|
+
};
|
|
22179
|
+
};
|
|
21819
22180
|
const strike = () => {
|
|
21820
22181
|
const world = options.world;
|
|
21821
22182
|
if (!world || !host) return;
|