@adatechnology/game-floorplan-three 0.6.0 → 0.8.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.d.ts +5 -1
- package/dist/index.global.js +549 -98
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +389 -74
- package/dist/index.js.map +1 -1
- package/package.json +20 -21
package/dist/index.global.js
CHANGED
|
@@ -12693,6 +12693,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
12693
12693
|
},
|
|
12694
12694
|
hitDebris: "chips",
|
|
12695
12695
|
breakDebris: "dust",
|
|
12696
|
+
visual: { innerColor: 10197652 },
|
|
12696
12697
|
thermal: {
|
|
12697
12698
|
conductivity: 0.15,
|
|
12698
12699
|
heatCapacity: 2.5
|
|
@@ -12723,6 +12724,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
12723
12724
|
],
|
|
12724
12725
|
hitDebris: "chips",
|
|
12725
12726
|
breakDebris: "dust",
|
|
12727
|
+
visual: { innerColor: 12085567 },
|
|
12726
12728
|
thermal: {
|
|
12727
12729
|
conductivity: 0.15,
|
|
12728
12730
|
heatCapacity: 2.5
|
|
@@ -12753,6 +12755,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
12753
12755
|
],
|
|
12754
12756
|
hitDebris: "dust",
|
|
12755
12757
|
breakDebris: "dust",
|
|
12758
|
+
visual: { innerColor: 14999507 },
|
|
12756
12759
|
thermal: {
|
|
12757
12760
|
conductivity: 0.1,
|
|
12758
12761
|
heatCapacity: 1.5
|
|
@@ -12778,6 +12781,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
12778
12781
|
],
|
|
12779
12782
|
hitDebris: "chips",
|
|
12780
12783
|
breakDebris: "dust",
|
|
12784
|
+
visual: { innerColor: 9407364 },
|
|
12781
12785
|
thermal: {
|
|
12782
12786
|
conductivity: 0.2,
|
|
12783
12787
|
heatCapacity: 2.5
|
|
@@ -13519,6 +13523,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13519
13523
|
var FLAMMABLE_FLUIDS = ["gas", "fuel"];
|
|
13520
13524
|
var VENT_DIAMETER = 8e-3;
|
|
13521
13525
|
var FLAME_LIGHTS_LEAK_C = 470;
|
|
13526
|
+
var RESTING_HOLD_COS = Math.cos(Math.PI / 6);
|
|
13522
13527
|
var BREACH_RADIUS = 0.55;
|
|
13523
13528
|
var BREACH_BLAST_FACTOR = 6;
|
|
13524
13529
|
var BREACH_AREA = 0.25;
|
|
@@ -13534,7 +13539,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13534
13539
|
var BREACH_GRID_CAP = 4096;
|
|
13535
13540
|
var CHIP_ENERGY = 200;
|
|
13536
13541
|
var CHIP_SPLASH = 0.5;
|
|
13542
|
+
var CRATER_SPENT = 0.6;
|
|
13537
13543
|
var BAR_BOW = 0.08;
|
|
13544
|
+
var BULLET_JOINT_SPAN = 0.3;
|
|
13538
13545
|
var CUT_SLICE = 1;
|
|
13539
13546
|
var cutFactorOf = (shape) => clamp2(Math.max(...shapeDims(shape)) / CUT_SLICE, 1, 50);
|
|
13540
13547
|
var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
@@ -13553,12 +13560,22 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13553
13560
|
const cells = p.st.chipped;
|
|
13554
13561
|
if (!cells?.length) return 0;
|
|
13555
13562
|
const [u, v] = planeAxes(p.thin);
|
|
13556
|
-
const
|
|
13563
|
+
const role = p.spec.structure?.role;
|
|
13564
|
+
const up = rotateVec(p.q, [0, 0, 0].map((_, k) => k === v ? 1 : 0));
|
|
13565
|
+
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;
|
|
13566
|
+
const along = (role === "wall" || role === "partition" || role === "column") && upright >= 0 ? upright : p.dims[u] >= p.dims[v] ? u : v;
|
|
13557
13567
|
const across = along === u ? v : u;
|
|
13558
13568
|
let worst = 0;
|
|
13559
13569
|
for (const c of cells) {
|
|
13560
|
-
|
|
13561
|
-
for (const d of cells) if (Math.abs(d.center[along] - c.center[along]) < d.size[along] / 2 + 1e-6)
|
|
13570
|
+
const spans = [];
|
|
13571
|
+
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]);
|
|
13572
|
+
spans.sort((a, b) => a[0] - b[0]);
|
|
13573
|
+
let gone = 0, end = -Infinity;
|
|
13574
|
+
for (const [a, b] of spans) {
|
|
13575
|
+
if (b <= end) continue;
|
|
13576
|
+
gone += b - Math.max(a, end);
|
|
13577
|
+
end = b;
|
|
13578
|
+
}
|
|
13562
13579
|
worst = Math.max(worst, gone / Math.max(p.dims[across], 1e-6));
|
|
13563
13580
|
}
|
|
13564
13581
|
return Math.min(1, worst);
|
|
@@ -13854,7 +13871,18 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
13854
13871
|
if (k < 0 || j.spec.kind !== "tie") return k;
|
|
13855
13872
|
return partXf(o, o.parts[k]).c[1] > partXf(o, o.parts[r]).c[1] + 1e-6 ? k : -1;
|
|
13856
13873
|
};
|
|
13874
|
+
let poseVersion = 0;
|
|
13875
|
+
const contactMemo = /* @__PURE__ */ new WeakMap();
|
|
13857
13876
|
const contactBox = (p, support) => {
|
|
13877
|
+
let byTouching = contactMemo.get(p);
|
|
13878
|
+
if (!byTouching) contactMemo.set(p, byTouching = /* @__PURE__ */ new Map());
|
|
13879
|
+
const known = byTouching.get(support);
|
|
13880
|
+
if (known && known.version === poseVersion) return known.box;
|
|
13881
|
+
const box3 = measureContact(p, support);
|
|
13882
|
+
byTouching.set(support, { version: poseVersion, box: box3 });
|
|
13883
|
+
return box3;
|
|
13884
|
+
};
|
|
13885
|
+
const measureContact = (p, support) => {
|
|
13858
13886
|
const A = aabbOf(p.spec.shape, p.pos, p.q), B = aabbOf(support.spec.shape, support.pos, support.q);
|
|
13859
13887
|
const lo = [0, 0, 0], hi = [0, 0, 0];
|
|
13860
13888
|
for (let k = 0; k < 3; k++) {
|
|
@@ -14649,7 +14677,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14649
14677
|
p.st.holes.push(hole);
|
|
14650
14678
|
if (p.st.holes.length > maxHoles) p.st.holes.shift();
|
|
14651
14679
|
markDamaged(p);
|
|
14652
|
-
emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {} });
|
|
14680
|
+
emit({ ...base(o, src), type: "hole", part: p.spec.id, hole, worldPoint: wp, ...exit ? { exitPoint: exit } : {}, ...p.spec.powder ? { powder: { ...p.spec.powder } } : {} });
|
|
14653
14681
|
setStage(o, p, 1, {}, src);
|
|
14654
14682
|
return hole;
|
|
14655
14683
|
};
|
|
@@ -14661,11 +14689,27 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14661
14689
|
emit({ ...base(o, src), type: "crack", part: p.spec.id, crack, worldPoint: wp });
|
|
14662
14690
|
setStage(o, p, 1, {}, src);
|
|
14663
14691
|
};
|
|
14664
|
-
const debris = (o, p, kind, point, normal, count, speed, src) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed });
|
|
14692
|
+
const debris = (o, p, kind, point, normal, count, speed, src, material) => emit({ ...base(o, src), type: "debris", part: p.spec.id, kind, point, normal, count, speed, ...material && material !== p.spec.material ? { material } : {} });
|
|
14665
14693
|
const holeRate = (hole, pressurized) => (pressurized ? 0.5 : 0.1) * (hole.diameter / 9e-3);
|
|
14666
14694
|
const detachEvents = (o, opened, by, src) => {
|
|
14667
14695
|
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]) });
|
|
14668
14696
|
};
|
|
14697
|
+
const slideOffResting = (o, rotation) => {
|
|
14698
|
+
const upY = 1 - 2 * (rotation[0] * rotation[0] + rotation[2] * rotation[2]);
|
|
14699
|
+
if (upY >= RESTING_HOLD_COS) return;
|
|
14700
|
+
const opened = o.joints.filter((j) => j.spec.kind === "resting" && !j.st.open);
|
|
14701
|
+
if (opened.length === 0) return;
|
|
14702
|
+
run(() => {
|
|
14703
|
+
for (const j of opened) {
|
|
14704
|
+
j.st.open = true;
|
|
14705
|
+
j.st.openedBy = "break";
|
|
14706
|
+
}
|
|
14707
|
+
solveStructure(o);
|
|
14708
|
+
recomputeGroups(o);
|
|
14709
|
+
detachEvents(o, opened, "break");
|
|
14710
|
+
collapseCheck(o);
|
|
14711
|
+
});
|
|
14712
|
+
};
|
|
14669
14713
|
const checkDestroyed = (o, src) => {
|
|
14670
14714
|
if (!o.st.destroyed && o.parts.length > 0 && o.parts.every(isLost)) {
|
|
14671
14715
|
o.st.destroyed = true;
|
|
@@ -14702,7 +14746,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14702
14746
|
const small = pa.dims[0] * pa.dims[1] * pa.dims[2] <= pb.dims[0] * pb.dims[1] * pb.dims[2] ? pa : pb;
|
|
14703
14747
|
return Math.max(0.06, 0.5 * Math.min(small.dims[0], small.dims[1], small.dims[2]));
|
|
14704
14748
|
};
|
|
14705
|
-
const
|
|
14749
|
+
const jointLength = (o, j) => {
|
|
14750
|
+
const A = partWorldAabb(o, o.parts[j.ia]), B = partWorldAabb(o, o.parts[j.ib]);
|
|
14751
|
+
let len = 0;
|
|
14752
|
+
for (let k = 0; k < 3; k++) len = Math.max(len, Math.min(A.max[k], B.max[k]) - Math.max(A.min[k], B.min[k]));
|
|
14753
|
+
return len;
|
|
14754
|
+
};
|
|
14755
|
+
const wearJoints = (o, p, amount, near, radius = 0.06, src, split = false, span) => {
|
|
14706
14756
|
if (amount <= 0) return;
|
|
14707
14757
|
const mine = (o.byPart.get(p.spec.id) ?? []).filter((j) => !j.st.open);
|
|
14708
14758
|
if (split && mine.length) amount /= mine.length;
|
|
@@ -14710,7 +14760,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14710
14760
|
if (j.st.open) continue;
|
|
14711
14761
|
if (j.spec.kind === "rebar" && (isReinforced(o.parts[j.ia].spec) || isReinforced(o.parts[j.ib].spec))) continue;
|
|
14712
14762
|
if (near && jointReach(o, j, near) > Math.max(radius, jointReachLimit(o, j))) continue;
|
|
14713
|
-
j.st.hp -= amount;
|
|
14763
|
+
j.st.hp -= span === void 0 ? amount : amount * Math.min(1, span / Math.max(jointLength(o, j), 1e-6));
|
|
14714
14764
|
if (j.st.hp <= 0) {
|
|
14715
14765
|
j.st.hp = 0;
|
|
14716
14766
|
j.st.open = true;
|
|
@@ -14739,7 +14789,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14739
14789
|
if (spalls.length > MAX_SPALLS) spalls.shift();
|
|
14740
14790
|
markDamaged(p);
|
|
14741
14791
|
emit({ ...base(o, src), type: "spall", part: p.spec.id, layer: layer.index, point: wp, area: Math.PI * radius * radius });
|
|
14742
|
-
debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src);
|
|
14792
|
+
debris(o, p, layer.mat.hitDebris, wp, normal, 5, 2, src, layer.mat.id);
|
|
14743
14793
|
setStage(o, p, 1, {}, src);
|
|
14744
14794
|
};
|
|
14745
14795
|
const crater = (o, p, wp, normal, radius, src) => {
|
|
@@ -14760,7 +14810,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14760
14810
|
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 }));
|
|
14761
14811
|
const unitsOf2 = (p) => p.spec.units ?? { size: [p.mat.fragmentSize?.[1] ?? 0.2, p.mat.fragmentSize?.[1] ?? 0.2, p.thickness], bond: "stack" };
|
|
14762
14812
|
const FRAGMENT_SHAPE = { shatter: "shard", splinter: "splinter", tear: "scrap" };
|
|
14763
|
-
const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag) => {
|
|
14813
|
+
const cellFragments = (o, p, cells, hitW, dir, speed, mass, tag, inner = false) => {
|
|
14764
14814
|
const { c, q } = partXf(o, p);
|
|
14765
14815
|
const holes = p.st.holes.length > 0 ? localHolesOf(p) : [];
|
|
14766
14816
|
const [u, v] = planeAxes(p.thin);
|
|
@@ -14784,7 +14834,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14784
14834
|
velocity: [dv[0] * k, dv[1] * k + 0.5 * rnd(), dv[2] * k],
|
|
14785
14835
|
angularVelocity: [(rnd() - 0.5) * 8, (rnd() - 0.5) * 8, (rnd() - 0.5) * 8],
|
|
14786
14836
|
mass: mass * faceArea(cell) / total,
|
|
14787
|
-
...marks.length > 0 ? { marks } : {}
|
|
14837
|
+
...marks.length > 0 ? { marks } : {},
|
|
14838
|
+
...inner ? { inner } : {}
|
|
14788
14839
|
};
|
|
14789
14840
|
});
|
|
14790
14841
|
};
|
|
@@ -14825,7 +14876,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14825
14876
|
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);
|
|
14826
14877
|
};
|
|
14827
14878
|
const chipUnits = (o, p, kind, wp, hitEnergy, dir, radius, src) => {
|
|
14828
|
-
var _a, _b;
|
|
14879
|
+
var _a, _b, _c;
|
|
14829
14880
|
if (!p.spec.units || !p.staged || chipEnergy <= 0 || p.st.stripped || isLost(p) || (p.st.stage ?? 0) >= 2) return;
|
|
14830
14881
|
const rule = p.mat.hits?.[kind];
|
|
14831
14882
|
const perKg = rule?.unitJoules ?? chipEnergy;
|
|
@@ -14835,7 +14886,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14835
14886
|
const lp = toLocal(p.pos, p.q, objLocal(o, wp));
|
|
14836
14887
|
const grid = gridOf(p);
|
|
14837
14888
|
const worn = (_a = p.rt).chips ?? (_a.chips = {});
|
|
14889
|
+
const cut = (_b = p.rt).craters ?? (_b.craters = {});
|
|
14838
14890
|
const out = [];
|
|
14891
|
+
const shot = kind === "ballistic";
|
|
14839
14892
|
for (let i = 0; i < grid.length; i++) {
|
|
14840
14893
|
const cell = grid[i];
|
|
14841
14894
|
if (worn[i] === -1) continue;
|
|
@@ -14848,18 +14901,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14848
14901
|
worn[i] = total;
|
|
14849
14902
|
continue;
|
|
14850
14903
|
}
|
|
14851
|
-
|
|
14852
|
-
|
|
14904
|
+
if (!shot) {
|
|
14905
|
+
worn[i] = -1;
|
|
14906
|
+
out.push(cell);
|
|
14907
|
+
continue;
|
|
14908
|
+
}
|
|
14909
|
+
const size = [cell.size[0], cell.size[1], cell.size[2]];
|
|
14910
|
+
const center = [cell.center[0], cell.center[1], cell.center[2]];
|
|
14911
|
+
for (const a of [u, v]) {
|
|
14912
|
+
const w = Math.min(cell.size[a], craterAcross(hitEnergy) * (0.85 + 0.3 * rnd()));
|
|
14913
|
+
const lo = cell.center[a] - cell.size[a] / 2 + w / 2, hi = cell.center[a] + cell.size[a] / 2 - w / 2;
|
|
14914
|
+
size[a] = w;
|
|
14915
|
+
center[a] = clamp2(lp[a], lo, hi);
|
|
14916
|
+
}
|
|
14917
|
+
const taken2 = (cut[i] ?? 0) + size[u] * size[v];
|
|
14918
|
+
if (taken2 >= cell.size[u] * cell.size[v] * CRATER_SPENT) {
|
|
14919
|
+
worn[i] = -1;
|
|
14920
|
+
out.push({ ...cell, crater: true });
|
|
14921
|
+
continue;
|
|
14922
|
+
}
|
|
14923
|
+
worn[i] = 0;
|
|
14924
|
+
cut[i] = taken2;
|
|
14925
|
+
out.push({ center, size, crater: true });
|
|
14853
14926
|
}
|
|
14854
14927
|
if (out.length === 0) return;
|
|
14855
14928
|
const { c, q } = partXf(o, p);
|
|
14856
|
-
const chipped = (
|
|
14929
|
+
const chipped = (_c = p.st).chipped ?? (_c.chipped = []);
|
|
14857
14930
|
const holes = localHolesOf(p);
|
|
14858
14931
|
for (const cell of out) {
|
|
14859
14932
|
const mass = Math.min(p.st.mass, p.mat.density * cell.size[0] * cell.size[1] * cell.size[2]);
|
|
14860
14933
|
const center = toWorld(c, q, cell.center);
|
|
14861
14934
|
const speed = clamp2(Math.sqrt(2 * energy * 0.05 / Math.max(mass, 0.05)), 0.5, 6);
|
|
14862
|
-
const
|
|
14935
|
+
const tag = `c${chipped.length}_`;
|
|
14936
|
+
const fragments = cell.crater ? craterFragments(o, p, cell, dir, speed, mass, tag) : cellFragments(o, p, [cell], wp, dir, speed, mass, tag, true);
|
|
14863
14937
|
p.st.holes = p.st.holes.filter((_, k) => marksInBox(cell.center, cell.size, void 0, [holes[k]]).length === 0);
|
|
14864
14938
|
chipped.push({ center: [cell.center[0], cell.center[1], cell.center[2]], size: [cell.size[0], cell.size[1], cell.size[2]] });
|
|
14865
14939
|
p.st.mass = Math.max(0, p.st.mass - mass);
|
|
@@ -14876,6 +14950,44 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
14876
14950
|
collapseCheck(o, src);
|
|
14877
14951
|
}
|
|
14878
14952
|
};
|
|
14953
|
+
const craterAcross = (energy) => clamp2(0.03 + 12e-4 * Math.sqrt(Math.max(0, energy)), 0.03, 0.14);
|
|
14954
|
+
const craterFragments = (o, p, cell, dir, speed, mass, tag) => {
|
|
14955
|
+
const { c, q } = partXf(o, p);
|
|
14956
|
+
const [u, v] = planeAxes(p.thin);
|
|
14957
|
+
const d = dir ?? rotateVec(q, [p.thin === 0 ? 1 : 0, p.thin === 1 ? 1 : 0, p.thin === 2 ? 1 : 0]);
|
|
14958
|
+
const exitSign = Math.sign(rotateVec(quat.conjugate(q), d)[p.thin]) || 1;
|
|
14959
|
+
const bit = Math.max(0.01, Math.min(cell.size[u], cell.size[v]) / 3);
|
|
14960
|
+
const n = clamp2(Math.round(cell.size[u] * cell.size[v] / (bit * bit * 3)), 4, 10) + Math.floor(rnd() * 3);
|
|
14961
|
+
const out = [];
|
|
14962
|
+
for (let i = 0; i < n; i++) {
|
|
14963
|
+
const back = i % 3 === 2;
|
|
14964
|
+
const lc = [cell.center[0], cell.center[1], cell.center[2]];
|
|
14965
|
+
lc[u] += (rnd() - 0.5) * cell.size[u] * 0.7;
|
|
14966
|
+
lc[v] += (rnd() - 0.5) * cell.size[v] * 0.7;
|
|
14967
|
+
lc[p.thin] += (back ? -exitSign : exitSign) * (p.thickness / 2 + 0.01);
|
|
14968
|
+
const size = [0, 0, 0];
|
|
14969
|
+
size[u] = bit * (0.6 + 0.8 * rnd());
|
|
14970
|
+
size[v] = bit * (0.5 + 0.7 * rnd());
|
|
14971
|
+
size[p.thin] = bit * (0.4 + 0.6 * rnd());
|
|
14972
|
+
const spread3 = [(rnd() - 0.5) * 0.9, (rnd() - 0.3) * 0.9, (rnd() - 0.5) * 0.9];
|
|
14973
|
+
const k = speed * (back ? 0.4 + 0.4 * rnd() : 0.7 + 0.8 * rnd());
|
|
14974
|
+
const vdir = vec.normalize(vec.add(back ? vec.scale(d, -1) : d, spread3));
|
|
14975
|
+
out.push({
|
|
14976
|
+
id: `${p.spec.id}#${tag}${i}`,
|
|
14977
|
+
part: p.spec.id,
|
|
14978
|
+
material: p.spec.material,
|
|
14979
|
+
shape: "chunk",
|
|
14980
|
+
center: toWorld(c, q, lc),
|
|
14981
|
+
size,
|
|
14982
|
+
rotation: q,
|
|
14983
|
+
velocity: [vdir[0] * k, vdir[1] * k + 0.5 * rnd(), vdir[2] * k],
|
|
14984
|
+
angularVelocity: [(rnd() - 0.5) * 20, (rnd() - 0.5) * 20, (rnd() - 0.5) * 20],
|
|
14985
|
+
mass: mass / n,
|
|
14986
|
+
inner: true
|
|
14987
|
+
});
|
|
14988
|
+
}
|
|
14989
|
+
return out;
|
|
14990
|
+
};
|
|
14879
14991
|
const exposeRebar = (o, p, src) => {
|
|
14880
14992
|
const r = p.spec.reinforcement;
|
|
14881
14993
|
if (!r) return;
|
|
@@ -15163,7 +15275,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15163
15275
|
});
|
|
15164
15276
|
}
|
|
15165
15277
|
const patch = [scale * 4, scale * 4, scale * 4];
|
|
15166
|
-
emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments });
|
|
15278
|
+
emit({ ...base(o, src), type: "chip", part: p.spec.id, center: exit ?? entry2, size: patch, rotation: q, fragments, torn: true });
|
|
15167
15279
|
};
|
|
15168
15280
|
const ballistic = (o, p, entry2, n, d, E, caliber, pathLen, src, round3 = {}) => {
|
|
15169
15281
|
const mat = p.mat;
|
|
@@ -15222,12 +15334,13 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15222
15334
|
} else if (mat.ductility >= 0.5) {
|
|
15223
15335
|
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);
|
|
15224
15336
|
} else if (brittle) {
|
|
15225
|
-
addCrack(o, p, entry2, n, crackLen, src);
|
|
15337
|
+
if (!p.spec.units) addCrack(o, p, entry2, n, crackLen, src);
|
|
15226
15338
|
if (mat.fracture !== "shatter") addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
|
|
15227
15339
|
} else addHole(o, p, entry2, n, caliber / 1e3, false, void 0, src);
|
|
15228
15340
|
debris(o, p, mat.hitDebris, entry2, n, clamp2(Math.round(E / 60) + 2, 2, 14), clamp2(Math.sqrt(E) / 6, 1, 8), src);
|
|
15229
|
-
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true);
|
|
15230
|
-
const
|
|
15341
|
+
wearJoints(o, p, (penetrated ? thr : E) * 0.02, void 0, 0, src, true, BULLET_JOINT_SPAN);
|
|
15342
|
+
const hollowTear = mat.fracture === "tear" && p.spec.fill !== void 0 && p.spec.fill < 0.5;
|
|
15343
|
+
const absorbed = penetrated && (mat.ductility >= 0.5 && mat.fracture !== "tear" || hollowTear) ? Math.min(E, thr) : E;
|
|
15231
15344
|
const dmg = applyDamage(o, p, absorbed * DMG_PER_J * scale, "ballistic", entry2, d, absorbed * 0.1, 12, src);
|
|
15232
15345
|
if (!isLost(p)) tearAt(o, p, "ballistic", entry2, penetrated ? exitPoint : void 0, d, E, caliber / 1e3, src);
|
|
15233
15346
|
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);
|
|
@@ -15312,10 +15425,25 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15312
15425
|
if (dist <= BREACH_BLAST_REACH * Math.cbrt(Math.max(0, energy) / 1e3)) addBreach(o, p, wp, breachAreaOf(p, dealt, true), Ein, null, src);
|
|
15313
15426
|
return reach;
|
|
15314
15427
|
};
|
|
15428
|
+
const blastProbe = { partsTested: 0, partsNear: 0 };
|
|
15429
|
+
const blastCandidates = (o, point, radius) => {
|
|
15430
|
+
const local = toLocal(o.st.position, o.st.rotation, point);
|
|
15431
|
+
const near = [];
|
|
15432
|
+
for (const p of o.parts) {
|
|
15433
|
+
const reach = radius + p.bound;
|
|
15434
|
+
const dx = local[0] - p.pos[0];
|
|
15435
|
+
const dy = local[1] - p.pos[1];
|
|
15436
|
+
const dz = local[2] - p.pos[2];
|
|
15437
|
+
if (dx * dx + dy * dy + dz * dz < reach * reach) near.push(p);
|
|
15438
|
+
}
|
|
15439
|
+
blastProbe.partsTested += o.parts.length;
|
|
15440
|
+
blastProbe.partsNear += near.length;
|
|
15441
|
+
return near;
|
|
15442
|
+
};
|
|
15315
15443
|
const blastObject = (o, point, energy, radius, src) => {
|
|
15316
15444
|
const start = cur.length;
|
|
15317
15445
|
const res = { object: o.id, damage: 0, penetrated: false, exitEnergy: 0, events: [] };
|
|
15318
|
-
for (const p of o
|
|
15446
|
+
for (const p of blastCandidates(o, point, radius)) blastPart(o, p, point, energy, radius, src, res);
|
|
15319
15447
|
res.events = cur.slice(start);
|
|
15320
15448
|
return res;
|
|
15321
15449
|
};
|
|
@@ -15439,7 +15567,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15439
15567
|
let order = 0;
|
|
15440
15568
|
for (const o of inRange) {
|
|
15441
15569
|
if (only && only !== o) continue;
|
|
15442
|
-
for (const p of o
|
|
15570
|
+
for (const p of blastCandidates(o, point, radius)) {
|
|
15443
15571
|
if (isLost(p)) continue;
|
|
15444
15572
|
const dist = distToPart(o, p, point);
|
|
15445
15573
|
if (dist < radius) items.push({ dist, order: order++, o, p });
|
|
@@ -15986,6 +16114,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
15986
16114
|
const p = getPart(o, partId);
|
|
15987
16115
|
p.pos = [position[0], position[1], position[2]];
|
|
15988
16116
|
p.q = q;
|
|
16117
|
+
poseVersion++;
|
|
15989
16118
|
updateBound(o);
|
|
15990
16119
|
},
|
|
15991
16120
|
remove(id) {
|
|
@@ -16004,6 +16133,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16004
16133
|
const o = getObj(id);
|
|
16005
16134
|
o.base = { pos: [position[0], position[1], position[2]], rot: [rotation[0], rotation[1], rotation[2], rotation[3]] };
|
|
16006
16135
|
applySoilPose(o);
|
|
16136
|
+
slideOffResting(o, rotation);
|
|
16007
16137
|
},
|
|
16008
16138
|
splitGroup(id, group, newId) {
|
|
16009
16139
|
if (impls.has(id)) throw new Error(`object "${id}" is a registered DestructibleImpl and cannot be split; its module owns its pieces`);
|
|
@@ -16027,7 +16157,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16027
16157
|
if (o.spec.name !== void 0) s.name = o.spec.name;
|
|
16028
16158
|
if (o.spec.tags) s.tags = [...o.spec.tags];
|
|
16029
16159
|
if (o.spec.soil !== void 0) s.soil = o.spec.soil;
|
|
16030
|
-
return
|
|
16160
|
+
return s;
|
|
16031
16161
|
};
|
|
16032
16162
|
const inNew = (pid) => moved.has(pid);
|
|
16033
16163
|
const inOld = (pid) => !moved.has(pid);
|
|
@@ -16054,7 +16184,12 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16054
16184
|
const p = getPart(o, partId);
|
|
16055
16185
|
return whole(() => doHit(o, h, p));
|
|
16056
16186
|
},
|
|
16187
|
+
blastProbe() {
|
|
16188
|
+
return { ...blastProbe };
|
|
16189
|
+
},
|
|
16057
16190
|
explode(point, energy, radius, source, blast) {
|
|
16191
|
+
blastProbe.partsTested = 0;
|
|
16192
|
+
blastProbe.partsNear = 0;
|
|
16058
16193
|
return withCollapses(run(() => blastWorld(point, energy, radius, source, blast)));
|
|
16059
16194
|
},
|
|
16060
16195
|
raycast(origin, direction, maxDistance = Infinity) {
|
|
@@ -16064,7 +16199,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
16064
16199
|
sweepBox(box3, direction, distance2, options2 = {}) {
|
|
16065
16200
|
const d = vec.normalize(direction);
|
|
16066
16201
|
const ignored = options2.ignore === void 0 ? void 0 : objects.get(options2.ignore);
|
|
16067
|
-
const
|
|
16202
|
+
const hosted = options2.ignore === void 0 || ignored ? void 0 : impls.get(options2.ignore);
|
|
16203
|
+
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;
|
|
16068
16204
|
let best = null;
|
|
16069
16205
|
for (const origin of sweepOrigins(box3, d, options2.spacing ?? 0.2)) {
|
|
16070
16206
|
const r = raycastInternal(origin, d, distance2 + SWEEP_BACK, ignore);
|
|
@@ -19578,6 +19714,10 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19578
19714
|
function collapseRange(mesh2, v0, v1) {
|
|
19579
19715
|
const pos = mesh2.geometry.attributes.position;
|
|
19580
19716
|
const a = pos.array;
|
|
19717
|
+
const u = mesh2.userData;
|
|
19718
|
+
if (!u._p0) u._p0 = a.slice();
|
|
19719
|
+
const list2 = u.collapsedRanges ?? (u.collapsedRanges = []);
|
|
19720
|
+
if (!list2.some((r) => r[0] === v0 && r[1] === v1)) list2.push([v0, v1]);
|
|
19581
19721
|
const saved = a.slice(v0 * 3, v1 * 3);
|
|
19582
19722
|
const x = a[v0 * 3], y = a[v0 * 3 + 1], z = a[v0 * 3 + 2];
|
|
19583
19723
|
for (let k = v0; k < v1; k++) {
|
|
@@ -19592,65 +19732,57 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19592
19732
|
const pos = mesh2.geometry.attributes.position;
|
|
19593
19733
|
pos.array.set(saved, v0 * 3);
|
|
19594
19734
|
pos.needsUpdate = true;
|
|
19595
|
-
|
|
19735
|
+
const list2 = mesh2.userData.collapsedRanges;
|
|
19736
|
+
if (list2) mesh2.userData.collapsedRanges = list2.filter((r) => r[0] !== v0);
|
|
19737
|
+
}
|
|
19738
|
+
function noise2(x, y, cell) {
|
|
19739
|
+
const h = (i2, j2) => {
|
|
19740
|
+
const s = Math.sin(i2 * 127.1 + j2 * 311.7) * 43758.5453;
|
|
19741
|
+
return s - Math.floor(s);
|
|
19742
|
+
};
|
|
19743
|
+
const fx = x / cell, fy = y / cell, i = Math.floor(fx), j = Math.floor(fy), u = fx - i, v = fy - j;
|
|
19744
|
+
const su = u * u * (3 - 2 * u), sv = v * v * (3 - 2 * v);
|
|
19745
|
+
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;
|
|
19746
|
+
}
|
|
19747
|
+
var MASK_TEXEL = 0.02;
|
|
19748
|
+
var SPALL_MIN = 0.025;
|
|
19749
|
+
var SPALL_RAGGED = 0.045;
|
|
19750
|
+
var COVER2 = 0.015;
|
|
19751
|
+
var MAX_FLAKES = 64;
|
|
19596
19752
|
function installDestructionBreach(K, _shared) {
|
|
19597
19753
|
const { THREE, fragmentMaterial, invalidate, invalidateShadow } = K;
|
|
19598
19754
|
function createBreachView(opts) {
|
|
19599
19755
|
const { house, structure } = opts;
|
|
19600
19756
|
const views = /* @__PURE__ */ new Map();
|
|
19757
|
+
const dirty = /* @__PURE__ */ new Set();
|
|
19758
|
+
const paintBudgetMs = opts.paintBudgetMs ?? Infinity;
|
|
19759
|
+
const dropped = [];
|
|
19601
19760
|
let lod = 0;
|
|
19602
|
-
const unitMaterial = new THREE.MeshLambertMaterial({ color: 16777215 });
|
|
19603
19761
|
const unitGeometry = new THREE.BoxGeometry(1, 1, 1);
|
|
19604
19762
|
const tmp = { m: new THREE.Matrix4(), q: new THREE.Quaternion(), p: new THREE.Vector3(), s: new THREE.Vector3(), c: new THREE.Color() };
|
|
19605
|
-
const wallColour = (items) => {
|
|
19606
|
-
for (const it of items) {
|
|
19607
|
-
if (!it.mesh) continue;
|
|
19608
|
-
const c = it.mesh.geometry.attributes.color;
|
|
19609
|
-
if (!c) continue;
|
|
19610
|
-
const a = c.array;
|
|
19611
|
-
tmp.c.setRGB(a[it.v0 * 3], a[it.v0 * 3 + 1], a[it.v0 * 3 + 2]);
|
|
19612
|
-
return tmp.c.getHex();
|
|
19613
|
-
}
|
|
19614
|
-
return null;
|
|
19615
|
-
};
|
|
19616
19763
|
const local = (p) => {
|
|
19617
19764
|
house.updateMatrixWorld(true);
|
|
19618
19765
|
const v = house.worldToLocal(tmp.p.set(p[0], p[1], p[2]));
|
|
19619
19766
|
return [v.x, v.y, v.z];
|
|
19620
19767
|
};
|
|
19621
|
-
function
|
|
19768
|
+
function cells(part, units) {
|
|
19622
19769
|
const [u0, v0, u1, v1] = part.aabb;
|
|
19623
19770
|
const alongU = u1 - u0 >= v1 - v0;
|
|
19624
19771
|
const len = alongU ? u1 - u0 : v1 - v0;
|
|
19625
|
-
const thick = alongU ? v1 - v0 : u1 - u0;
|
|
19626
19772
|
const [ul, uh] = units.size;
|
|
19627
19773
|
const rows = Math.max(1, Math.round((part.y1 - part.y0) / uh));
|
|
19628
19774
|
const rowH = (part.y1 - part.y0) / rows;
|
|
19629
19775
|
const per = Math.max(1, Math.round(len / ul));
|
|
19630
19776
|
const w = len / per;
|
|
19631
|
-
const
|
|
19632
|
-
const list2 = [];
|
|
19777
|
+
const out = [];
|
|
19633
19778
|
for (let r = 0; r < rows; r++) {
|
|
19634
19779
|
const shift = units.bond === "running" && r & 1 ? w / 2 : 0;
|
|
19635
|
-
|
|
19636
|
-
|
|
19637
|
-
|
|
19638
|
-
a = Math.max(0, a);
|
|
19639
|
-
b = Math.min(len, b);
|
|
19640
|
-
if (b - a < 1e-3) continue;
|
|
19641
|
-
const mid = (a + b) / 2;
|
|
19642
|
-
const x = alongU ? u0 + mid : (u0 + u1) / 2;
|
|
19643
|
-
const z = alongU ? (v0 + v1) / 2 : v0 + mid;
|
|
19644
|
-
const y = part.y0 + (r + 0.5) * rowH;
|
|
19645
|
-
const d = opening ? Math.hypot(x - opening.center[0], y - opening.center[1], z - opening.center[2]) : Infinity;
|
|
19646
|
-
if (opening && d < opening.radius) continue;
|
|
19647
|
-
const gone = (c) => Math.abs(c[1] - y) <= rowH / 2 && (alongU ? c[0] >= u0 + a && c[0] <= u0 + b : c[2] >= v0 + a && c[2] <= v0 + b);
|
|
19648
|
-
if (chips.some(gone)) continue;
|
|
19649
|
-
const near = opening && d < opening.radius + 0.25 || chips.some((c) => Math.hypot(c[0] - x, c[1] - y, c[2] - z) < 0.15);
|
|
19650
|
-
list2.push({ x, y, z, sx: alongU ? b - a : thick, sy: rowH, sz: alongU ? thick : b - a, colour: near || paint === null ? edge : paint });
|
|
19780
|
+
for (let i = 0; i < per + (shift ? 1 : 0); i++) {
|
|
19781
|
+
const a = Math.max(0, i * w - shift), b = Math.min(len, i * w - shift + w);
|
|
19782
|
+
if (b - a > 1e-3) out.push({ a, b, y0: part.y0 + r * rowH, y1: part.y0 + (r + 1) * rowH, r, i });
|
|
19651
19783
|
}
|
|
19652
19784
|
}
|
|
19653
|
-
return
|
|
19785
|
+
return out;
|
|
19654
19786
|
}
|
|
19655
19787
|
const specOf = (id) => opts.spec()?.parts?.find((q) => q.id === id);
|
|
19656
19788
|
const same = (a, b) => a.every((x, k) => Math.abs(x - b[k]) < 1e-3);
|
|
@@ -19664,6 +19796,200 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19664
19796
|
return null;
|
|
19665
19797
|
}
|
|
19666
19798
|
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);
|
|
19799
|
+
function shell(it, mask) {
|
|
19800
|
+
const g0 = it.mesh.geometry, g = new THREE.BufferGeometry();
|
|
19801
|
+
const src = it.mesh.userData._p0 || g0.attributes.position.array;
|
|
19802
|
+
for (const [name, attr] of Object.entries(g0.attributes)) {
|
|
19803
|
+
const k = attr.itemSize;
|
|
19804
|
+
g.setAttribute(name, new THREE.BufferAttribute((name === "position" ? src : attr.array).slice(it.v0 * k, it.v1 * k), k));
|
|
19805
|
+
}
|
|
19806
|
+
if (g0.index) {
|
|
19807
|
+
const ix = g0.index.array.slice(it.i0, it.i1);
|
|
19808
|
+
for (let k = 0; k < ix.length; k++) ix[k] -= it.v0;
|
|
19809
|
+
g.setIndex(new THREE.BufferAttribute(ix, 1));
|
|
19810
|
+
}
|
|
19811
|
+
g.computeBoundingBox();
|
|
19812
|
+
g.computeBoundingSphere();
|
|
19813
|
+
const base = it.mesh.material;
|
|
19814
|
+
const material = base.clone();
|
|
19815
|
+
const toHouse = { value: new THREE.Matrix4() };
|
|
19816
|
+
material.onBeforeCompile = (sh) => {
|
|
19817
|
+
base.onBeforeCompile?.(sh);
|
|
19818
|
+
sh.uniforms.uToHouse = toHouse;
|
|
19819
|
+
sh.uniforms.uMask = { value: mask.texture };
|
|
19820
|
+
sh.uniforms.uMaskRect = { value: mask.rect };
|
|
19821
|
+
sh.uniforms.uMaskAlongX = { value: mask.alongU ? 1 : 0 };
|
|
19822
|
+
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;");
|
|
19823
|
+
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;");
|
|
19824
|
+
};
|
|
19825
|
+
material.customProgramCacheKey = () => "breachMask" + (base.customProgramCacheKey?.() ?? "");
|
|
19826
|
+
const mesh2 = new THREE.Mesh(g, material);
|
|
19827
|
+
mesh2.name = "breach-shell";
|
|
19828
|
+
mesh2.userData.breachMask = mask;
|
|
19829
|
+
mesh2.userData.cutAt = (p) => {
|
|
19830
|
+
const v = local([p.x, p.y, p.z]);
|
|
19831
|
+
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);
|
|
19832
|
+
return i >= 0 && i < mask.w && j >= 0 && j < mask.h && mask.data[j * mask.w + i] > 127;
|
|
19833
|
+
};
|
|
19834
|
+
mesh2.position.copy(it.mesh.position);
|
|
19835
|
+
mesh2.quaternion.copy(it.mesh.quaternion);
|
|
19836
|
+
mesh2.scale.copy(it.mesh.scale);
|
|
19837
|
+
mesh2.castShadow = it.mesh.castShadow;
|
|
19838
|
+
mesh2.receiveShadow = it.mesh.receiveShadow;
|
|
19839
|
+
mesh2.renderOrder = it.mesh.renderOrder;
|
|
19840
|
+
mesh2.onBeforeRender = () => {
|
|
19841
|
+
house.updateMatrixWorld();
|
|
19842
|
+
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
|
|
19843
|
+
};
|
|
19844
|
+
it.mesh.parent.add(mesh2);
|
|
19845
|
+
mesh2.updateMatrixWorld(true);
|
|
19846
|
+
toHouse.value.copy(house.matrixWorld).invert().multiply(mesh2.matrixWorld);
|
|
19847
|
+
return mesh2;
|
|
19848
|
+
}
|
|
19849
|
+
function unitsMaterial(core) {
|
|
19850
|
+
const material = new THREE.MeshLambertMaterial({ color: 16777215, side: THREE.DoubleSide });
|
|
19851
|
+
material.onBeforeCompile = (sh) => {
|
|
19852
|
+
sh.uniforms.uCore = { value: core.texture };
|
|
19853
|
+
sh.uniforms.uCoreRect = { value: core.rect };
|
|
19854
|
+
sh.uniforms.uCoreAlongX = { value: core.alongU ? 1 : 0 };
|
|
19855
|
+
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");
|
|
19856
|
+
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;");
|
|
19857
|
+
};
|
|
19858
|
+
material.customProgramCacheKey = () => "breachCore";
|
|
19859
|
+
material.userData.breachCore = core;
|
|
19860
|
+
return material;
|
|
19861
|
+
}
|
|
19862
|
+
function maskOf(hostId, parts) {
|
|
19863
|
+
const host = structure.byId.get(hostId);
|
|
19864
|
+
const [u0, v0, u1, v1] = host.aabb;
|
|
19865
|
+
const alongU = u1 - u0 >= v1 - v0;
|
|
19866
|
+
const y0 = Math.min(...parts.map((id) => structure.byId.get(id).y0)), y1 = Math.max(...parts.map((id) => structure.byId.get(id).y1));
|
|
19867
|
+
const len = alongU ? u1 - u0 : v1 - v0;
|
|
19868
|
+
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)));
|
|
19869
|
+
const data = new Uint8Array(w * h);
|
|
19870
|
+
const texture = new THREE.DataTexture(data, w, h, THREE.RedFormat);
|
|
19871
|
+
texture.magFilter = THREE.LinearFilter;
|
|
19872
|
+
texture.minFilter = THREE.LinearFilter;
|
|
19873
|
+
texture.needsUpdate = true;
|
|
19874
|
+
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) };
|
|
19875
|
+
}
|
|
19876
|
+
const boxDistance = (s, y, a, b, c, d) => Math.hypot(Math.max(a - s, 0, s - b), Math.max(c - y, 0, y - d));
|
|
19877
|
+
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);
|
|
19878
|
+
function covered(c, boxes) {
|
|
19879
|
+
const n = 8;
|
|
19880
|
+
let hit = 0;
|
|
19881
|
+
for (let j = 0; j < n; j++) {
|
|
19882
|
+
const y = c.y0 + (j + 0.5) / n * (c.y1 - c.y0);
|
|
19883
|
+
for (let i = 0; i < n; i++) {
|
|
19884
|
+
const s = c.a + (i + 0.5) / n * (c.b - c.a);
|
|
19885
|
+
if (boxes.some((k) => s >= k.s0 && s <= k.s1 && y >= k.y0 && y <= k.y1)) hit++;
|
|
19886
|
+
}
|
|
19887
|
+
}
|
|
19888
|
+
return hit / (n * n);
|
|
19889
|
+
}
|
|
19890
|
+
function paint(view) {
|
|
19891
|
+
const { mask, core } = view;
|
|
19892
|
+
const along = (p) => (mask.alongU ? p[0] : p[2]) - mask.start;
|
|
19893
|
+
const holes = [];
|
|
19894
|
+
const pits = [];
|
|
19895
|
+
const discs = [];
|
|
19896
|
+
const flakes = [];
|
|
19897
|
+
const shown = [];
|
|
19898
|
+
for (const id of view.parts) {
|
|
19899
|
+
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);
|
|
19900
|
+
const all = cells(part, units2);
|
|
19901
|
+
const thick = mask.alongU ? part.aabb[3] - part.aabb[1] : part.aabb[2] - part.aabb[0];
|
|
19902
|
+
const mid = mask.alongU ? (part.aabb[1] + part.aabb[3]) / 2 : (part.aabb[0] + part.aabb[2]) / 2;
|
|
19903
|
+
if (opening) discs.push({ s: along(opening.center), y: opening.center[1], r: opening.radius });
|
|
19904
|
+
for (const k of spalls) flakes.push({ s: along(k.center), y: k.center[1], r: k.radius });
|
|
19905
|
+
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 }));
|
|
19906
|
+
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;
|
|
19907
|
+
const kept = [];
|
|
19908
|
+
for (const c of all) (gone(c) ? holes : kept).push(c);
|
|
19909
|
+
for (const c of kept) shown.push({ c, part, units: units2, thick, mid });
|
|
19910
|
+
for (const k of boxes) {
|
|
19911
|
+
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;
|
|
19912
|
+
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 });
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
const reach = SPALL_MIN + SPALL_RAGGED;
|
|
19916
|
+
const ragged = (s, y) => 0.6 * noise2(s, y, 0.03) + 0.4 * noise2(s + 7.3, y - 3.1, 0.011);
|
|
19917
|
+
mask.data.fill(0);
|
|
19918
|
+
core.data.fill(0);
|
|
19919
|
+
const stamp = (m, s0, s1, y0, y1, inside2) => {
|
|
19920
|
+
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));
|
|
19921
|
+
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));
|
|
19922
|
+
for (let j = j0; j <= j1; j++) {
|
|
19923
|
+
const y = m.y0 + (j + 0.5) / m.h * m.height;
|
|
19924
|
+
for (let i = i0; i <= i1; i++) {
|
|
19925
|
+
const s = (i + 0.5) / m.w * m.len;
|
|
19926
|
+
if (inside2(s, y)) m.data[j * m.w + i] = 255;
|
|
19927
|
+
}
|
|
19928
|
+
}
|
|
19929
|
+
};
|
|
19930
|
+
const plaster = (d, s, y) => d <= SPALL_MIN + SPALL_RAGGED * ragged(s, y);
|
|
19931
|
+
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));
|
|
19932
|
+
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));
|
|
19933
|
+
for (const f of flakes) {
|
|
19934
|
+
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));
|
|
19935
|
+
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);
|
|
19936
|
+
}
|
|
19937
|
+
for (const p of pits) {
|
|
19938
|
+
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));
|
|
19939
|
+
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));
|
|
19940
|
+
}
|
|
19941
|
+
mask.texture.needsUpdate = true;
|
|
19942
|
+
core.texture.needsUpdate = true;
|
|
19943
|
+
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);
|
|
19944
|
+
const list2 = [];
|
|
19945
|
+
const tone = (hex2, k) => tmp.c.setHex(hex2).multiplyScalar(k).getHex();
|
|
19946
|
+
for (const { c, part, units: units2, thick, mid } of shown) {
|
|
19947
|
+
if (!near(c)) continue;
|
|
19948
|
+
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;
|
|
19949
|
+
const at = (s, dy, d, colour) => list2.push({ x: mask.alongU ? centre : mid, y, z: mask.alongU ? mid : centre, sx: mask.alongU ? s : d, sy: dy, sz: mask.alongU ? d : s, colour });
|
|
19950
|
+
const base = fragmentMaterial(part.material).color.getHex();
|
|
19951
|
+
const n = noise2(centre * 3.1, y * 2.7, 0.07);
|
|
19952
|
+
if (units2.mortar === "concrete") {
|
|
19953
|
+
at(len, h, thick - 2 * COVER2, tone(base, 0.9 + 0.04 * n));
|
|
19954
|
+
} else {
|
|
19955
|
+
at(len, h, thick - 2 * COVER2 - 0.012, 10327948);
|
|
19956
|
+
at(len - 0.012, h - 0.012, thick - 2 * COVER2, tone(base, 0.8 + 0.35 * n));
|
|
19957
|
+
}
|
|
19958
|
+
}
|
|
19959
|
+
if (view.units) {
|
|
19960
|
+
view.units.removeFromParent();
|
|
19961
|
+
view.units.dispose();
|
|
19962
|
+
}
|
|
19963
|
+
const units = new THREE.InstancedMesh(unitGeometry, view.unitMaterial, Math.max(1, list2.length));
|
|
19964
|
+
units.name = "breach:" + view.host;
|
|
19965
|
+
list2.forEach((u, i) => {
|
|
19966
|
+
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
19967
|
+
units.setMatrixAt(i, tmp.m);
|
|
19968
|
+
units.setColorAt(i, tmp.c.setHex(u.colour));
|
|
19969
|
+
});
|
|
19970
|
+
units.count = list2.length;
|
|
19971
|
+
units.instanceMatrix.needsUpdate = true;
|
|
19972
|
+
if (units.instanceColor) units.instanceColor.needsUpdate = true;
|
|
19973
|
+
units.castShadow = true;
|
|
19974
|
+
units.receiveShadow = true;
|
|
19975
|
+
units.visible = lod === 0;
|
|
19976
|
+
house.add(units);
|
|
19977
|
+
view.units = units;
|
|
19978
|
+
view.gone = holes.length;
|
|
19979
|
+
view.pits = pits.length;
|
|
19980
|
+
view.flakes = flakes.length;
|
|
19981
|
+
}
|
|
19982
|
+
function chipBox(chip, alongU) {
|
|
19983
|
+
house.updateMatrixWorld(true);
|
|
19984
|
+
const q = tmp.q.set(chip.rotation[0], chip.rotation[1], chip.rotation[2], chip.rotation[3]).premultiply(house.getWorldQuaternion(new THREE.Quaternion()).invert());
|
|
19985
|
+
let a = 0, u = 0;
|
|
19986
|
+
for (let k = 0; k < 3; k++) {
|
|
19987
|
+
const axis = tmp.p.set(k === 0 ? 1 : 0, k === 1 ? 1 : 0, k === 2 ? 1 : 0).applyQuaternion(q);
|
|
19988
|
+
a += Math.abs(alongU ? axis.x : axis.z) * chip.size[k] / 2;
|
|
19989
|
+
u += Math.abs(axis.y) * chip.size[k] / 2;
|
|
19990
|
+
}
|
|
19991
|
+
return { center: local(chip.center), along: a, up: u };
|
|
19992
|
+
}
|
|
19667
19993
|
function draw2(partId, change) {
|
|
19668
19994
|
if (lod !== 0) return false;
|
|
19669
19995
|
const hostId = hostOf(partId);
|
|
@@ -19673,40 +19999,50 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19673
19999
|
const items = structure.meshesOf(hostId).filter((it) => it.mesh);
|
|
19674
20000
|
if (!items.length) return false;
|
|
19675
20001
|
const parts = [hostId, ...riders(hostId)].filter((id) => structure.byId.get(id) && specOf(id)?.units);
|
|
19676
|
-
|
|
20002
|
+
if (!parts.includes(partId)) return false;
|
|
20003
|
+
const mask = maskOf(hostId, parts), core = maskOf(hostId, parts);
|
|
20004
|
+
view = {
|
|
20005
|
+
host: hostId,
|
|
20006
|
+
parts,
|
|
20007
|
+
items,
|
|
20008
|
+
mask,
|
|
20009
|
+
core,
|
|
20010
|
+
unitMaterial: unitsMaterial(core),
|
|
20011
|
+
shells: [],
|
|
20012
|
+
saved: null,
|
|
20013
|
+
units: null,
|
|
20014
|
+
gone: 0,
|
|
20015
|
+
pits: 0,
|
|
20016
|
+
chips: new Map(parts.map((id) => [id, []])),
|
|
20017
|
+
spalls: new Map(parts.map((id) => [id, []])),
|
|
20018
|
+
openings: /* @__PURE__ */ new Map(),
|
|
20019
|
+
flakes: 0
|
|
20020
|
+
};
|
|
19677
20021
|
}
|
|
19678
20022
|
if (!view.chips.has(partId)) return false;
|
|
19679
20023
|
if (change.opening) {
|
|
19680
20024
|
if (view.openings.has(partId)) return false;
|
|
19681
20025
|
view.openings.set(partId, { center: local(change.opening.center), radius: change.opening.radius });
|
|
19682
|
-
if (partId === hostId) view.opening = view.openings.get(partId);
|
|
19683
20026
|
}
|
|
19684
|
-
if (change.chip) view.chips.get(partId).push(
|
|
19685
|
-
|
|
19686
|
-
|
|
19687
|
-
|
|
19688
|
-
|
|
19689
|
-
|
|
20027
|
+
if (change.chip) view.chips.get(partId).push(chipBox(change.chip, view.mask.alongU));
|
|
20028
|
+
if (change.spall) {
|
|
20029
|
+
const list2 = view.spalls.get(partId);
|
|
20030
|
+
list2.push({ center: local(change.spall.center), radius: change.spall.radius });
|
|
20031
|
+
if (list2.length > MAX_FLAKES) list2.shift();
|
|
20032
|
+
}
|
|
20033
|
+
if (!view.saved) {
|
|
20034
|
+
view.shells = view.items.map((it) => shell(it, view.mask));
|
|
20035
|
+
for (const m of view.shells) m.userData.behind = () => view.units;
|
|
20036
|
+
view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
19690
20037
|
}
|
|
19691
|
-
const mesh2 = new THREE.InstancedMesh(unitGeometry, unitMaterial, Math.max(1, list2.length));
|
|
19692
|
-
mesh2.name = "breach:" + hostId;
|
|
19693
|
-
list2.forEach((u, i) => {
|
|
19694
|
-
tmp.m.compose(tmp.p.set(u.x, u.y, u.z), tmp.q.identity(), tmp.s.set(u.sx, u.sy, u.sz));
|
|
19695
|
-
mesh2.setMatrixAt(i, tmp.m);
|
|
19696
|
-
mesh2.setColorAt(i, tmp.c.setHex(u.colour));
|
|
19697
|
-
});
|
|
19698
|
-
mesh2.count = list2.length;
|
|
19699
|
-
mesh2.instanceMatrix.needsUpdate = true;
|
|
19700
|
-
if (mesh2.instanceColor) mesh2.instanceColor.needsUpdate = true;
|
|
19701
|
-
mesh2.castShadow = true;
|
|
19702
|
-
mesh2.receiveShadow = true;
|
|
19703
|
-
house.add(mesh2);
|
|
19704
|
-
if (!view.saved) view.saved = view.items.map((it) => ({ it, v0: it.v0, data: collapseRange(it.mesh, it.v0, it.v1) }));
|
|
19705
|
-
view.mesh = mesh2;
|
|
19706
|
-
view.units = list2.length;
|
|
19707
20038
|
views.set(hostId, view);
|
|
19708
|
-
|
|
19709
|
-
|
|
20039
|
+
if (opts.deferPaint && change.spall) dirty.add(view);
|
|
20040
|
+
else {
|
|
20041
|
+
dirty.delete(view);
|
|
20042
|
+
paint(view);
|
|
20043
|
+
invalidate();
|
|
20044
|
+
invalidateShadow();
|
|
20045
|
+
}
|
|
19710
20046
|
return true;
|
|
19711
20047
|
}
|
|
19712
20048
|
function plain2(view, shown) {
|
|
@@ -19715,30 +20051,88 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19715
20051
|
else s.data = collapseRange(s.it.mesh, s.v0, s.it.v1);
|
|
19716
20052
|
}
|
|
19717
20053
|
}
|
|
20054
|
+
function flush(budgetMs = paintBudgetMs) {
|
|
20055
|
+
if (dirty.size === 0) return 0;
|
|
20056
|
+
const deadline = performance.now() + budgetMs;
|
|
20057
|
+
for (const view of dirty) {
|
|
20058
|
+
dirty.delete(view);
|
|
20059
|
+
paint(view);
|
|
20060
|
+
if (performance.now() >= deadline) break;
|
|
20061
|
+
}
|
|
20062
|
+
invalidate();
|
|
20063
|
+
invalidateShadow();
|
|
20064
|
+
return dirty.size;
|
|
20065
|
+
}
|
|
20066
|
+
function dispose(view) {
|
|
20067
|
+
dirty.delete(view);
|
|
20068
|
+
if (view.units) {
|
|
20069
|
+
view.units.removeFromParent();
|
|
20070
|
+
view.units.dispose();
|
|
20071
|
+
}
|
|
20072
|
+
view.unitMaterial.dispose();
|
|
20073
|
+
for (const m of view.shells) {
|
|
20074
|
+
m.removeFromParent();
|
|
20075
|
+
m.geometry.dispose();
|
|
20076
|
+
m.material.dispose();
|
|
20077
|
+
}
|
|
20078
|
+
view.mask.texture.dispose();
|
|
20079
|
+
view.core.texture.dispose();
|
|
20080
|
+
}
|
|
19718
20081
|
return {
|
|
19719
|
-
/** Handles a `stage` event of the house's object. Returns true when the wall was
|
|
20082
|
+
/** Handles a `stage` event of the house's object. Returns true when the wall was cut. */
|
|
19720
20083
|
stage(event) {
|
|
19721
20084
|
return event.stage === 2 && !!event.opening ? draw2(event.part, { opening: event.opening }) : false;
|
|
19722
20085
|
},
|
|
19723
|
-
/**
|
|
20086
|
+
/**
|
|
20087
|
+
* 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
|
|
20088
|
+
* renderer leaves its flat patch out on the meshes `claim` marked). Returns true when the wall was cut.
|
|
20089
|
+
*/
|
|
20090
|
+
spall(event) {
|
|
20091
|
+
const r = Math.sqrt(Math.max(0, event.area) / Math.PI);
|
|
20092
|
+
return r > 0 ? draw2(event.part, { spall: { center: event.point, radius: r } }) : false;
|
|
20093
|
+
},
|
|
20094
|
+
/**
|
|
20095
|
+
* Marks the meshes that draw a wall of units as drawing its spalls itself (`userData.drawnSpalls`, the part ids): the destruction renderer then
|
|
20096
|
+
* throws the bits and leaves its patch out. Call it once the spec is known.
|
|
20097
|
+
*/
|
|
20098
|
+
claim() {
|
|
20099
|
+
var _a;
|
|
20100
|
+
for (const q of opts.spec()?.parts ?? []) {
|
|
20101
|
+
if (!q.units) continue;
|
|
20102
|
+
const host = hostOf(q.id);
|
|
20103
|
+
if (!host) continue;
|
|
20104
|
+
for (const it of structure.meshesOf(host)) {
|
|
20105
|
+
if (!it.mesh) continue;
|
|
20106
|
+
const list2 = (_a = it.mesh.userData).drawnSpalls ?? (_a.drawnSpalls = []);
|
|
20107
|
+
if (!list2.includes(q.id)) list2.push(q.id);
|
|
20108
|
+
}
|
|
20109
|
+
}
|
|
20110
|
+
},
|
|
20111
|
+
/**
|
|
20112
|
+
* 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
|
|
20113
|
+
* tore off a face) leaves the marks the renderer drew. Returns true when the wall was cut.
|
|
20114
|
+
*/
|
|
19724
20115
|
chip(event) {
|
|
19725
|
-
|
|
20116
|
+
if (event.torn) return false;
|
|
20117
|
+
return draw2(event.part, { chip: { center: event.center, size: event.size ?? [0, 0, 0], rotation: event.rotation ?? [0, 0, 0, 1] } });
|
|
19726
20118
|
},
|
|
19727
|
-
/** The part broke or fell:
|
|
20119
|
+
/** The part broke or fell: its damage goes (the plain range stays collapsed, the renderer hides the part). */
|
|
19728
20120
|
drop(partId) {
|
|
19729
20121
|
const v = views.get(partId);
|
|
19730
20122
|
if (!v) return;
|
|
19731
|
-
v
|
|
19732
|
-
v.mesh.dispose();
|
|
20123
|
+
dispose(v);
|
|
19733
20124
|
views.delete(partId);
|
|
20125
|
+
if (v.saved) dropped.push(v);
|
|
19734
20126
|
},
|
|
19735
|
-
/** LOD 0 draws the
|
|
20127
|
+
/** LOD 0 draws the cut wall; LOD 1 and 2 give the wall its box back. */
|
|
19736
20128
|
setLod(level) {
|
|
20129
|
+
flush(Infinity);
|
|
19737
20130
|
const was = lod;
|
|
19738
20131
|
lod = level;
|
|
19739
20132
|
if (was === 0 === (level === 0)) return;
|
|
19740
20133
|
for (const v of views.values()) {
|
|
19741
|
-
v.
|
|
20134
|
+
v.units.visible = level === 0;
|
|
20135
|
+
for (const m of v.shells) m.visible = level === 0;
|
|
19742
20136
|
plain2(v, level !== 0);
|
|
19743
20137
|
}
|
|
19744
20138
|
invalidate();
|
|
@@ -19747,23 +20141,33 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19747
20141
|
/** Everything back to what it was (after `world.reset`). */
|
|
19748
20142
|
restore() {
|
|
19749
20143
|
for (const v of views.values()) {
|
|
19750
|
-
v
|
|
19751
|
-
v.mesh.dispose();
|
|
20144
|
+
dispose(v);
|
|
19752
20145
|
if (lod === 0) plain2(v, true);
|
|
19753
20146
|
}
|
|
19754
20147
|
views.clear();
|
|
20148
|
+
for (const v of dropped) plain2(v, true);
|
|
20149
|
+
dropped.length = 0;
|
|
19755
20150
|
},
|
|
19756
|
-
|
|
20151
|
+
flush,
|
|
20152
|
+
/** The wall has damage drawn. */
|
|
19757
20153
|
has(partId) {
|
|
19758
20154
|
return views.has(partId);
|
|
19759
20155
|
},
|
|
19760
|
-
|
|
20156
|
+
/** 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. */
|
|
20157
|
+
statistics: () => (flush(Infinity), {
|
|
20158
|
+
walls: views.size,
|
|
20159
|
+
gone: [...views.values()].reduce((n, v) => n + v.gone, 0),
|
|
20160
|
+
craters: [...views.values()].reduce((n, v) => n + v.pits, 0),
|
|
20161
|
+
flakes: [...views.values()].reduce((n, v) => n + v.flakes, 0),
|
|
20162
|
+
units: [...views.values()].reduce((n, v) => n + v.units.count, 0)
|
|
20163
|
+
})
|
|
19761
20164
|
};
|
|
19762
20165
|
}
|
|
19763
20166
|
Object.assign(K, { createBreachView });
|
|
19764
20167
|
}
|
|
19765
20168
|
|
|
19766
20169
|
// src/destruction/world.ts
|
|
20170
|
+
var FOLLOW_FLUSH_FALLBACK_MS = 100;
|
|
19767
20171
|
function installDestructionWorld(K, shared) {
|
|
19768
20172
|
const { THREE, cfg, invalidateShadow, invalidate, rngString, FRAGMENT_GEO, fragmentMaterial, createBreachView } = K;
|
|
19769
20173
|
function createHouseDestruction(root, house, structure, parts, colliders, water) {
|
|
@@ -19772,7 +20176,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19772
20176
|
const R = cfg.destructionRenderer || null, listeners = /* @__PURE__ */ new Set();
|
|
19773
20177
|
D.renderer = R;
|
|
19774
20178
|
D.spec = null;
|
|
19775
|
-
const breach = createBreachView({ house, structure, spec: () => D.spec });
|
|
20179
|
+
const breach = createBreachView({ house, structure, spec: () => D.spec, deferPaint: true, ...cfg.breachPaintBudgetMs !== void 0 ? { paintBudgetMs: cfg.breachPaintBudgetMs } : {} });
|
|
19776
20180
|
D.breach = breach;
|
|
19777
20181
|
D.onEvent = (fn) => {
|
|
19778
20182
|
listeners.add(fn);
|
|
@@ -19957,6 +20361,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19957
20361
|
case "chip":
|
|
19958
20362
|
breach.chip(e);
|
|
19959
20363
|
break;
|
|
20364
|
+
case "spall":
|
|
20365
|
+
breach.spall(e);
|
|
20366
|
+
break;
|
|
19960
20367
|
case "collapse":
|
|
19961
20368
|
for (const id of e.parts) {
|
|
19962
20369
|
breach.drop(id);
|
|
@@ -19998,7 +20405,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
19998
20405
|
D.specOptions = specOptions || null;
|
|
19999
20406
|
D.spec = structure.spec(specOptions);
|
|
20000
20407
|
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] });
|
|
20001
|
-
|
|
20408
|
+
breach.claim();
|
|
20409
|
+
if (R) R.bind(D.id, house, { parts: rangesOf(), onFrame: () => breach.flush() });
|
|
20002
20410
|
off = D.world.on("*", handleEvent);
|
|
20003
20411
|
D.active = true;
|
|
20004
20412
|
return D;
|
|
@@ -20013,6 +20421,8 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20013
20421
|
};
|
|
20014
20422
|
D.follow = (world, id, spec) => {
|
|
20015
20423
|
D.spec = spec;
|
|
20424
|
+
breach.claim();
|
|
20425
|
+
let fallback = null;
|
|
20016
20426
|
const offFollow = world.on("*", (e) => {
|
|
20017
20427
|
if (e.object !== id) return;
|
|
20018
20428
|
switch (e.type) {
|
|
@@ -20022,6 +20432,9 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20022
20432
|
case "chip":
|
|
20023
20433
|
breach.chip(e);
|
|
20024
20434
|
break;
|
|
20435
|
+
case "spall":
|
|
20436
|
+
breach.spall(e);
|
|
20437
|
+
break;
|
|
20025
20438
|
case "fracture":
|
|
20026
20439
|
breach.drop(e.part);
|
|
20027
20440
|
break;
|
|
@@ -20031,10 +20444,16 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20031
20444
|
default:
|
|
20032
20445
|
break;
|
|
20033
20446
|
}
|
|
20447
|
+
if (!fallback && typeof setTimeout === "function") fallback = setTimeout(() => {
|
|
20448
|
+
fallback = null;
|
|
20449
|
+
breach.flush(Infinity);
|
|
20450
|
+
}, FOLLOW_FLUSH_FALLBACK_MS);
|
|
20034
20451
|
});
|
|
20035
20452
|
return { restore: () => breach.restore(), dispose: () => {
|
|
20036
20453
|
offFollow();
|
|
20037
20454
|
breach.restore();
|
|
20455
|
+
}, update: () => {
|
|
20456
|
+
breach.flush();
|
|
20038
20457
|
} };
|
|
20039
20458
|
};
|
|
20040
20459
|
D.piece = (id) => D.active ? D.world.part(D.id, id) : null;
|
|
@@ -20067,6 +20486,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
20067
20486
|
return r;
|
|
20068
20487
|
};
|
|
20069
20488
|
D.animate = (dt) => {
|
|
20489
|
+
breach.flush();
|
|
20070
20490
|
if (R) return;
|
|
20071
20491
|
let moved = false;
|
|
20072
20492
|
for (const [id, list2] of bodies) {
|
|
@@ -21531,6 +21951,7 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
21531
21951
|
houseRenderer = create3(THREE, {
|
|
21532
21952
|
locale,
|
|
21533
21953
|
lod: { interior: 10, hide: 40 },
|
|
21954
|
+
breachPaintBudgetMs: 4,
|
|
21534
21955
|
...options.house,
|
|
21535
21956
|
quality: settings.quality,
|
|
21536
21957
|
...options.world ? { destruction: options.world } : {},
|
|
@@ -21686,9 +22107,39 @@ uniform mat4 uNodes[${nodes.length}]; attribute float aNodeIndex; attribute vec3
|
|
|
21686
22107
|
host?.log(text.steps.no_door);
|
|
21687
22108
|
return;
|
|
21688
22109
|
}
|
|
21689
|
-
|
|
22110
|
+
const fo = target.target;
|
|
22111
|
+
const reach = fo && host?.reachFor && fo.type === "swing" && fo.gs?.[0] && fo.handles?.[0] ? doorReach(p, fo) : null;
|
|
22112
|
+
if (reach && host?.reachFor) {
|
|
22113
|
+
host.reachFor({ ...reach, onGrip: () => void p.house.toggleDoor(fo) });
|
|
22114
|
+
} else p.house.interact(eyeOf(p));
|
|
21690
22115
|
host?.log(fill(text.steps.door, { text: String(target.text) }));
|
|
21691
22116
|
};
|
|
22117
|
+
const doorReach = (p, fo) => {
|
|
22118
|
+
const g = fo.gs[0];
|
|
22119
|
+
const pivot = fo.handles[0].pivot;
|
|
22120
|
+
const at = (angle) => {
|
|
22121
|
+
const c = Math.cos(angle);
|
|
22122
|
+
const s = Math.sin(angle);
|
|
22123
|
+
const v = new THREE.Vector3(pivot[0] * c + pivot[2] * s, pivot[1], -pivot[0] * s + pivot[2] * c).add(g.position);
|
|
22124
|
+
return g.parent ? g.parent.localToWorld(v) : v;
|
|
22125
|
+
};
|
|
22126
|
+
const camera = host?.camera;
|
|
22127
|
+
const eye = new THREE.Vector3(camera.position.x, 0, camera.position.z);
|
|
22128
|
+
const flat = (v) => Math.hypot(v.x - eye.x, v.z - eye.z);
|
|
22129
|
+
const closed = at(0);
|
|
22130
|
+
const open = at(fo.rot);
|
|
22131
|
+
const opening = !fo.open;
|
|
22132
|
+
return {
|
|
22133
|
+
handle: () => {
|
|
22134
|
+
g.updateMatrixWorld(true);
|
|
22135
|
+
return g.localToWorld(new THREE.Vector3(pivot[0], pivot[1], pivot[2])).toArray();
|
|
22136
|
+
},
|
|
22137
|
+
hinge: () => g.getWorldPosition(new THREE.Vector3()).toArray(),
|
|
22138
|
+
seconds: fo.locked || fo.blockedFlag ? 0.6 : 0.55,
|
|
22139
|
+
pull: opening ? flat(open) <= flat(closed) : flat(closed) <= flat(open),
|
|
22140
|
+
locked: !!(fo.locked || fo.blockedFlag)
|
|
22141
|
+
};
|
|
22142
|
+
};
|
|
21692
22143
|
const strike = () => {
|
|
21693
22144
|
const world = options.world;
|
|
21694
22145
|
if (!world || !host) return;
|