@adatechnology/game-test-room 0.1.2 → 0.2.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 +7 -0
- package/dist/index.global.js +575 -66
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +141 -24
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.global.js
CHANGED
|
@@ -1646,6 +1646,7 @@ var GameTestRoom = (() => {
|
|
|
1646
1646
|
var clamp2 = (v, a, b) => Math.max(a, Math.min(b, v));
|
|
1647
1647
|
var isLost = (p) => p.st.status === "broken" || p.st.status === "melted" || p.st.status === "ashes";
|
|
1648
1648
|
var clone = (v) => structuredClone(v);
|
|
1649
|
+
var overloadRate = (stress) => Math.min(0.6, 0.12 + 0.8 * (stress - 1));
|
|
1649
1650
|
var updateBound = (o) => {
|
|
1650
1651
|
if (!o.parts.length) return;
|
|
1651
1652
|
const c = [0, 0, 0];
|
|
@@ -1956,7 +1957,10 @@ var GameTestRoom = (() => {
|
|
|
1956
1957
|
for (const g of o.st.groups) {
|
|
1957
1958
|
const ps = g.map((id) => o.byId.get(id)).filter((p) => !isLost(p) && !o.collapsed.has(p.spec.id));
|
|
1958
1959
|
if (ps.length === 0 || g.some((id) => o.byId.get(id).anchored) || !ps.some((p) => p.structural)) continue;
|
|
1959
|
-
for (const p of ps)
|
|
1960
|
+
for (const p of ps) {
|
|
1961
|
+
o.collapsed.add(p.spec.id);
|
|
1962
|
+
p.st.status = "collapsed";
|
|
1963
|
+
}
|
|
1960
1964
|
emit({ ...base(o, src), type: "collapse", parts: ps.map((p) => p.spec.id), mass: ps.reduce((sum, p) => sum + p.st.mass, 0) });
|
|
1961
1965
|
}
|
|
1962
1966
|
}
|
|
@@ -2256,7 +2260,7 @@ var GameTestRoom = (() => {
|
|
|
2256
2260
|
const effM = p.thickness / Math.max(sin, 0.3);
|
|
2257
2261
|
const thr = mat.penetration * effM * 1e3;
|
|
2258
2262
|
const penetrated = E > thr;
|
|
2259
|
-
const scale2 = mat.damageScale?.ballistic ?? 1;
|
|
2263
|
+
const scale2 = (mat.damageScale?.ballistic ?? 1) * (mat.fracture === "shatter" ? fragility(mat.ductility, p.thickness) : 1);
|
|
2260
2264
|
const id = p.spec.id;
|
|
2261
2265
|
if (!penetrated && mat.ricochetDeg && angle < mat.ricochetDeg) {
|
|
2262
2266
|
const r = vec.sub(d, vec.scale(n, 2 * vec.dot(d, n)));
|
|
@@ -2606,7 +2610,7 @@ var GameTestRoom = (() => {
|
|
|
2606
2610
|
p.rt.stressT = 0.5;
|
|
2607
2611
|
emit({ ...base(o), type: "stress", part: p.spec.id, stress, load: p.st.load ?? 0 });
|
|
2608
2612
|
}
|
|
2609
|
-
applyDamage(o, p, (stress
|
|
2613
|
+
applyDamage(o, p, overloadRate(stress) * p.st.maxHp * dt, "blunt", null, null, 5, 3);
|
|
2610
2614
|
} else if (stress > 0.8 && p.mat.ductility < 0.3 && p.rt.stressT <= 0 && p.st.cracks.length < 6) {
|
|
2611
2615
|
p.rt.stressT = 0.5;
|
|
2612
2616
|
const { c, q } = partXf(o, p);
|
|
@@ -3096,7 +3100,7 @@ var GameTestRoom = (() => {
|
|
|
3096
3100
|
wax: "wax",
|
|
3097
3101
|
soil: "soil"
|
|
3098
3102
|
},
|
|
3099
|
-
partStatus: { intact: "intact", damaged: "damaged", broken: "broken", melted: "melted", ashes: "ashes" },
|
|
3103
|
+
partStatus: { intact: "intact", damaged: "damaged", collapsed: "collapsed", broken: "broken", melted: "melted", ashes: "ashes" },
|
|
3100
3104
|
jointKinds: {
|
|
3101
3105
|
screw: "screw",
|
|
3102
3106
|
bolt: "nut and bolt",
|
|
@@ -3177,7 +3181,7 @@ var GameTestRoom = (() => {
|
|
|
3177
3181
|
wax: "cera",
|
|
3178
3182
|
soil: "terra"
|
|
3179
3183
|
},
|
|
3180
|
-
partStatus: { intact: "intacto", damaged: "danificado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
|
|
3184
|
+
partStatus: { intact: "intacto", damaged: "danificado", collapsed: "desabado", broken: "quebrado", melted: "derretido", ashes: "cinzas" },
|
|
3181
3185
|
jointKinds: {
|
|
3182
3186
|
screw: "parafuso",
|
|
3183
3187
|
bolt: "porca e parafuso",
|
|
@@ -4472,6 +4476,8 @@ void main() {
|
|
|
4472
4476
|
__publicField(this, "b");
|
|
4473
4477
|
__publicField(this, "m4");
|
|
4474
4478
|
__publicField(this, "box");
|
|
4479
|
+
__publicField(this, "t");
|
|
4480
|
+
__publicField(this, "lbox");
|
|
4475
4481
|
__publicField(this, "decalCount", 0);
|
|
4476
4482
|
__publicField(this, "tints", /* @__PURE__ */ new WeakMap());
|
|
4477
4483
|
this.kit = new DecalKit(THREE);
|
|
@@ -4479,9 +4485,22 @@ void main() {
|
|
|
4479
4485
|
this.b = tmpV(THREE);
|
|
4480
4486
|
this.m4 = new THREE.Matrix4();
|
|
4481
4487
|
this.box = new THREE.Box3();
|
|
4488
|
+
this.t = tmpV(THREE);
|
|
4489
|
+
this.lbox = new THREE.Box3();
|
|
4482
4490
|
}
|
|
4483
|
-
|
|
4484
|
-
|
|
4491
|
+
/**
|
|
4492
|
+
* @param range vertex range `[v0, v1)` of a merged mesh this part is drawn by (see `MeshRange`); `hosts` caches the shared state per mesh for one `bind`.
|
|
4493
|
+
*/
|
|
4494
|
+
makeMeshVis(mesh, home, range, hosts) {
|
|
4495
|
+
let host = null;
|
|
4496
|
+
if (range) {
|
|
4497
|
+
host = hosts?.get(mesh) ?? null;
|
|
4498
|
+
if (!host) {
|
|
4499
|
+
host = { mesh, origGeometry: mesh.geometry, origMaterial: mesh.material, ownGeo: false, work: null, ownMat: false, sharers: [] };
|
|
4500
|
+
hosts?.set(mesh, host);
|
|
4501
|
+
}
|
|
4502
|
+
}
|
|
4503
|
+
const mv = {
|
|
4485
4504
|
mesh,
|
|
4486
4505
|
home,
|
|
4487
4506
|
origGeometry: mesh.geometry,
|
|
@@ -4498,31 +4517,72 @@ void main() {
|
|
|
4498
4517
|
decals: [],
|
|
4499
4518
|
meltApplied: 0,
|
|
4500
4519
|
bottomY: 0,
|
|
4501
|
-
moved: false
|
|
4520
|
+
moved: false,
|
|
4521
|
+
host,
|
|
4522
|
+
range: range ? [range[0], range[1]] : null,
|
|
4523
|
+
merged: host && range ? { host, range: [range[0], range[1]] } : null,
|
|
4524
|
+
loose: false,
|
|
4525
|
+
collapsed: false,
|
|
4526
|
+
hidden: false,
|
|
4527
|
+
touched: false,
|
|
4528
|
+
charK: 0,
|
|
4529
|
+
charHex: 0,
|
|
4530
|
+
glowHex: 0,
|
|
4531
|
+
glowK: 0
|
|
4502
4532
|
};
|
|
4533
|
+
host?.sharers.push(mv);
|
|
4534
|
+
return mv;
|
|
4503
4535
|
}
|
|
4504
4536
|
vertexCount(mv) {
|
|
4537
|
+
if (mv.range) return mv.range[1] - mv.range[0];
|
|
4505
4538
|
return mv.mesh.geometry.getAttribute(POS_ATTR)?.count ?? 0;
|
|
4506
4539
|
}
|
|
4540
|
+
/** Is the part drawn? (A ranged part shares its mesh, so `mesh.visible` says nothing about it.) */
|
|
4541
|
+
shown(mv) {
|
|
4542
|
+
return mv.host ? !mv.hidden : mv.mesh.visible;
|
|
4543
|
+
}
|
|
4544
|
+
/** Box of the part's vertices in the space of its mesh (the whole geometry for a plain mesh). */
|
|
4545
|
+
localBox(mv, out) {
|
|
4546
|
+
out.makeEmpty();
|
|
4547
|
+
if (!mv.host || !mv.range) {
|
|
4548
|
+
const geo = mv.mesh.geometry;
|
|
4549
|
+
if (!geo.boundingBox) geo.computeBoundingBox();
|
|
4550
|
+
return geo.boundingBox ? out.copy(geo.boundingBox) : out;
|
|
4551
|
+
}
|
|
4552
|
+
const arr = mv.host.work ?? positionAttr(mv.mesh.geometry)?.array;
|
|
4553
|
+
if (!arr) return out;
|
|
4554
|
+
for (let i = mv.range[0] * 3; i < mv.range[1] * 3; i += 3) out.expandByPoint(this.t.set(arr[i], arr[i + 1], arr[i + 2]));
|
|
4555
|
+
return out;
|
|
4556
|
+
}
|
|
4557
|
+
/** Box of the part in world space. */
|
|
4558
|
+
worldBox(mv, out) {
|
|
4559
|
+
if (!mv.host) return out.setFromObject(mv.mesh);
|
|
4560
|
+
this.localBox(mv, out);
|
|
4561
|
+
mv.mesh.updateWorldMatrix(true, false);
|
|
4562
|
+
return out.isEmpty() ? out : out.applyMatrix4(mv.mesh.matrixWorld);
|
|
4563
|
+
}
|
|
4507
4564
|
/**
|
|
4508
4565
|
* Average vertex colour of a mesh whose material uses `vertexColors` (computed once per mesh, from its original geometry), else null. Fragments
|
|
4509
4566
|
* and debris of such a mesh multiply it by `material.color`; without it they come out white.
|
|
4510
4567
|
*/
|
|
4511
4568
|
vertexTint(mv) {
|
|
4512
|
-
if (this.tints.has(mv
|
|
4569
|
+
if (this.tints.has(mv)) return this.tints.get(mv) ?? null;
|
|
4513
4570
|
let tint = null;
|
|
4514
4571
|
const used = materialsOf(mv.origMaterial).some((m) => m.vertexColors === true);
|
|
4515
4572
|
const attr = used ? mv.origGeometry.getAttribute("color") : void 0;
|
|
4516
4573
|
if (attr && attr.count > 0 && attr.itemSize >= 3) {
|
|
4574
|
+
const from = mv.merged ? mv.merged.range[0] : 0;
|
|
4575
|
+
const to = mv.merged ? Math.min(attr.count, mv.merged.range[1]) : attr.count;
|
|
4517
4576
|
let r = 0, g = 0, b = 0;
|
|
4518
|
-
for (let i =
|
|
4577
|
+
for (let i = from; i < to; i++) {
|
|
4519
4578
|
r += attr.getX(i);
|
|
4520
4579
|
g += attr.getY(i);
|
|
4521
4580
|
b += attr.getZ(i);
|
|
4522
4581
|
}
|
|
4523
|
-
|
|
4582
|
+
const n = Math.max(1, to - from);
|
|
4583
|
+
tint = new this.THREE.Color(r / n, g / n, b / n);
|
|
4524
4584
|
}
|
|
4525
|
-
this.tints.set(mv
|
|
4585
|
+
this.tints.set(mv, tint);
|
|
4526
4586
|
return tint;
|
|
4527
4587
|
}
|
|
4528
4588
|
/** Colour of the original material (before any char) times the vertex tint, `0xRRGGBB`, or -1. */
|
|
@@ -4546,6 +4606,7 @@ void main() {
|
|
|
4546
4606
|
}
|
|
4547
4607
|
/** Clone the geometry (vertex edits); false when its positions cannot be edited. */
|
|
4548
4608
|
ownGeometry(mv) {
|
|
4609
|
+
if (mv.host) return this.ownHostGeometry(mv.host);
|
|
4549
4610
|
if (mv.ownGeo) return true;
|
|
4550
4611
|
const pos = positionAttr(mv.mesh.geometry);
|
|
4551
4612
|
if (!pos) return false;
|
|
@@ -4555,6 +4616,16 @@ void main() {
|
|
|
4555
4616
|
mv.work = new Float32Array(positionAttr(g).array);
|
|
4556
4617
|
return true;
|
|
4557
4618
|
}
|
|
4619
|
+
ownHostGeometry(host) {
|
|
4620
|
+
if (host.ownGeo) return true;
|
|
4621
|
+
const pos = positionAttr(host.mesh.geometry);
|
|
4622
|
+
if (!pos) return false;
|
|
4623
|
+
host.ownGeo = true;
|
|
4624
|
+
const g = host.mesh.geometry.clone();
|
|
4625
|
+
host.mesh.geometry = g;
|
|
4626
|
+
host.work = new Float32Array(positionAttr(g).array);
|
|
4627
|
+
return true;
|
|
4628
|
+
}
|
|
4558
4629
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
4559
4630
|
// Dents
|
|
4560
4631
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
@@ -4572,10 +4643,15 @@ void main() {
|
|
|
4572
4643
|
const r = dent.radius / s;
|
|
4573
4644
|
const depth = dent.depth / s;
|
|
4574
4645
|
const geo = mesh.geometry;
|
|
4575
|
-
|
|
4576
|
-
if (
|
|
4646
|
+
let bb;
|
|
4647
|
+
if (mv.host) bb = this.localBox(mv, this.lbox);
|
|
4648
|
+
else {
|
|
4649
|
+
if (!geo.boundingBox) geo.computeBoundingBox();
|
|
4650
|
+
bb = geo.boundingBox;
|
|
4651
|
+
}
|
|
4652
|
+
if (bb && !bb.isEmpty() && bb.distanceToPoint(lp) > r) return false;
|
|
4577
4653
|
if (!this.ownGeometry(mv)) return false;
|
|
4578
|
-
const work = mv.work;
|
|
4654
|
+
const work = mv.host ? mv.host.work : mv.work;
|
|
4579
4655
|
const pos = positionAttr(mesh.geometry);
|
|
4580
4656
|
const r2 = r * r;
|
|
4581
4657
|
let touched = false;
|
|
@@ -4585,7 +4661,9 @@ void main() {
|
|
|
4585
4661
|
const nx = ln.x;
|
|
4586
4662
|
const ny = ln.y;
|
|
4587
4663
|
const nz = ln.z;
|
|
4588
|
-
|
|
4664
|
+
const from = mv.range ? mv.range[0] * 3 : 0;
|
|
4665
|
+
const to = mv.range ? mv.range[1] * 3 : work.length;
|
|
4666
|
+
for (let i = from; i < to; i += 3) {
|
|
4589
4667
|
const dx = work[i] - px;
|
|
4590
4668
|
const dy = work[i + 1] - py;
|
|
4591
4669
|
const dz = work[i + 2] - pz;
|
|
@@ -4599,11 +4677,16 @@ void main() {
|
|
|
4599
4677
|
touched = true;
|
|
4600
4678
|
}
|
|
4601
4679
|
if (!touched) return false;
|
|
4680
|
+
mv.touched = true;
|
|
4602
4681
|
this.writePositions(mv, pos);
|
|
4603
4682
|
return true;
|
|
4604
4683
|
}
|
|
4605
4684
|
/** Copy `work` (dents) into the geometry, adding the melt sag when the mesh is melting. */
|
|
4606
4685
|
writePositions(mv, pos) {
|
|
4686
|
+
if (mv.host) {
|
|
4687
|
+
this.rebuildHost(mv.host);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4607
4690
|
const work = mv.work;
|
|
4608
4691
|
const arr = pos.array;
|
|
4609
4692
|
const melt = mv.meltApplied;
|
|
@@ -4717,6 +4800,12 @@ void main() {
|
|
|
4717
4800
|
pv.charApplied = char;
|
|
4718
4801
|
const target = new this.THREE.Color(charColor);
|
|
4719
4802
|
for (const mv of pv.meshes) {
|
|
4803
|
+
if (mv.host) {
|
|
4804
|
+
mv.charK = clamp3(char, 0, 1);
|
|
4805
|
+
mv.charHex = charColor;
|
|
4806
|
+
this.paint(mv);
|
|
4807
|
+
continue;
|
|
4808
|
+
}
|
|
4720
4809
|
this.ownMaterial(mv);
|
|
4721
4810
|
const mats = materialsOf(mv.mesh.material);
|
|
4722
4811
|
mats.forEach((m, i) => {
|
|
@@ -4734,6 +4823,12 @@ void main() {
|
|
|
4734
4823
|
pv.glowApplied = key;
|
|
4735
4824
|
const intensity = hex === 0 ? 0 : clamp3(0.35 + (temperatureC - 450) / 900, 0, 1.4);
|
|
4736
4825
|
for (const mv of pv.meshes) {
|
|
4826
|
+
if (mv.host) {
|
|
4827
|
+
mv.glowHex = hex;
|
|
4828
|
+
mv.glowK = intensity;
|
|
4829
|
+
this.paint(mv);
|
|
4830
|
+
continue;
|
|
4831
|
+
}
|
|
4737
4832
|
this.ownMaterial(mv);
|
|
4738
4833
|
for (const m of materialsOf(mv.mesh.material)) {
|
|
4739
4834
|
const e = m.emissive;
|
|
@@ -4754,6 +4849,12 @@ void main() {
|
|
|
4754
4849
|
pv.meltApplied = m;
|
|
4755
4850
|
for (const mv of pv.meshes) {
|
|
4756
4851
|
const mesh = mv.mesh;
|
|
4852
|
+
if (mv.host) {
|
|
4853
|
+
mv.meltApplied = m;
|
|
4854
|
+
mv.touched = true;
|
|
4855
|
+
if (!(this.lowQuality && this.vertexCount(mv) > 2e4) && this.ownGeometry(mv)) this.rebuildHost(mv.host);
|
|
4856
|
+
continue;
|
|
4857
|
+
}
|
|
4757
4858
|
if (mv.meltApplied === 0 && m > 0) {
|
|
4758
4859
|
mesh.updateWorldMatrix(true, false);
|
|
4759
4860
|
mv.bottomY = this.box.setFromObject(mesh).min.y;
|
|
@@ -4784,10 +4885,270 @@ void main() {
|
|
|
4784
4885
|
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
4785
4886
|
hide(pv, reason) {
|
|
4786
4887
|
pv.gone = reason;
|
|
4787
|
-
for (const mv of pv.meshes)
|
|
4888
|
+
for (const mv of pv.meshes) {
|
|
4889
|
+
if (!mv.host || mv.loose) {
|
|
4890
|
+
mv.mesh.visible = false;
|
|
4891
|
+
mv.hidden = true;
|
|
4892
|
+
continue;
|
|
4893
|
+
}
|
|
4894
|
+
mv.hidden = true;
|
|
4895
|
+
for (const d of mv.decals) d.obj.visible = false;
|
|
4896
|
+
this.collapse(mv);
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
4900
|
+
// Parts drawn by a vertex range of a merged mesh
|
|
4901
|
+
// ------------------------------------------------------------------------------------------------------------------------------------
|
|
4902
|
+
/** Collapse the part's vertices in the merged mesh to one point (its triangles become degenerate and vanish). */
|
|
4903
|
+
collapse(mv) {
|
|
4904
|
+
if (!mv.merged || !this.ownHostGeometry(mv.merged.host)) return;
|
|
4905
|
+
mv.collapsed = true;
|
|
4906
|
+
mv.touched = true;
|
|
4907
|
+
this.rebuildHost(mv.merged.host);
|
|
4908
|
+
}
|
|
4909
|
+
/** Write `work` into the geometry again with the melt of ranged parts and the collapsed ranges applied. */
|
|
4910
|
+
rebuildHost(host) {
|
|
4911
|
+
const geo = host.mesh.geometry;
|
|
4912
|
+
const pos = positionAttr(geo);
|
|
4913
|
+
const work = host.work;
|
|
4914
|
+
if (!pos || !work) return;
|
|
4915
|
+
const arr = pos.array;
|
|
4916
|
+
arr.set(work);
|
|
4917
|
+
for (const s of host.sharers) {
|
|
4918
|
+
if (s.merged && s.merged.host === host) {
|
|
4919
|
+
if (s.collapsed) this.collapseRange(arr, s.merged.range);
|
|
4920
|
+
else if (s.meltApplied > 0 && !s.loose) this.meltRange(arr, work, s.merged.range, s.meltApplied);
|
|
4921
|
+
} else if (s.host === host && s.range && s.meltApplied > 0) this.meltRange(arr, work, s.range, s.meltApplied);
|
|
4922
|
+
}
|
|
4923
|
+
pos.needsUpdate = true;
|
|
4924
|
+
geo.computeVertexNormals();
|
|
4925
|
+
geo.boundingBox = null;
|
|
4926
|
+
geo.boundingSphere = null;
|
|
4927
|
+
}
|
|
4928
|
+
collapseRange(arr, range) {
|
|
4929
|
+
const x = arr[range[0] * 3];
|
|
4930
|
+
const y = arr[range[0] * 3 + 1];
|
|
4931
|
+
const z = arr[range[0] * 3 + 2];
|
|
4932
|
+
for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
|
|
4933
|
+
arr[i] = x;
|
|
4934
|
+
arr[i + 1] = y;
|
|
4935
|
+
arr[i + 2] = z;
|
|
4936
|
+
}
|
|
4937
|
+
}
|
|
4938
|
+
/** Melt of one range: shorter in Y (down to 25 %) with the bottom fixed, a bit wider around its centre, with the same noise as a whole mesh. */
|
|
4939
|
+
meltRange(arr, work, range, melt) {
|
|
4940
|
+
let minY = Infinity;
|
|
4941
|
+
let maxY = -Infinity;
|
|
4942
|
+
let cx = 0;
|
|
4943
|
+
let cz = 0;
|
|
4944
|
+
const n = Math.max(1, range[1] - range[0]);
|
|
4945
|
+
for (let i = range[0] * 3; i < range[1] * 3; i += 3) {
|
|
4946
|
+
minY = Math.min(minY, work[i + 1]);
|
|
4947
|
+
maxY = Math.max(maxY, work[i + 1]);
|
|
4948
|
+
cx += work[i];
|
|
4949
|
+
cz += work[i + 2];
|
|
4950
|
+
}
|
|
4951
|
+
cx /= n;
|
|
4952
|
+
cz /= n;
|
|
4953
|
+
const h = Math.max(1e-4, maxY - minY);
|
|
4954
|
+
const sy = lerp(1, 0.25, melt);
|
|
4955
|
+
const sxz = 1 + 0.18 * melt;
|
|
4956
|
+
for (let i = range[0] * 3, v = 0; i < range[1] * 3; i += 3, v++) {
|
|
4957
|
+
const noise = (Math.imul(v + 1, 2654435761) >>> 0) % 1e3 / 1e3;
|
|
4958
|
+
arr[i] = cx + (work[i] - cx) * sxz;
|
|
4959
|
+
arr[i + 2] = cz + (work[i + 2] - cz) * sxz;
|
|
4960
|
+
arr[i + 1] = Math.max(minY, minY + (work[i + 1] - minY) * sy - noise * melt * h * 0.04);
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
/** The colour attribute of the host's current geometry (created white when there is none) with vertex colours switched on in its material. */
|
|
4964
|
+
ensureColors(host) {
|
|
4965
|
+
if (!this.ownHostGeometry(host)) return null;
|
|
4966
|
+
const geo = host.mesh.geometry;
|
|
4967
|
+
let attr = geo.getAttribute("color");
|
|
4968
|
+
if (!attr) {
|
|
4969
|
+
const n = geo.getAttribute(POS_ATTR).count;
|
|
4970
|
+
attr = new this.THREE.BufferAttribute(new Float32Array(n * 3).fill(1), 3);
|
|
4971
|
+
geo.setAttribute("color", attr);
|
|
4972
|
+
}
|
|
4973
|
+
if (!host.ownMat) {
|
|
4974
|
+
host.ownMat = true;
|
|
4975
|
+
const src = materialsOf(host.mesh.material);
|
|
4976
|
+
const cloned = src.map((m) => m.clone());
|
|
4977
|
+
host.mesh.material = Array.isArray(host.mesh.material) ? cloned : cloned[0];
|
|
4978
|
+
}
|
|
4979
|
+
for (const m of materialsOf(host.mesh.material)) {
|
|
4980
|
+
if (m.vertexColors !== true) {
|
|
4981
|
+
;
|
|
4982
|
+
m.vertexColors = true;
|
|
4983
|
+
m.needsUpdate = true;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4986
|
+
return attr;
|
|
4987
|
+
}
|
|
4988
|
+
/** Colour of the part's vertices from the original colours, its char and its heat. Only the part's range is written. */
|
|
4989
|
+
paint(mv) {
|
|
4990
|
+
if (!mv.host || !mv.range || !mv.merged) return;
|
|
4991
|
+
const idle = mv.charK <= 0 && mv.glowK <= 0;
|
|
4992
|
+
if (idle && !mv.touched) return;
|
|
4993
|
+
const attr = this.ensureColors(mv.host);
|
|
4994
|
+
if (!attr) return;
|
|
4995
|
+
const size = attr.itemSize;
|
|
4996
|
+
const arr = attr.array;
|
|
4997
|
+
const orig = mv.merged.host.origGeometry.getAttribute("color");
|
|
4998
|
+
const oSize = orig?.itemSize ?? 3;
|
|
4999
|
+
const oArr = orig?.array;
|
|
5000
|
+
const shift = mv.merged.range[0] - mv.range[0];
|
|
5001
|
+
const ch = new this.THREE.Color(mv.charHex);
|
|
5002
|
+
const heat = new this.THREE.Color(mv.glowHex);
|
|
5003
|
+
const k = clamp3(mv.charK, 0, 1);
|
|
5004
|
+
const gk = clamp3(mv.glowK, 0, 1);
|
|
5005
|
+
const boost = 1 + Math.max(0, mv.glowK - 1);
|
|
5006
|
+
for (let i = mv.range[0]; i < mv.range[1]; i++) {
|
|
5007
|
+
const o = (i + shift) * oSize;
|
|
5008
|
+
let r = oArr ? oArr[o] : 1;
|
|
5009
|
+
let g = oArr ? oArr[o + 1] : 1;
|
|
5010
|
+
let b = oArr ? oArr[o + 2] : 1;
|
|
5011
|
+
if (k > 0) {
|
|
5012
|
+
r = lerp(r, ch.r, k);
|
|
5013
|
+
g = lerp(g, ch.g, k);
|
|
5014
|
+
b = lerp(b, ch.b, k);
|
|
5015
|
+
}
|
|
5016
|
+
if (gk > 0) {
|
|
5017
|
+
r = lerp(r, heat.r * boost, gk);
|
|
5018
|
+
g = lerp(g, heat.g * boost, gk);
|
|
5019
|
+
b = lerp(b, heat.b * boost, gk);
|
|
5020
|
+
}
|
|
5021
|
+
arr[i * size] = r;
|
|
5022
|
+
arr[i * size + 1] = g;
|
|
5023
|
+
arr[i * size + 2] = b;
|
|
5024
|
+
}
|
|
5025
|
+
mv.touched = true;
|
|
5026
|
+
attr.needsUpdate = true;
|
|
5027
|
+
}
|
|
5028
|
+
/**
|
|
5029
|
+
* Move a ranged part to a mesh of its own: the faces whose vertices all lie in its range are copied (with the current dents and colours) into a
|
|
5030
|
+
* new mesh that takes the merged mesh's transform and material, then attached to `group`; the range is collapsed in the merged mesh. Decals
|
|
5031
|
+
* go with it.
|
|
5032
|
+
*/
|
|
5033
|
+
extract(mv, group) {
|
|
5034
|
+
if (!mv.host || !mv.range || !mv.merged || mv.loose) return;
|
|
5035
|
+
const host = mv.host;
|
|
5036
|
+
const [v0, v1] = mv.range;
|
|
5037
|
+
const src = host.mesh.geometry;
|
|
5038
|
+
const geo = new this.THREE.BufferGeometry();
|
|
5039
|
+
const count = v1 - v0;
|
|
5040
|
+
for (const name of Object.keys(src.attributes)) {
|
|
5041
|
+
const a = src.getAttribute(name);
|
|
5042
|
+
if (a.isInterleavedBufferAttribute) continue;
|
|
5043
|
+
const size = a.itemSize;
|
|
5044
|
+
const from = (name === POS_ATTR ? host.work : null) ?? a.array;
|
|
5045
|
+
const slice = from.slice(v0 * size, v1 * size);
|
|
5046
|
+
const Ctor = a.array.constructor;
|
|
5047
|
+
geo.setAttribute(name, new this.THREE.BufferAttribute(new Ctor(slice), size, a.normalized));
|
|
5048
|
+
}
|
|
5049
|
+
const index = src.getIndex();
|
|
5050
|
+
if (index) {
|
|
5051
|
+
const out = [];
|
|
5052
|
+
for (let i = 0; i + 2 < index.count; i += 3) {
|
|
5053
|
+
const a = index.getX(i);
|
|
5054
|
+
const b = index.getX(i + 1);
|
|
5055
|
+
const c = index.getX(i + 2);
|
|
5056
|
+
if (a >= v0 && a < v1 && b >= v0 && b < v1 && c >= v0 && c < v1) out.push(a - v0, b - v0, c - v0);
|
|
5057
|
+
}
|
|
5058
|
+
geo.setIndex(count > 65535 ? new this.THREE.Uint32BufferAttribute(out, 1) : new this.THREE.Uint16BufferAttribute(out, 1));
|
|
5059
|
+
}
|
|
5060
|
+
const mats = materialsOf(host.mesh.material).map((m) => m.clone());
|
|
5061
|
+
const loose = new this.THREE.Mesh(geo, mats[0]);
|
|
5062
|
+
loose.name = `${host.mesh.name || "mesh"}:${v0}-${v1}`;
|
|
5063
|
+
loose.castShadow = host.mesh.castShadow;
|
|
5064
|
+
loose.receiveShadow = host.mesh.receiveShadow;
|
|
5065
|
+
loose.renderOrder = host.mesh.renderOrder;
|
|
5066
|
+
loose.frustumCulled = host.mesh.frustumCulled;
|
|
5067
|
+
loose.layers.mask = host.mesh.layers.mask;
|
|
5068
|
+
loose.position.copy(host.mesh.position);
|
|
5069
|
+
loose.quaternion.copy(host.mesh.quaternion);
|
|
5070
|
+
loose.scale.copy(host.mesh.scale);
|
|
5071
|
+
const parent = host.mesh.parent;
|
|
5072
|
+
if (parent) parent.add(loose);
|
|
5073
|
+
loose.updateMatrixWorld(true);
|
|
5074
|
+
this.collapse(mv);
|
|
5075
|
+
for (const d of mv.decals) {
|
|
5076
|
+
d.obj.visible = true;
|
|
5077
|
+
loose.add(d.obj);
|
|
5078
|
+
}
|
|
5079
|
+
const lp = positionAttr(geo);
|
|
5080
|
+
const lh = {
|
|
5081
|
+
mesh: loose,
|
|
5082
|
+
origGeometry: geo,
|
|
5083
|
+
origMaterial: loose.material,
|
|
5084
|
+
ownGeo: true,
|
|
5085
|
+
work: new Float32Array(lp.array),
|
|
5086
|
+
ownMat: true,
|
|
5087
|
+
sharers: [mv]
|
|
5088
|
+
};
|
|
5089
|
+
mv.host = lh;
|
|
5090
|
+
mv.range = [0, count];
|
|
5091
|
+
mv.mesh = loose;
|
|
5092
|
+
mv.loose = true;
|
|
5093
|
+
mv.moved = true;
|
|
5094
|
+
group.attach(loose);
|
|
5095
|
+
for (const m of materialsOf(loose.material)) m.needsUpdate = true;
|
|
5096
|
+
}
|
|
5097
|
+
restoreRanged(mv) {
|
|
5098
|
+
const merged = mv.merged;
|
|
5099
|
+
const host = merged.host;
|
|
5100
|
+
for (const d of mv.decals) this.disposeDecal(d);
|
|
5101
|
+
mv.decals.length = 0;
|
|
5102
|
+
if (mv.loose) {
|
|
5103
|
+
const loose = mv.mesh;
|
|
5104
|
+
loose.parent?.remove(loose);
|
|
5105
|
+
loose.geometry.dispose();
|
|
5106
|
+
for (const m of materialsOf(loose.material)) m.dispose();
|
|
5107
|
+
mv.host = host;
|
|
5108
|
+
mv.range = [merged.range[0], merged.range[1]];
|
|
5109
|
+
mv.mesh = host.mesh;
|
|
5110
|
+
mv.loose = false;
|
|
5111
|
+
mv.moved = false;
|
|
5112
|
+
}
|
|
5113
|
+
mv.collapsed = false;
|
|
5114
|
+
mv.hidden = false;
|
|
5115
|
+
mv.meltApplied = 0;
|
|
5116
|
+
mv.charK = 0;
|
|
5117
|
+
mv.glowK = 0;
|
|
5118
|
+
mv.touched = false;
|
|
5119
|
+
if (!host.ownGeo) return;
|
|
5120
|
+
const work = host.work;
|
|
5121
|
+
const op = positionAttr(host.origGeometry);
|
|
5122
|
+
const [v0, v1] = merged.range;
|
|
5123
|
+
if (op) work.set(op.array.subarray(v0 * 3, v1 * 3), v0 * 3);
|
|
5124
|
+
const attr = host.mesh.geometry.getAttribute("color");
|
|
5125
|
+
if (attr) {
|
|
5126
|
+
const orig = host.origGeometry.getAttribute("color");
|
|
5127
|
+
const size = attr.itemSize;
|
|
5128
|
+
const a = attr.array;
|
|
5129
|
+
for (let i = v0; i < v1; i++) {
|
|
5130
|
+
for (let c = 0; c < 3; c++) a[i * size + c] = orig ? orig.array[i * orig.itemSize + c] : 1;
|
|
5131
|
+
}
|
|
5132
|
+
attr.needsUpdate = true;
|
|
5133
|
+
}
|
|
5134
|
+
if (host.sharers.every((s) => !s.touched)) {
|
|
5135
|
+
host.mesh.geometry.dispose();
|
|
5136
|
+
host.mesh.geometry = host.origGeometry;
|
|
5137
|
+
host.work = null;
|
|
5138
|
+
host.ownGeo = false;
|
|
5139
|
+
if (host.ownMat) {
|
|
5140
|
+
for (const m of materialsOf(host.mesh.material)) m.dispose();
|
|
5141
|
+
host.mesh.material = host.origMaterial;
|
|
5142
|
+
host.ownMat = false;
|
|
5143
|
+
}
|
|
5144
|
+
} else this.rebuildHost(host);
|
|
4788
5145
|
}
|
|
4789
5146
|
/** Bring one mesh back to how it was when it was bound. */
|
|
4790
5147
|
restoreMesh(mv) {
|
|
5148
|
+
if (mv.merged) {
|
|
5149
|
+
this.restoreRanged(mv);
|
|
5150
|
+
return;
|
|
5151
|
+
}
|
|
4791
5152
|
const mesh = mv.mesh;
|
|
4792
5153
|
for (const d of mv.decals) this.disposeDecal(d);
|
|
4793
5154
|
mv.decals.length = 0;
|
|
@@ -4885,6 +5246,10 @@ void main() {
|
|
|
4885
5246
|
function materialsOf2(m) {
|
|
4886
5247
|
return Array.isArray(m) ? m : [m];
|
|
4887
5248
|
}
|
|
5249
|
+
function isMeshRange(v) {
|
|
5250
|
+
const r = v;
|
|
5251
|
+
return !!r.mesh && Array.isArray(r.range);
|
|
5252
|
+
}
|
|
4888
5253
|
function createDestructionRenderer(THREE, options) {
|
|
4889
5254
|
return new Renderer(THREE, options);
|
|
4890
5255
|
}
|
|
@@ -4978,12 +5343,23 @@ void main() {
|
|
|
4978
5343
|
const partIds = new Set((state?.parts ?? []).map((p) => p.id));
|
|
4979
5344
|
const byPart = /* @__PURE__ */ new Map();
|
|
4980
5345
|
const taken = /* @__PURE__ */ new Set();
|
|
5346
|
+
const put = (pid, e) => {
|
|
5347
|
+
const l = byPart.get(pid);
|
|
5348
|
+
if (l) l.push(e);
|
|
5349
|
+
else byPart.set(pid, [e]);
|
|
5350
|
+
};
|
|
4981
5351
|
const add2 = (pid, m) => {
|
|
4982
5352
|
if (taken.has(m)) return;
|
|
4983
5353
|
taken.add(m);
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
5354
|
+
put(pid, { mesh: m });
|
|
5355
|
+
};
|
|
5356
|
+
const addRange = (pid, r) => {
|
|
5357
|
+
const n = r.mesh.geometry.getAttribute("position")?.count ?? 0;
|
|
5358
|
+
const [v0, v1] = r.range;
|
|
5359
|
+
if (!Number.isInteger(v0) || !Number.isInteger(v1) || v0 < 0 || v1 <= v0 || v1 > n) {
|
|
5360
|
+
throw new Error(`game-destruction-three: invalid vertex range [${v0}, ${v1}) for part "${pid}" (the mesh has ${n} vertices)`);
|
|
5361
|
+
}
|
|
5362
|
+
put(pid, { mesh: r.mesh, range: [v0, v1] });
|
|
4987
5363
|
};
|
|
4988
5364
|
const meshesIn = (o) => {
|
|
4989
5365
|
const out = [];
|
|
@@ -4994,7 +5370,10 @@ void main() {
|
|
|
4994
5370
|
};
|
|
4995
5371
|
if (options.parts) {
|
|
4996
5372
|
for (const [pid, v] of Object.entries(options.parts)) {
|
|
4997
|
-
for (const o of Array.isArray(v) ? v : [v])
|
|
5373
|
+
for (const o of Array.isArray(v) ? v : [v]) {
|
|
5374
|
+
if (isMeshRange(o)) addRange(pid, o);
|
|
5375
|
+
else for (const m of meshesIn(o)) add2(pid, m);
|
|
5376
|
+
}
|
|
4998
5377
|
}
|
|
4999
5378
|
} else {
|
|
5000
5379
|
const leftovers = [];
|
|
@@ -5007,14 +5386,15 @@ void main() {
|
|
|
5007
5386
|
if (leftovers.length) this.matchByBox(objectId, root, leftovers, add2);
|
|
5008
5387
|
}
|
|
5009
5388
|
const bound = { id: objectId, root, sync: options.syncTransform !== false, parts: /* @__PURE__ */ new Map(), lastPos: null, lastQuat: null, piece: null, isSplit: false };
|
|
5010
|
-
|
|
5389
|
+
const hosts = /* @__PURE__ */ new Map();
|
|
5390
|
+
for (const pid of partIds) bound.parts.set(pid, this.makePartVis(objectId, pid, state?.parts.find((p) => p.id === pid)?.material ?? "", byPart.get(pid) ?? [], hosts));
|
|
5011
5391
|
for (const [pid, meshes] of byPart) {
|
|
5012
|
-
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes));
|
|
5392
|
+
if (!bound.parts.has(pid)) bound.parts.set(pid, this.makePartVis(objectId, pid, this.world.part(objectId, pid)?.material ?? "", meshes, hosts));
|
|
5013
5393
|
}
|
|
5014
5394
|
this.bounds.set(objectId, bound);
|
|
5015
5395
|
}
|
|
5016
|
-
makePartVis(home, id, material, meshes) {
|
|
5017
|
-
return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m, home)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
|
|
5396
|
+
makePartVis(home, id, material, meshes, hosts) {
|
|
5397
|
+
return { id, home, material, meshes: meshes.map((m) => this.visuals.makeMeshVis(m.mesh, home, m.range, hosts)), charApplied: 0, glowApplied: 0, meltApplied: 0, gone: "none" };
|
|
5018
5398
|
}
|
|
5019
5399
|
/** Match unlabelled meshes to the part whose shape contains the centre of the mesh's bounding box. */
|
|
5020
5400
|
matchByBox(objectId, root, meshes, add2) {
|
|
@@ -5074,11 +5454,13 @@ void main() {
|
|
|
5074
5454
|
const inv = root.matrixWorld.clone().invert();
|
|
5075
5455
|
const total = new this.THREE.Box3();
|
|
5076
5456
|
const m4 = new this.THREE.Matrix4();
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5457
|
+
const lb = new this.THREE.Box3();
|
|
5458
|
+
for (const mv of meshes) {
|
|
5459
|
+
const m = mv.mesh;
|
|
5460
|
+
m.updateWorldMatrix(true, false);
|
|
5461
|
+
this.visuals.localBox(mv, lb);
|
|
5462
|
+
if (lb.isEmpty()) continue;
|
|
5463
|
+
total.union(lb.clone().applyMatrix4(m4.multiplyMatrices(inv, m.matrixWorld)));
|
|
5082
5464
|
}
|
|
5083
5465
|
if (total.isEmpty()) return { half: [0.25, 0.25, 0.25], center: new this.THREE.Vector3() };
|
|
5084
5466
|
const size = total.getSize(new this.THREE.Vector3());
|
|
@@ -5086,7 +5468,7 @@ void main() {
|
|
|
5086
5468
|
}
|
|
5087
5469
|
meshesOf(b) {
|
|
5088
5470
|
const out = [];
|
|
5089
|
-
for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv
|
|
5471
|
+
for (const pv of b.parts.values()) if (pv.gone === "none") for (const mv of pv.meshes) out.push(mv);
|
|
5090
5472
|
return out;
|
|
5091
5473
|
}
|
|
5092
5474
|
massOf(objectId) {
|
|
@@ -5217,6 +5599,9 @@ void main() {
|
|
|
5217
5599
|
case "detach":
|
|
5218
5600
|
this.onDetach(ev);
|
|
5219
5601
|
break;
|
|
5602
|
+
case "collapse":
|
|
5603
|
+
this.splitLoose(ev.object);
|
|
5604
|
+
break;
|
|
5220
5605
|
case "disassemble_progress": {
|
|
5221
5606
|
const k = `unscrew:${ev.joint}`;
|
|
5222
5607
|
if (this.time - (this.throttles.get(k) ?? -1) > 0.25) {
|
|
@@ -5335,8 +5720,8 @@ void main() {
|
|
|
5335
5720
|
let best = null;
|
|
5336
5721
|
let bd = Infinity;
|
|
5337
5722
|
for (const mv of pv.meshes) {
|
|
5338
|
-
if (!
|
|
5339
|
-
const d = this.
|
|
5723
|
+
if (!this.visuals.shown(mv)) continue;
|
|
5724
|
+
const d = this.visuals.worldBox(mv, this.box).distanceToPoint(p);
|
|
5340
5725
|
if (d < bd) {
|
|
5341
5726
|
bd = d;
|
|
5342
5727
|
best = mv;
|
|
@@ -5356,7 +5741,7 @@ void main() {
|
|
|
5356
5741
|
const wp = new this.THREE.Vector3(ev.worldPoint[0], ev.worldPoint[1], ev.worldPoint[2]);
|
|
5357
5742
|
const wn = this.worldDir(b, ev.dent.normal);
|
|
5358
5743
|
for (const mv of pv.meshes) {
|
|
5359
|
-
if (!
|
|
5744
|
+
if (!this.visuals.shown(mv)) continue;
|
|
5360
5745
|
this.visuals.applyDent(mv, wp, wn, ev.dent);
|
|
5361
5746
|
}
|
|
5362
5747
|
if (def?.tags?.includes("metal")) this.particles.burst("sparks", ev.worldPoint, [wn.x, wn.y, wn.z], 4 + Math.min(8, ev.dent.depth * 150), 3.2);
|
|
@@ -5447,16 +5832,20 @@ void main() {
|
|
|
5447
5832
|
}
|
|
5448
5833
|
onDetach(ev) {
|
|
5449
5834
|
this.sound("detach", this.rootPoint(ev.object), 0.5);
|
|
5835
|
+
this.splitLoose(ev.object);
|
|
5836
|
+
}
|
|
5837
|
+
/** Turn every loose group of the object (a detach, a collapse) into a piece of its own. */
|
|
5838
|
+
splitLoose(objectId) {
|
|
5450
5839
|
if (!this.autoSplit) return;
|
|
5451
|
-
const b = this.bounds.get(
|
|
5840
|
+
const b = this.bounds.get(objectId);
|
|
5452
5841
|
if (!b) return;
|
|
5453
|
-
const st = this.world.state(
|
|
5842
|
+
const st = this.world.state(objectId);
|
|
5454
5843
|
if (!st || st.groups.length < 2) return;
|
|
5455
5844
|
const loose = st.groups.length - 1;
|
|
5456
5845
|
for (let i = 0; i < loose; i++) {
|
|
5457
5846
|
let newId;
|
|
5458
5847
|
try {
|
|
5459
|
-
newId = this.world.splitGroup(
|
|
5848
|
+
newId = this.world.splitGroup(objectId, 1);
|
|
5460
5849
|
} catch {
|
|
5461
5850
|
break;
|
|
5462
5851
|
}
|
|
@@ -5467,7 +5856,7 @@ void main() {
|
|
|
5467
5856
|
const { half, center } = this.measure(b.root, this.meshesOf(b));
|
|
5468
5857
|
old.halfExtents = half;
|
|
5469
5858
|
this.pieceSys.setCenter(old, center);
|
|
5470
|
-
old.mass = Math.max(0.05, this.massOf(
|
|
5859
|
+
old.mass = Math.max(0.05, this.massOf(objectId));
|
|
5471
5860
|
old.sleeping = false;
|
|
5472
5861
|
}
|
|
5473
5862
|
}
|
|
@@ -5489,8 +5878,11 @@ void main() {
|
|
|
5489
5878
|
from.parts.delete(p.id);
|
|
5490
5879
|
nb.parts.set(p.id, pv);
|
|
5491
5880
|
for (const mv of pv.meshes) {
|
|
5492
|
-
|
|
5493
|
-
|
|
5881
|
+
if (mv.host && !mv.loose) this.visuals.extract(mv, group);
|
|
5882
|
+
else {
|
|
5883
|
+
group.attach(mv.mesh);
|
|
5884
|
+
mv.moved = true;
|
|
5885
|
+
}
|
|
5494
5886
|
}
|
|
5495
5887
|
}
|
|
5496
5888
|
this.bounds.set(newId, nb);
|
|
@@ -5606,7 +5998,7 @@ void main() {
|
|
|
5606
5998
|
}
|
|
5607
5999
|
onGone(objectId, pv, reason, def) {
|
|
5608
6000
|
const bb = new this.THREE.Box3();
|
|
5609
|
-
for (const mv of pv.meshes) bb.union(this.
|
|
6001
|
+
for (const mv of pv.meshes) bb.union(this.visuals.worldBox(mv, this.box));
|
|
5610
6002
|
this.visuals.hide(pv, reason);
|
|
5611
6003
|
if (bb.isEmpty()) return;
|
|
5612
6004
|
const c = bb.getCenter(new this.THREE.Vector3());
|
|
@@ -5623,7 +6015,7 @@ void main() {
|
|
|
5623
6015
|
const bb = this.box;
|
|
5624
6016
|
bb.makeEmpty();
|
|
5625
6017
|
const tmp = new this.THREE.Box3();
|
|
5626
|
-
for (const mv of pv.meshes) if (
|
|
6018
|
+
for (const mv of pv.meshes) if (this.visuals.shown(mv)) bb.union(this.visuals.worldBox(mv, tmp));
|
|
5627
6019
|
let cx = f.point[0];
|
|
5628
6020
|
let cy = f.point[1];
|
|
5629
6021
|
let cz = f.point[2];
|
|
@@ -10850,6 +11242,9 @@ void main() {
|
|
|
10850
11242
|
"ui.quality.medium": "medium",
|
|
10851
11243
|
"ui.quality.high": "high",
|
|
10852
11244
|
"ui.equip": "Equip as a tool",
|
|
11245
|
+
"ui.search_items": "Search items (all groups)",
|
|
11246
|
+
"ui.search_tests": "Search tests",
|
|
11247
|
+
"ui.no_results": "Nothing found",
|
|
10853
11248
|
"ui.objects": "{n} objects",
|
|
10854
11249
|
"ui.tests": "Tests",
|
|
10855
11250
|
"ui.run_all": "Run all",
|
|
@@ -10885,8 +11280,8 @@ void main() {
|
|
|
10885
11280
|
"sc.concrete_stops_bullet.check": "9 mm into 40 cm of concrete: went through: {through}, {dents}, {holes}, {cracks}, block {hp} hp (expected: stopped, wall behind untouched)",
|
|
10886
11281
|
"sc.hammer_dents_drum": "Hammer dents a steel drum",
|
|
10887
11282
|
"sc.hammer_dents_drum.check": "Hammer on the drum: {swings} swing(s), {dents} dent(s), first {depth} mm deep, body {hp} hp (expected: dented, not broken)",
|
|
10888
|
-
"sc.glass_shatters": "Glass pane shatters
|
|
10889
|
-
"sc.glass_shatters.check": "Glass pane: broken after {shots} shot(s) into {n} shards (expected: broken, at least 4 shards)",
|
|
11283
|
+
"sc.glass_shatters": "Glass pane shatters on the first 9 mm shot",
|
|
11284
|
+
"sc.glass_shatters.check": "Glass pane: broken after {shots} shot(s) into {n} shards (expected: broken on the first shot, at least 4 shards)",
|
|
10890
11285
|
"sc.shotgun_splinters_crate": "Shotgun splinters a wooden crate",
|
|
10891
11286
|
"sc.shotgun_splinters_crate.check": "12 ga on the crate: {shots} shot(s), {splinters} board(s) splintered, {holes} holes (expected: at least one splintered)",
|
|
10892
11287
|
"sc.kick_chair_leg": "A kick takes a chair leg off",
|
|
@@ -11139,6 +11534,9 @@ void main() {
|
|
|
11139
11534
|
"ui.quality.medium": "m\xE9dia",
|
|
11140
11535
|
"ui.quality.high": "alta",
|
|
11141
11536
|
"ui.equip": "Equipar como ferramenta",
|
|
11537
|
+
"ui.search_items": "Buscar item (todas as abas)",
|
|
11538
|
+
"ui.search_tests": "Buscar teste",
|
|
11539
|
+
"ui.no_results": "Nada encontrado",
|
|
11142
11540
|
"ui.objects": "{n} objetos",
|
|
11143
11541
|
"ui.tests": "Testes",
|
|
11144
11542
|
"ui.run_all": "Rodar todos",
|
|
@@ -11174,8 +11572,8 @@ void main() {
|
|
|
11174
11572
|
"sc.concrete_stops_bullet.check": "9 mm em 40 cm de concreto: atravessou: {through}, {dents}, {holes}, {cracks}, bloco com {hp} hp (esperado: parou, parede atr\xE1s intacta)",
|
|
11175
11573
|
"sc.hammer_dents_drum": "Martelo amassa um tambor de a\xE7o",
|
|
11176
11574
|
"sc.hammer_dents_drum.check": "Martelo no tambor: {swings} golpe(s), {dents} amassado(s), o primeiro com {depth} mm, corpo com {hp} hp (esperado: amassado, sem quebrar)",
|
|
11177
|
-
"sc.glass_shatters": "Vidro estilha\xE7a
|
|
11178
|
-
"sc.glass_shatters.check": "Vidro: quebrou depois de {shots} tiro(s) em {n} cacos (esperado: quebrado, pelo menos 4 cacos)",
|
|
11575
|
+
"sc.glass_shatters": "Vidro estilha\xE7a no primeiro tiro de 9 mm",
|
|
11576
|
+
"sc.glass_shatters.check": "Vidro: quebrou depois de {shots} tiro(s) em {n} cacos (esperado: quebrado no primeiro tiro, pelo menos 4 cacos)",
|
|
11179
11577
|
"sc.shotgun_splinters_crate": "Escopeta lasca um caixote de madeira",
|
|
11180
11578
|
"sc.shotgun_splinters_crate.check": "Calibre 12 no caixote: {shots} tiro(s), {splinters} t\xE1bua(s) lascada(s), {holes} furos (esperado: pelo menos uma lascada)",
|
|
11181
11579
|
"sc.kick_chair_leg": "Um chute solta a perna da cadeira",
|
|
@@ -12617,7 +13015,7 @@ void main() {
|
|
|
12617
13015
|
step(0.5);
|
|
12618
13016
|
const fr = room.log.entries({ type: "fracture", objectId: id }).find((e) => e.part === "pane");
|
|
12619
13017
|
const n = Number(fr?.data?.fragments ?? 0);
|
|
12620
|
-
expect(part(room, id, "pane")?.status === "broken" && n >= 4, t("sc.glass_shatters.check", { shots, n }), { shots, fragments: n });
|
|
13018
|
+
expect(part(room, id, "pane")?.status === "broken" && shots === 1 && n >= 4, t("sc.glass_shatters.check", { shots, n }), { shots, fragments: n });
|
|
12621
13019
|
}),
|
|
12622
13020
|
sc("shotgun_splinters_crate", ({ room, step, expect }) => {
|
|
12623
13021
|
const id = at(room, "wooden_crate", 0, -4);
|
|
@@ -12876,7 +13274,11 @@ void main() {
|
|
|
12876
13274
|
var CSS = `
|
|
12877
13275
|
.tr-root{position:absolute;inset:0;pointer-events:none;font:12px/1.35 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;color:#e8eaee;overflow:hidden}
|
|
12878
13276
|
.tr-root *{box-sizing:border-box}
|
|
12879
|
-
.tr-root button,.tr-root select,.tr-root label{pointer-events:auto}
|
|
13277
|
+
.tr-root button,.tr-root select,.tr-root label,.tr-root input{pointer-events:auto}
|
|
13278
|
+
.tr-search{width:100%;margin:0 0 6px;padding:5px 8px;border-radius:5px;border:1px solid #3a404c;background:#0f1116;color:#e8eaee;font:inherit}
|
|
13279
|
+
.tr-search::placeholder{color:#7d8594}
|
|
13280
|
+
.tr-item small{color:#7d8594;margin-left:4px}
|
|
13281
|
+
.tr-empty{color:#9aa3b2;padding:4px 2px;grid-column:1/-1}
|
|
12880
13282
|
.tr-cross{position:absolute;left:50%;top:50%;width:18px;height:18px;margin:-9px 0 0 -9px}
|
|
12881
13283
|
.tr-cross:before,.tr-cross:after{content:"";position:absolute;background:#fff;opacity:.85;box-shadow:0 0 2px #000}
|
|
12882
13284
|
.tr-cross:before{left:8px;top:0;width:2px;height:18px}.tr-cross:after{top:8px;left:0;height:2px;width:18px}
|
|
@@ -12927,6 +13329,7 @@ void main() {
|
|
|
12927
13329
|
.tr-summary{margin-top:6px;color:#9aa3b2}
|
|
12928
13330
|
@media (max-width:980px){.tr-left,.tr-right{display:none}.tr-root.open .tr-left,.tr-root.open .tr-right{display:flex}.tr-toggle{display:block;pointer-events:auto}.tr-root.open .tr-toggle{right:min(310px,34vw)}.tr-left{width:min(400px,46vw)}.tr-help{display:none}}
|
|
12929
13331
|
`;
|
|
13332
|
+
var fold = (text) => text.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
12930
13333
|
var mk = (tag, cls, text) => {
|
|
12931
13334
|
const e = document.createElement(tag);
|
|
12932
13335
|
if (cls) e.className = cls;
|
|
@@ -13066,15 +13469,27 @@ void main() {
|
|
|
13066
13469
|
const tests = mk("div", "tr-panel tr-tests");
|
|
13067
13470
|
right.append(cat, anim, tests);
|
|
13068
13471
|
cat.appendChild(mk("h3", void 0, t("ui.catalog")));
|
|
13472
|
+
const itemSearch = mk("input", "tr-search");
|
|
13473
|
+
itemSearch.type = "search";
|
|
13474
|
+
itemSearch.placeholder = t("ui.search_items");
|
|
13069
13475
|
const tabs = mk("div", "tr-tabs");
|
|
13070
13476
|
const items = mk("div", "tr-items");
|
|
13071
|
-
cat.append(tabs, items);
|
|
13477
|
+
cat.append(itemSearch, tabs, items);
|
|
13072
13478
|
let activeGroup = "";
|
|
13479
|
+
const visibleItems = () => {
|
|
13480
|
+
const term = fold(itemSearch.value.trim());
|
|
13481
|
+
if (!term) return (o.groups().get(activeGroup) ?? []).map((item) => ({ item, group: activeGroup }));
|
|
13482
|
+
return [...o.groups()].flatMap(([group, list2]) => list2.filter((item) => fold(`${item.name} ${item.id} ${group}`).includes(term)).map((item) => ({ item, group })));
|
|
13483
|
+
};
|
|
13073
13484
|
const renderItems = () => {
|
|
13074
13485
|
items.textContent = "";
|
|
13075
|
-
|
|
13486
|
+
const searching = itemSearch.value.trim() !== "";
|
|
13487
|
+
const found = visibleItems();
|
|
13488
|
+
if (!found.length) items.appendChild(mk("div", "tr-empty", t("ui.no_results")));
|
|
13489
|
+
for (const { item: it, group } of found) {
|
|
13076
13490
|
const wrap = mk("div", "tr-item");
|
|
13077
13491
|
const b = mk("button", "tr-btn", it.name);
|
|
13492
|
+
if (searching) b.appendChild(mk("small", void 0, group));
|
|
13078
13493
|
b.type = "button";
|
|
13079
13494
|
b.title = it.note ?? it.name;
|
|
13080
13495
|
b.onclick = () => o.onSpawn(it.id);
|
|
@@ -13105,6 +13520,8 @@ void main() {
|
|
|
13105
13520
|
renderItems();
|
|
13106
13521
|
};
|
|
13107
13522
|
renderTabs();
|
|
13523
|
+
itemSearch.oninput = renderItems;
|
|
13524
|
+
for (const box2 of [itemSearch]) box2.addEventListener("keydown", (e) => e.stopPropagation());
|
|
13108
13525
|
const controls = mk("div", "tr-row");
|
|
13109
13526
|
const ctl = (label, fn) => {
|
|
13110
13527
|
const b = mk("button", "tr-btn", label);
|
|
@@ -13146,8 +13563,16 @@ void main() {
|
|
|
13146
13563
|
const runAll = mk("button", "tr-btn", t("ui.run_all"));
|
|
13147
13564
|
runAll.type = "button";
|
|
13148
13565
|
const summary = mk("div", "tr-summary");
|
|
13566
|
+
const testSearch = mk("input", "tr-search");
|
|
13567
|
+
testSearch.type = "search";
|
|
13568
|
+
testSearch.placeholder = t("ui.search_tests");
|
|
13569
|
+
testSearch.addEventListener("keydown", (e) => e.stopPropagation());
|
|
13149
13570
|
const list = mk("div", "list");
|
|
13150
|
-
tests.append(runAll, summary, list);
|
|
13571
|
+
tests.append(runAll, summary, testSearch, list);
|
|
13572
|
+
testSearch.oninput = () => {
|
|
13573
|
+
const term = fold(testSearch.value.trim());
|
|
13574
|
+
for (const b of scBtns.values()) b.style.display = !term || fold(b.textContent ?? "").includes(term) ? "" : "none";
|
|
13575
|
+
};
|
|
13151
13576
|
const scBtns = /* @__PURE__ */ new Map();
|
|
13152
13577
|
let busy = false;
|
|
13153
13578
|
const mark = (rep) => {
|
|
@@ -13346,6 +13771,11 @@ void main() {
|
|
|
13346
13771
|
renderTabs();
|
|
13347
13772
|
renderScenarios();
|
|
13348
13773
|
},
|
|
13774
|
+
showGroup(group) {
|
|
13775
|
+
activeGroup = group;
|
|
13776
|
+
itemSearch.value = "";
|
|
13777
|
+
renderTabs();
|
|
13778
|
+
},
|
|
13349
13779
|
togglePanels,
|
|
13350
13780
|
dispose() {
|
|
13351
13781
|
offLog();
|
|
@@ -13416,7 +13846,13 @@ void main() {
|
|
|
13416
13846
|
const p = world.part(arena.table.id, arena.table.topPart);
|
|
13417
13847
|
return !!p && p.status !== "broken" && p.status !== "melted" && p.status !== "ashes";
|
|
13418
13848
|
};
|
|
13419
|
-
const
|
|
13849
|
+
const restingTops = [];
|
|
13850
|
+
const restingTopAt = (x, z) => {
|
|
13851
|
+
let top = 0;
|
|
13852
|
+
for (const c of restingTops) if (x > c.minX && x < c.maxX && z > c.minZ && z < c.maxZ && world.has(c.objectId)) top = Math.max(top, c.top);
|
|
13853
|
+
return top;
|
|
13854
|
+
};
|
|
13855
|
+
const groundAt = (x, z) => Math.max(x > arena.table.minX && x < arena.table.maxX && z > arena.table.minZ && z < arena.table.maxZ && tableOk() ? arena.table.top : 0, restingTopAt(x, z));
|
|
13420
13856
|
const ownFluids = !options.fluids;
|
|
13421
13857
|
const fluids = options.fluids ?? createFluids({ seed: 1, locale, liquids: options.liquids, groundAt });
|
|
13422
13858
|
const pristineFluids = ownFluids ? fluids.serialize() : null;
|
|
@@ -13549,16 +13985,16 @@ void main() {
|
|
|
13549
13985
|
const puncture = (ev) => {
|
|
13550
13986
|
const e = entryOf(ev.object);
|
|
13551
13987
|
if (!e?.containerId || e.fluidPart !== ev.part) return;
|
|
13552
|
-
const
|
|
13553
|
-
|
|
13554
|
-
tmpM.copy(
|
|
13988
|
+
const host2 = hostOf(e);
|
|
13989
|
+
host2.root.updateMatrixWorld(true);
|
|
13990
|
+
tmpM.copy(host2.root.matrixWorld).invert();
|
|
13555
13991
|
const local = tmpV2.set(ev.point[0], ev.point[1], ev.point[2]).applyMatrix4(tmpM);
|
|
13556
13992
|
local.x -= e.fluidOffset[0];
|
|
13557
13993
|
local.y -= e.fluidOffset[1];
|
|
13558
13994
|
local.z -= e.fluidOffset[2];
|
|
13559
|
-
|
|
13995
|
+
host2.root.getWorldQuaternion(tmpQ).invert();
|
|
13560
13996
|
const n = tmpV22.set(ev.direction[0], ev.direction[1], ev.direction[2]).applyQuaternion(tmpQ);
|
|
13561
|
-
const part = world.part(
|
|
13997
|
+
const part = world.part(host2.objectId, ev.part);
|
|
13562
13998
|
const hole = part?.holes[part.holes.length - 1];
|
|
13563
13999
|
fluids.puncture(e.containerId, [local.x, local.y, local.z], [n.x, n.y, n.z], hole?.diameter ?? 9e-3);
|
|
13564
14000
|
};
|
|
@@ -13764,6 +14200,7 @@ void main() {
|
|
|
13764
14200
|
};
|
|
13765
14201
|
const clear = () => {
|
|
13766
14202
|
clearEverything();
|
|
14203
|
+
restingTops.length = 0;
|
|
13767
14204
|
pending = [];
|
|
13768
14205
|
action("clear", t("act.clear"));
|
|
13769
14206
|
};
|
|
@@ -13983,6 +14420,7 @@ void main() {
|
|
|
13983
14420
|
}
|
|
13984
14421
|
});
|
|
13985
14422
|
player.kick(recoil[0], recoil[1]);
|
|
14423
|
+
emitHost({ type: "shot", weapon: toolId, shots: shots.length, recoil });
|
|
13986
14424
|
const f = first;
|
|
13987
14425
|
const d = f ? num(f.distance, 1) : "";
|
|
13988
14426
|
let text;
|
|
@@ -14222,7 +14660,8 @@ void main() {
|
|
|
14222
14660
|
if (cat === "hand") return;
|
|
14223
14661
|
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
14224
14662
|
if (res.status === "empty") {
|
|
14225
|
-
|
|
14663
|
+
emitHost({ type: "empty", weapon: toolId });
|
|
14664
|
+
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
14226
14665
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
14227
14666
|
return;
|
|
14228
14667
|
}
|
|
@@ -14274,6 +14713,7 @@ void main() {
|
|
|
14274
14713
|
progress = null;
|
|
14275
14714
|
action("tool", t("act.tool", { weapon: toolName(id) }), { data: { weapon: id } });
|
|
14276
14715
|
fire("tool", id);
|
|
14716
|
+
emitHost({ type: "tool", weapon: id });
|
|
14277
14717
|
};
|
|
14278
14718
|
const cycleTool = (dir) => {
|
|
14279
14719
|
if (!loadout.length) return;
|
|
@@ -14501,7 +14941,7 @@ void main() {
|
|
|
14501
14941
|
case "KeyR":
|
|
14502
14942
|
if (heldPiece) heldYaw += Math.PI / 8;
|
|
14503
14943
|
else {
|
|
14504
|
-
|
|
14944
|
+
startReload();
|
|
14505
14945
|
say(t("hud.reloading"));
|
|
14506
14946
|
}
|
|
14507
14947
|
break;
|
|
@@ -14599,10 +15039,10 @@ void main() {
|
|
|
14599
15039
|
const syncContainers = (sdt) => {
|
|
14600
15040
|
for (const e of entries.values()) {
|
|
14601
15041
|
if (!e.containerId) continue;
|
|
14602
|
-
const
|
|
14603
|
-
|
|
14604
|
-
const q =
|
|
14605
|
-
const p = tmpV2.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(
|
|
15042
|
+
const host2 = hostOf(e);
|
|
15043
|
+
host2.root.updateMatrixWorld(true);
|
|
15044
|
+
const q = host2.root.getWorldQuaternion(tmpQ);
|
|
15045
|
+
const p = tmpV2.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(host2.root.matrixWorld);
|
|
14606
15046
|
const vel = sdt > 0 ? [(p.x - e.last[0]) / sdt, (p.y - e.last[1]) / sdt, (p.z - e.last[2]) / sdt] : void 0;
|
|
14607
15047
|
e.last = [p.x, p.y, p.z];
|
|
14608
15048
|
fluids.setTransform(e.containerId, [p.x, p.y, p.z], [q.x, q.y, q.z, q.w], vel);
|
|
@@ -14613,9 +15053,9 @@ void main() {
|
|
|
14613
15053
|
if (!e.containerId || !e.fluidPart) continue;
|
|
14614
15054
|
const c = fluids.container(e.containerId);
|
|
14615
15055
|
if (!c) continue;
|
|
14616
|
-
const
|
|
14617
|
-
const part = world.part(
|
|
14618
|
-
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(
|
|
15056
|
+
const host2 = hostOf(e);
|
|
15057
|
+
const part = world.part(host2.objectId, e.fluidPart);
|
|
15058
|
+
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host2.objectId, e.fluidPart, c.liters);
|
|
14619
15059
|
}
|
|
14620
15060
|
};
|
|
14621
15061
|
let paused = false;
|
|
@@ -14646,6 +15086,9 @@ void main() {
|
|
|
14646
15086
|
updateImpacts();
|
|
14647
15087
|
fluidView.update(dt);
|
|
14648
15088
|
for (const s of tools.values()) s.update(dt);
|
|
15089
|
+
if (reloadingSeen && !tool.reloading) emitHost({ type: "reload", weapon: toolId, phase: "end" });
|
|
15090
|
+
reloadingSeen = tool.reloading;
|
|
15091
|
+
for (const update2 of frameListeners) update2(dt);
|
|
14649
15092
|
for (const tr of tracers) {
|
|
14650
15093
|
if (tr.life > 0) {
|
|
14651
15094
|
tr.life -= dt;
|
|
@@ -14787,6 +15230,36 @@ void main() {
|
|
|
14787
15230
|
const moduleCheck = (ok, text, data, itemId) => {
|
|
14788
15231
|
log.add({ channel: "check", type: ok ? "pass" : "fail", text: `${ok ? "\u2713" : "\u2717"} ${text}`, itemId, data: { ok, ...data } });
|
|
14789
15232
|
};
|
|
15233
|
+
const modules = /* @__PURE__ */ new Map();
|
|
15234
|
+
const detachers = [];
|
|
15235
|
+
const hostListeners = /* @__PURE__ */ new Set();
|
|
15236
|
+
const frameListeners = /* @__PURE__ */ new Set();
|
|
15237
|
+
let reloadingSeen = false;
|
|
15238
|
+
function emitHost(event) {
|
|
15239
|
+
for (const listener of hostListeners) listener(event);
|
|
15240
|
+
}
|
|
15241
|
+
function startReload() {
|
|
15242
|
+
if (tool.reload()) emitHost({ type: "reload", weapon: toolId, phase: "start" });
|
|
15243
|
+
}
|
|
15244
|
+
const host = {
|
|
15245
|
+
THREE,
|
|
15246
|
+
scene,
|
|
15247
|
+
camera,
|
|
15248
|
+
locale,
|
|
15249
|
+
groundAt,
|
|
15250
|
+
tool: () => ({ weapon: toolId, category: tool.def.category, ammo: tool.ammo, capacity: tool.capacity, reserve: tool.reserve, reloading: tool.reloading }),
|
|
15251
|
+
on(listener) {
|
|
15252
|
+
hostListeners.add(listener);
|
|
15253
|
+
return () => hostListeners.delete(listener);
|
|
15254
|
+
},
|
|
15255
|
+
onFrame(update2) {
|
|
15256
|
+
frameListeners.add(update2);
|
|
15257
|
+
return () => frameListeners.delete(update2);
|
|
15258
|
+
},
|
|
15259
|
+
log(text, data) {
|
|
15260
|
+
action("module_note", text, { data });
|
|
15261
|
+
}
|
|
15262
|
+
};
|
|
14790
15263
|
const loadModule = (mod) => {
|
|
14791
15264
|
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
14792
15265
|
const fail = (problem) => {
|
|
@@ -14851,6 +15324,8 @@ void main() {
|
|
|
14851
15324
|
report.scenarios.push(sc.id);
|
|
14852
15325
|
}
|
|
14853
15326
|
action("module", t("act.module_loaded", { name: mod.name, items: report.items.length, scenarios: report.scenarios.length, result: t(report.ok ? "mod.all_ok" : "mod.some_failed") }), { data: { module: mod.id, items: report.items, scenarios: report.scenarios, ok: report.ok } });
|
|
15327
|
+
modules.set(mod.id, mod);
|
|
15328
|
+
if (mod.attach) detachers.push(mod.attach(host));
|
|
14854
15329
|
ui?.refresh();
|
|
14855
15330
|
return report;
|
|
14856
15331
|
};
|
|
@@ -14906,6 +15381,37 @@ void main() {
|
|
|
14906
15381
|
}
|
|
14907
15382
|
return report;
|
|
14908
15383
|
};
|
|
15384
|
+
const layArea = (layout) => {
|
|
15385
|
+
const holders = layout.placements.filter((p) => p.surface);
|
|
15386
|
+
const rest = layout.placements.filter((p) => !p.surface);
|
|
15387
|
+
const placed = [...holders, ...rest].map((p) => {
|
|
15388
|
+
const so = spawn(p.item, p.position, p.rotationY ?? 0);
|
|
15389
|
+
if (p.surface) {
|
|
15390
|
+
const [hx, hz] = p.surface.halfSize;
|
|
15391
|
+
restingTops.push({ objectId: so.objectId, minX: p.position[0] - hx, maxX: p.position[0] + hx, minZ: p.position[2] - hz, maxZ: p.position[2] + hz, top: p.surface.height });
|
|
15392
|
+
}
|
|
15393
|
+
return { so, asleep: p.asleep === true };
|
|
15394
|
+
});
|
|
15395
|
+
for (const { so, asleep } of placed) {
|
|
15396
|
+
const piece = asleep ? rendererPieces.get(so.objectId) : void 0;
|
|
15397
|
+
if (piece) piece.sleeping = true;
|
|
15398
|
+
}
|
|
15399
|
+
if (layout.view) room.look(layout.view.eye, layout.view.target);
|
|
15400
|
+
return placed.map(({ so }) => so);
|
|
15401
|
+
};
|
|
15402
|
+
const openArea = (area) => {
|
|
15403
|
+
clear();
|
|
15404
|
+
const mod = [...modules.values()].find((m) => m.id === area || m.group === area);
|
|
15405
|
+
const label = mod?.group ?? S.groups[area] ?? area;
|
|
15406
|
+
ui?.showGroup(label);
|
|
15407
|
+
const layout = mod?.area?.(locale);
|
|
15408
|
+
if (layout) return layArea(layout);
|
|
15409
|
+
const items = catalog.filter((item) => item.group === label);
|
|
15410
|
+
const front = items.filter((item) => item.movable !== false);
|
|
15411
|
+
const back = items.filter((item) => item.movable === false);
|
|
15412
|
+
const row = (list, gap, z) => list.map((item, index) => spawn(item.id, [(index - (list.length - 1) / 2) * gap, 0, z], 0));
|
|
15413
|
+
return [...row(front, 1.6, 1.5), ...row(back, 4.5, -4)];
|
|
15414
|
+
};
|
|
14909
15415
|
const room = {
|
|
14910
15416
|
world,
|
|
14911
15417
|
renderer,
|
|
@@ -14985,10 +15491,12 @@ void main() {
|
|
|
14985
15491
|
fastForward = on;
|
|
14986
15492
|
},
|
|
14987
15493
|
equip,
|
|
15494
|
+
openArea,
|
|
14988
15495
|
loadModule,
|
|
14989
15496
|
animations: animationsOf,
|
|
14990
15497
|
playAnimation,
|
|
14991
15498
|
dispose() {
|
|
15499
|
+
for (const detach of detachers.splice(0)) detach();
|
|
14992
15500
|
cancelAnimationFrame?.(raf);
|
|
14993
15501
|
window.removeEventListener("resize", onResize);
|
|
14994
15502
|
player.dispose();
|
|
@@ -15078,6 +15586,7 @@ void main() {
|
|
|
15078
15586
|
if (options.initial) {
|
|
15079
15587
|
for (const [i, id] of options.initial.entries()) spawn(id, [(i - (options.initial.length - 1) / 2) * 1.3, 0, 0.5]);
|
|
15080
15588
|
}
|
|
15589
|
+
if (options.area) openArea(options.area);
|
|
15081
15590
|
return room;
|
|
15082
15591
|
}
|
|
15083
15592
|
function structuredCloneSpec(spec) {
|