@adatechnology/game-test-room 0.18.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.d.ts +126 -16
- package/dist/index.global.js +2734 -645
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1123 -208
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ import { createDestructionRenderer, createDestructionSound, dragSoundKey, impact
|
|
|
5
5
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
6
6
|
import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
7
7
|
import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
8
|
-
import { createVfx } from "@adatechnology/game-vfx";
|
|
8
|
+
import { SPRITE_KINDS, createVfx, patchSpriteKinds } from "@adatechnology/game-vfx";
|
|
9
9
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
10
|
-
import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, shotExtras, useToHit } from "@adatechnology/game-contracts";
|
|
10
|
+
import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, sealBlock, sealOpener, shotExtras, twistStroke, TWIST_LIFT, useToHit } from "@adatechnology/game-contracts";
|
|
11
11
|
|
|
12
12
|
// src/arena.ts
|
|
13
13
|
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
@@ -110,7 +110,7 @@ function resolveScenery(scenery) {
|
|
|
110
110
|
if (!position) throw new Error(`game-test-room: scenery "${id}" is new, so it needs a position`);
|
|
111
111
|
merged.set(id, { ...obj, position });
|
|
112
112
|
}
|
|
113
|
-
return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, follow: o.follow, label: o.label, top: o.top, spout: o.spout, free: o.free }));
|
|
113
|
+
return [...merged.entries()].map(([id, o]) => ({ id, spec: o.spec, position: o.position, rotationY: o.rotationY ?? 0, colors: o.colors, build: o.build, follow: o.follow, label: o.label, top: o.top, spout: o.spout, free: o.free, pushable: o.pushable }));
|
|
114
114
|
}
|
|
115
115
|
function createArena(THREE, size, scene, scenery, tapOption) {
|
|
116
116
|
const group = markKitOrigin(new THREE.Group(), { owner: "game-test-room", kind: "room" });
|
|
@@ -435,8 +435,8 @@ function buildFromSpec(THREE, spec, options = {}) {
|
|
|
435
435
|
const mats = /* @__PURE__ */ new Map();
|
|
436
436
|
const materialFor = (p) => {
|
|
437
437
|
const color = options.colors?.[p.id] ?? options.colors?.[p.material] ?? MATERIAL_COLORS[p.material] ?? 8947848;
|
|
438
|
-
const
|
|
439
|
-
let m = mats.get(
|
|
438
|
+
const key2 = `${p.material}:${color}`;
|
|
439
|
+
let m = mats.get(key2);
|
|
440
440
|
if (!m) {
|
|
441
441
|
const alpha = TRANSPARENT[p.material];
|
|
442
442
|
const metal = p.material === "steel" || p.material === "aluminium" || p.material === "copper" || p.material === "cast_iron";
|
|
@@ -448,7 +448,7 @@ function buildFromSpec(THREE, spec, options = {}) {
|
|
|
448
448
|
opacity: alpha ?? 1,
|
|
449
449
|
depthWrite: !alpha
|
|
450
450
|
});
|
|
451
|
-
mats.set(
|
|
451
|
+
mats.set(key2, m);
|
|
452
452
|
}
|
|
453
453
|
return m;
|
|
454
454
|
};
|
|
@@ -472,6 +472,7 @@ function buildFromSpec(THREE, spec, options = {}) {
|
|
|
472
472
|
}
|
|
473
473
|
|
|
474
474
|
// src/explosives.ts
|
|
475
|
+
import { releaseObject3D } from "@adatechnology/game-shapes-three";
|
|
475
476
|
import { ExplosiveBody, backblastEffect, blastEffect, createExplosiveBody, tripwireSegment } from "@adatechnology/game-weapons";
|
|
476
477
|
|
|
477
478
|
// src/segment-box.ts
|
|
@@ -514,9 +515,13 @@ function createExplosives(context) {
|
|
|
514
515
|
const projectiles = [];
|
|
515
516
|
const meshes = [];
|
|
516
517
|
const up = new THREE.Vector3(0, 1, 0);
|
|
518
|
+
const drop = (objects) => {
|
|
519
|
+
for (const object of objects) object.removeFromParent();
|
|
520
|
+
releaseObject3D(objects, { keep: scene });
|
|
521
|
+
};
|
|
517
522
|
const clear = () => {
|
|
518
523
|
projectiles.length = 0;
|
|
519
|
-
|
|
524
|
+
drop(meshes);
|
|
520
525
|
meshes.length = 0;
|
|
521
526
|
};
|
|
522
527
|
const addMesh = (geometry, color, metalness, at) => {
|
|
@@ -540,13 +545,12 @@ function createExplosives(context) {
|
|
|
540
545
|
};
|
|
541
546
|
const remove = (projectile) => {
|
|
542
547
|
for (const extra of projectile.attached ?? []) {
|
|
543
|
-
extra.removeFromParent();
|
|
544
548
|
const extraAt = meshes.indexOf(extra);
|
|
545
549
|
if (extraAt >= 0) meshes.splice(extraAt, 1);
|
|
546
550
|
}
|
|
547
|
-
projectile.mesh.removeFromParent();
|
|
548
551
|
const meshAt = meshes.indexOf(projectile.mesh);
|
|
549
552
|
if (meshAt >= 0) meshes.splice(meshAt, 1);
|
|
553
|
+
drop([...projectile.attached ?? [], projectile.mesh]);
|
|
550
554
|
const at = projectiles.indexOf(projectile);
|
|
551
555
|
if (at >= 0) projectiles.splice(at, 1);
|
|
552
556
|
};
|
|
@@ -569,8 +573,8 @@ function createExplosives(context) {
|
|
|
569
573
|
age: 0,
|
|
570
574
|
...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
|
|
571
575
|
});
|
|
572
|
-
const
|
|
573
|
-
action(isRocket ? "launch_projectile" : "throw_projectile", translate(
|
|
576
|
+
const key2 = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
|
|
577
|
+
action(isRocket ? "launch_projectile" : "throw_projectile", translate(key2, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
|
|
574
578
|
data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
|
|
575
579
|
});
|
|
576
580
|
};
|
|
@@ -1229,9 +1233,9 @@ function createLog(options = {}) {
|
|
|
1229
1233
|
};
|
|
1230
1234
|
const log = {
|
|
1231
1235
|
add,
|
|
1232
|
-
coalesce(
|
|
1236
|
+
coalesce(key2, entry, windowS = 1) {
|
|
1233
1237
|
const now = entry.t ?? clock();
|
|
1234
|
-
const k = keyed.get(
|
|
1238
|
+
const k = keyed.get(key2);
|
|
1235
1239
|
if (k && now - k.last <= windowS && list.includes(k.entry)) {
|
|
1236
1240
|
const { t: _t, ...rest } = entry;
|
|
1237
1241
|
k.count++;
|
|
@@ -1242,7 +1246,7 @@ function createLog(options = {}) {
|
|
|
1242
1246
|
return k.entry;
|
|
1243
1247
|
}
|
|
1244
1248
|
const e = add(entry);
|
|
1245
|
-
keyed.set(
|
|
1249
|
+
keyed.set(key2, { entry: e, last: now, count: 1 });
|
|
1246
1250
|
return e;
|
|
1247
1251
|
},
|
|
1248
1252
|
setBeforeRead(fn) {
|
|
@@ -1378,7 +1382,7 @@ function createNarrator(c) {
|
|
|
1378
1382
|
if (p) materialMemo.set(`${objectId}|${partId}`, p.material);
|
|
1379
1383
|
return p;
|
|
1380
1384
|
};
|
|
1381
|
-
const plural = (
|
|
1385
|
+
const plural = (key2, n) => t(`${key2}.${n === 1 ? "one" : "other"}`, { n });
|
|
1382
1386
|
const pieces = (n) => plural("count.piece", n);
|
|
1383
1387
|
const joints = (n) => plural("count.joint", n);
|
|
1384
1388
|
const xz = (x, z) => t("coords", { x: num(x, 1), z: num(z, 1) });
|
|
@@ -1442,18 +1446,18 @@ function createNarrator(c) {
|
|
|
1442
1446
|
if (ev.by === "part_lost") {
|
|
1443
1447
|
const [a, b] = ev.parts;
|
|
1444
1448
|
const lost = isLost(ev.object, a) && !isLost(ev.object, b) ? a : isLost(ev.object, b) && !isLost(ev.object, a) ? b : isLost(ev.object, a) ? a : b;
|
|
1445
|
-
const
|
|
1446
|
-
const g = lostJoints.get(
|
|
1449
|
+
const key2 = `${ev.object}|${lost}`;
|
|
1450
|
+
const g = lostJoints.get(key2) ?? { obj: ev.object, part: lost, joints: 0, groups: 0, merged: false };
|
|
1447
1451
|
g.joints++;
|
|
1448
1452
|
g.groups = ev.groups.length;
|
|
1449
|
-
lostJoints.set(
|
|
1453
|
+
lostJoints.set(key2, g);
|
|
1450
1454
|
} else {
|
|
1451
|
-
const
|
|
1452
|
-
const g = openJoints.get(
|
|
1455
|
+
const key2 = `${ev.object}|${ev.by}`;
|
|
1456
|
+
const g = openJoints.get(key2) ?? { obj: ev.object, by: ev.by, list: [], groups: 0, done: false };
|
|
1453
1457
|
const [a, b] = ev.parts;
|
|
1454
1458
|
g.list.push({ joint: ev.joint, a, b });
|
|
1455
1459
|
g.groups = ev.groups.length;
|
|
1456
|
-
openJoints.set(
|
|
1460
|
+
openJoints.set(key2, g);
|
|
1457
1461
|
}
|
|
1458
1462
|
} else if (ev.type === "ignite") {
|
|
1459
1463
|
const g = fireParts.get(ev.object) ?? { obj: ev.object, parts: [], temp: 0, done: false };
|
|
@@ -1466,14 +1470,14 @@ function createNarrator(c) {
|
|
|
1466
1470
|
outParts.set(ev.object, g);
|
|
1467
1471
|
}
|
|
1468
1472
|
}
|
|
1469
|
-
const lostTail = (
|
|
1470
|
-
const g = lostJoints.get(
|
|
1473
|
+
const lostTail = (key2) => {
|
|
1474
|
+
const g = lostJoints.get(key2);
|
|
1471
1475
|
if (!g) return "";
|
|
1472
1476
|
g.merged = true;
|
|
1473
1477
|
return t("ev.lost.tail", { joints: joints(g.joints), pieces: pieces(g.groups) });
|
|
1474
1478
|
};
|
|
1475
|
-
const lostData = (
|
|
1476
|
-
const g = lostJoints.get(
|
|
1479
|
+
const lostData = (key2) => {
|
|
1480
|
+
const g = lostJoints.get(key2);
|
|
1477
1481
|
return g ? { jointsOpened: g.joints, pieces: g.groups } : {};
|
|
1478
1482
|
};
|
|
1479
1483
|
for (const ev of batch) {
|
|
@@ -1597,8 +1601,8 @@ function createNarrator(c) {
|
|
|
1597
1601
|
if (ev.by === "part_lost") {
|
|
1598
1602
|
const [a2, b2] = ev.parts;
|
|
1599
1603
|
const lost = isLost(ev.object, a2) && !isLost(ev.object, b2) ? a2 : isLost(ev.object, b2) && !isLost(ev.object, a2) ? b2 : isLost(ev.object, a2) ? a2 : b2;
|
|
1600
|
-
const
|
|
1601
|
-
const g2 = lostJoints.get(
|
|
1604
|
+
const key3 = `${ev.object}|${lost}`;
|
|
1605
|
+
const g2 = lostJoints.get(key3);
|
|
1602
1606
|
if (!g2 || g2.merged) break;
|
|
1603
1607
|
const hasOwnLine = batch.some((e) => (e.type === "fracture" || e.type === "burnout") && e.object === ev.object && e.part === lost);
|
|
1604
1608
|
if (hasOwnLine) break;
|
|
@@ -1609,12 +1613,12 @@ function createNarrator(c) {
|
|
|
1609
1613
|
...base(ev, lost),
|
|
1610
1614
|
type: "detach",
|
|
1611
1615
|
text: t("ev.lost.alone", { where: where(ev.object, lost), verb, joints: joints(g2.joints), pieces: pieces(g2.groups) }),
|
|
1612
|
-
data: { parts: [a2, b2], by: ev.by, ...lostData(
|
|
1616
|
+
data: { parts: [a2, b2], by: ev.by, ...lostData(key3) }
|
|
1613
1617
|
});
|
|
1614
1618
|
break;
|
|
1615
1619
|
}
|
|
1616
|
-
const
|
|
1617
|
-
const g = openJoints.get(
|
|
1620
|
+
const key2 = `${ev.object}|${ev.by}`;
|
|
1621
|
+
const g = openJoints.get(key2);
|
|
1618
1622
|
if (!g || g.done) break;
|
|
1619
1623
|
g.done = true;
|
|
1620
1624
|
const [a, b] = ev.parts;
|
|
@@ -1767,13 +1771,13 @@ function createNarrator(c) {
|
|
|
1767
1771
|
};
|
|
1768
1772
|
const kindMemo = /* @__PURE__ */ new Map();
|
|
1769
1773
|
const kindOf = (objectId, jointId) => {
|
|
1770
|
-
const
|
|
1774
|
+
const key2 = `${objectId}|${jointId}`;
|
|
1771
1775
|
const own = c.world.state(objectId)?.joints.find((j) => j.id === jointId)?.kind;
|
|
1772
1776
|
if (own) {
|
|
1773
|
-
kindMemo.set(
|
|
1777
|
+
kindMemo.set(key2, own);
|
|
1774
1778
|
return own;
|
|
1775
1779
|
}
|
|
1776
|
-
const known = kindMemo.get(
|
|
1780
|
+
const known = kindMemo.get(key2);
|
|
1777
1781
|
if (known) return known;
|
|
1778
1782
|
for (const id of c.world.ids()) {
|
|
1779
1783
|
const k = c.world.state(id)?.joints.find((j) => j.id === jointId)?.kind;
|
|
@@ -1809,6 +1813,7 @@ function createNarrator(c) {
|
|
|
1809
1813
|
break;
|
|
1810
1814
|
}
|
|
1811
1815
|
case "drink": {
|
|
1816
|
+
if (ev.eaten) break;
|
|
1812
1817
|
const now = c.now();
|
|
1813
1818
|
let acc = drinkAcc.get(ev.container);
|
|
1814
1819
|
if (!acc || now - acc.last > 0.5) acc = { liters: 0, thirst: 0, burn: false, effects: /* @__PURE__ */ new Set(), last: now, start: now };
|
|
@@ -1842,12 +1847,12 @@ function createNarrator(c) {
|
|
|
1842
1847
|
const surface = t(`surface.${ev.surface}`);
|
|
1843
1848
|
const liquid = c.liquidName(ev.liquid);
|
|
1844
1849
|
stainInfo.set(ev.stain, { liquid: ev.liquid, surface, x: ev.point[0], z: ev.point[2] });
|
|
1845
|
-
const
|
|
1846
|
-
const acc = stainAcc.get(
|
|
1850
|
+
const key2 = `stain:${ev.liquid}:${ev.surface}`;
|
|
1851
|
+
const acc = stainAcc.get(key2);
|
|
1847
1852
|
const n = acc && c.now() - acc.last <= 1 ? acc.n + 1 : 1;
|
|
1848
|
-
stainAcc.set(
|
|
1853
|
+
stainAcc.set(key2, { n, last: c.now() });
|
|
1849
1854
|
c.log.coalesce(
|
|
1850
|
-
|
|
1855
|
+
key2,
|
|
1851
1856
|
{
|
|
1852
1857
|
channel: "event",
|
|
1853
1858
|
type: f + "stain",
|
|
@@ -1860,13 +1865,13 @@ function createNarrator(c) {
|
|
|
1860
1865
|
}
|
|
1861
1866
|
case "dried": {
|
|
1862
1867
|
const info = stainInfo.get(ev.stain);
|
|
1863
|
-
const
|
|
1864
|
-
const acc = driedAcc.get(
|
|
1868
|
+
const key2 = `dried:${info?.liquid ?? "?"}:${ev.mark}`;
|
|
1869
|
+
const acc = driedAcc.get(key2);
|
|
1865
1870
|
const n = acc && c.now() - acc.last <= 1.5 ? acc.n + 1 : 1;
|
|
1866
|
-
driedAcc.set(
|
|
1871
|
+
driedAcc.set(key2, { n, last: c.now() });
|
|
1867
1872
|
const vars = { liquid: info ? c.liquidName(info.liquid) : "", surface: info?.surface ?? "", where: info ? xz(info.x, info.z) : "", n };
|
|
1868
1873
|
const text = !info ? t(ev.mark ? "fl.dried.mark" : "fl.dried") : n > 1 ? t(ev.mark ? "fl.dried.many.mark" : "fl.dried.many", vars) : t(ev.mark ? "fl.dried.known.mark" : "fl.dried.known", vars);
|
|
1869
|
-
c.log.coalesce(
|
|
1874
|
+
c.log.coalesce(key2, { channel: "event", type: f + "dried", text, data: { stain: ev.stain, mark: ev.mark, count: n } }, 1.5);
|
|
1870
1875
|
break;
|
|
1871
1876
|
}
|
|
1872
1877
|
case "mix":
|
|
@@ -1979,6 +1984,7 @@ function createPlayer(THREE, o) {
|
|
|
1979
1984
|
let useStarted = false;
|
|
1980
1985
|
let holdTimer = null;
|
|
1981
1986
|
let walkHeld = false;
|
|
1987
|
+
let seated = false;
|
|
1982
1988
|
let lookLimit = null;
|
|
1983
1989
|
const look = (dx, dy) => {
|
|
1984
1990
|
yaw -= dx * LOOK;
|
|
@@ -2119,6 +2125,11 @@ function createPlayer(THREE, o) {
|
|
|
2119
2125
|
setWalkHeld(held) {
|
|
2120
2126
|
walkHeld = held;
|
|
2121
2127
|
},
|
|
2128
|
+
setSeated(value) {
|
|
2129
|
+
seated = value;
|
|
2130
|
+
y = 0;
|
|
2131
|
+
vy = 0;
|
|
2132
|
+
},
|
|
2122
2133
|
setBend(metres) {
|
|
2123
2134
|
bendTarget = metres;
|
|
2124
2135
|
},
|
|
@@ -2171,6 +2182,11 @@ function createPlayer(THREE, o) {
|
|
|
2171
2182
|
if ((walkX || walkZ) && !walkHeld) move(walkX * speed * dt, walkZ * speed * dt);
|
|
2172
2183
|
walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
|
|
2173
2184
|
running = run && walked > 0;
|
|
2185
|
+
if (seated) {
|
|
2186
|
+
followFloor(dt);
|
|
2187
|
+
apply();
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2174
2190
|
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2175
2191
|
if (y < floor && vy <= 0) y = floor;
|
|
2176
2192
|
if (y <= floor && keys.has("Space") && !walkHeld) vy = JUMP;
|
|
@@ -2318,7 +2334,7 @@ function createRoomPush(options) {
|
|
|
2318
2334
|
}
|
|
2319
2335
|
control.release();
|
|
2320
2336
|
player.setWalkHeld(false);
|
|
2321
|
-
player.setBend(0);
|
|
2337
|
+
if (!options.hasBody?.()) player.setBend(0);
|
|
2322
2338
|
player.setLookLimit(null);
|
|
2323
2339
|
options.ring(null);
|
|
2324
2340
|
event("release", lean);
|
|
@@ -2380,7 +2396,7 @@ function createRoomPush(options) {
|
|
|
2380
2396
|
control.begin(material === void 0 ? { massKg } : { massKg, material });
|
|
2381
2397
|
dragged = 0;
|
|
2382
2398
|
player.setWalkHeld(true);
|
|
2383
|
-
if (isLow) player.setBend(Math.min(MAX_BEND, Math.max(0, STANDING_EYE - (max[1] - player.feet + LOW_EYE_OVER_TOP))));
|
|
2399
|
+
if (isLow && !options.hasBody?.()) player.setBend(Math.min(MAX_BEND, Math.max(0, STANDING_EYE - (max[1] - player.feet + LOW_EYE_OVER_TOP))));
|
|
2384
2400
|
player.setLookLimit({ yaw: Math.atan2(-into[0], -into[1]), range: LOOK_RANGE });
|
|
2385
2401
|
event("grip", leaning);
|
|
2386
2402
|
return true;
|
|
@@ -2440,7 +2456,503 @@ function createRoomPush(options) {
|
|
|
2440
2456
|
};
|
|
2441
2457
|
}
|
|
2442
2458
|
|
|
2459
|
+
// src/seating.ts
|
|
2460
|
+
var SIT_EYE = 0.62;
|
|
2461
|
+
var LIE_EYE = 0.18;
|
|
2462
|
+
var STANDING_EYE2 = 1.7;
|
|
2463
|
+
var LOOK_RANGE2 = 1.6;
|
|
2464
|
+
function createSeating(o) {
|
|
2465
|
+
const { THREE, player } = o;
|
|
2466
|
+
const v = new THREE.Vector3();
|
|
2467
|
+
const q = new THREE.Quaternion();
|
|
2468
|
+
const e = new THREE.Euler();
|
|
2469
|
+
let current = null;
|
|
2470
|
+
let facing = 0;
|
|
2471
|
+
const toWorld = (root, local) => {
|
|
2472
|
+
root.updateWorldMatrix(true, false);
|
|
2473
|
+
v.set(local[0], local[1], local[2]).applyMatrix4(root.matrixWorld);
|
|
2474
|
+
return [v.x, v.y, v.z];
|
|
2475
|
+
};
|
|
2476
|
+
const yawOf = (root) => {
|
|
2477
|
+
root.getWorldQuaternion(q);
|
|
2478
|
+
return e.setFromQuaternion(q, "YXZ").y;
|
|
2479
|
+
};
|
|
2480
|
+
const nearest = (holder) => {
|
|
2481
|
+
let best;
|
|
2482
|
+
let bestDistance = Infinity;
|
|
2483
|
+
for (const seat of holder.seats) {
|
|
2484
|
+
const [x, , z] = toWorld(holder.root, seat.pos);
|
|
2485
|
+
const distance = Math.hypot(x - player.x, z - player.z);
|
|
2486
|
+
if (distance < bestDistance) {
|
|
2487
|
+
best = seat;
|
|
2488
|
+
bestDistance = distance;
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
return best;
|
|
2492
|
+
};
|
|
2493
|
+
const hold = () => {
|
|
2494
|
+
player.setSeated(true);
|
|
2495
|
+
player.setWalkHeld(true);
|
|
2496
|
+
player.setLookLimit({ yaw: facing, range: LOOK_RANGE2 });
|
|
2497
|
+
};
|
|
2498
|
+
const release = () => {
|
|
2499
|
+
player.setSeated(false);
|
|
2500
|
+
player.setWalkHeld(false);
|
|
2501
|
+
player.setLookLimit(null);
|
|
2502
|
+
current = null;
|
|
2503
|
+
};
|
|
2504
|
+
const seating = {
|
|
2505
|
+
sit(objectId, seatId) {
|
|
2506
|
+
const holder = objectId ? o.find(objectId) : o.pick();
|
|
2507
|
+
if (!holder) return null;
|
|
2508
|
+
const seat = seatId ? holder.seats.find((s) => s.id === seatId) : nearest(holder);
|
|
2509
|
+
if (!seat) return null;
|
|
2510
|
+
const [x, y, z] = toWorld(holder.root, seat.pos);
|
|
2511
|
+
const eye = o.hasBody?.() ? STANDING_EYE2 : seat.eye ?? (seat.pose === "lie" ? LIE_EYE : SIT_EYE);
|
|
2512
|
+
facing = yawOf(holder.root) + seat.ry + Math.PI;
|
|
2513
|
+
player.setPose([x, y + eye, z], facing, 0);
|
|
2514
|
+
current = { objectId: holder.objectId, seatId: seat.id, pose: seat.pose, hips: [x, y, z] };
|
|
2515
|
+
hold();
|
|
2516
|
+
o.onSit?.(current);
|
|
2517
|
+
return current;
|
|
2518
|
+
},
|
|
2519
|
+
stand() {
|
|
2520
|
+
if (!current) return false;
|
|
2521
|
+
const holder = o.find(current.objectId);
|
|
2522
|
+
const seat = holder?.seats.find((s) => s.id === current?.seatId);
|
|
2523
|
+
if (holder && seat) {
|
|
2524
|
+
const [x, , z] = toWorld(holder.root, seat.exit);
|
|
2525
|
+
if (!player.canStand(x, z)) return false;
|
|
2526
|
+
player.setPose([x, o.groundAt(x, z) + STANDING_EYE2, z], player.yaw, 0);
|
|
2527
|
+
} else player.setPose([player.x, o.groundAt(player.x, player.z) + STANDING_EYE2, player.z], player.yaw, 0);
|
|
2528
|
+
const left = current;
|
|
2529
|
+
release();
|
|
2530
|
+
o.onStand?.(left);
|
|
2531
|
+
return true;
|
|
2532
|
+
},
|
|
2533
|
+
update() {
|
|
2534
|
+
if (!current) return;
|
|
2535
|
+
if (!o.find(current.objectId)) {
|
|
2536
|
+
seating.stand();
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
const [walkX, walkZ] = player.walkIntent;
|
|
2540
|
+
if (walkX || walkZ || player.keys.has("Space")) {
|
|
2541
|
+
seating.stand();
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
hold();
|
|
2545
|
+
},
|
|
2546
|
+
release,
|
|
2547
|
+
get seat() {
|
|
2548
|
+
return current;
|
|
2549
|
+
}
|
|
2550
|
+
};
|
|
2551
|
+
return seating;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// src/door-open.ts
|
|
2555
|
+
import { createDoorUse, DOOR_USE_DEFAULTS } from "@adatechnology/game-player";
|
|
2556
|
+
var DOOR_REACH = 2.2;
|
|
2557
|
+
var NEAR_LEAF = 0.25;
|
|
2558
|
+
var NEAR_OPEN_LEAF = 0.9;
|
|
2559
|
+
var BODY_HEIGHT = 1.8;
|
|
2560
|
+
var HAND_REST = [0.22, -0.42, -0.32];
|
|
2561
|
+
var SKIN = 0.01;
|
|
2562
|
+
var HANDLE_INSET = 0.06;
|
|
2563
|
+
var STANDOFF2 = 0.55;
|
|
2564
|
+
var DRAWER_TRAVEL = 0.3;
|
|
2565
|
+
var CLEAR_OF_ARC = 0.3;
|
|
2566
|
+
var OUT_IN_FRONT = 0.42;
|
|
2567
|
+
var ARRIVED = 0.05;
|
|
2568
|
+
var TURN_RATE = 8;
|
|
2569
|
+
var MAX_WALK_SECONDS = 1.5;
|
|
2570
|
+
var APPROACH_SPEED2 = 3;
|
|
2571
|
+
var FLOOR_GAP = 0.03;
|
|
2572
|
+
var key = (objectId, part) => `${objectId}\0${part}`;
|
|
2573
|
+
var slides = (clip, part) => {
|
|
2574
|
+
const last = clip.keys[clip.keys.length - 1]?.parts?.[part];
|
|
2575
|
+
const r = last?.rotation ?? [0, 0, 0];
|
|
2576
|
+
return Math.abs(r[0]) + Math.abs(r[1]) + Math.abs(r[2]) < 1e-3;
|
|
2577
|
+
};
|
|
2578
|
+
function createRoomDoors(options) {
|
|
2579
|
+
const { THREE, player, world, camera, arena } = options;
|
|
2580
|
+
const control = createDoorUse(options.rules);
|
|
2581
|
+
const openings = /* @__PURE__ */ new Map();
|
|
2582
|
+
const box2 = new THREE.Box3();
|
|
2583
|
+
const before = new THREE.Box3();
|
|
2584
|
+
const tmp = new THREE.Vector3();
|
|
2585
|
+
const centre = new THREE.Vector3();
|
|
2586
|
+
let using = null;
|
|
2587
|
+
let reaching = null;
|
|
2588
|
+
const rules = { ...DOOR_USE_DEFAULTS, ...options.rules };
|
|
2589
|
+
const holding = () => {
|
|
2590
|
+
if (reaching) {
|
|
2591
|
+
const h = reaching.reach.handle();
|
|
2592
|
+
return { phase: reaching.phase, progress: reaching.progress, at: new THREE.Vector3(h[0], h[1], h[2]), pull: reaching.reach.pull ?? true };
|
|
2593
|
+
}
|
|
2594
|
+
if (!using || control.phase === "idle") return null;
|
|
2595
|
+
using.obj.updateMatrixWorld(true);
|
|
2596
|
+
return { phase: control.phase, progress: using.progress, at: using.obj.localToWorld(new THREE.Vector3().copy(using.grip)), pull: using.pull };
|
|
2597
|
+
};
|
|
2598
|
+
const standFor = (handle, hinge, slide) => {
|
|
2599
|
+
if (!hinge || slide) {
|
|
2600
|
+
const dx = handle.x - player.x;
|
|
2601
|
+
const dz = handle.z - player.z;
|
|
2602
|
+
const d = Math.hypot(dx, dz) || 1;
|
|
2603
|
+
const off = slide ? STANDOFF2 + DRAWER_TRAVEL : STANDOFF2;
|
|
2604
|
+
return [handle.x - dx / d * off, handle.z - dz / d * off];
|
|
2605
|
+
}
|
|
2606
|
+
let ax = handle.x - hinge.x;
|
|
2607
|
+
let az = handle.z - hinge.z;
|
|
2608
|
+
const width = Math.hypot(ax, az);
|
|
2609
|
+
if (width < 0.05) return standFor(handle, null, false);
|
|
2610
|
+
ax /= width;
|
|
2611
|
+
az /= width;
|
|
2612
|
+
let nx = -az;
|
|
2613
|
+
let nz = ax;
|
|
2614
|
+
if ((player.x - handle.x) * nx + (player.z - handle.z) * nz < 0) {
|
|
2615
|
+
nx = -nx;
|
|
2616
|
+
nz = -nz;
|
|
2617
|
+
}
|
|
2618
|
+
const x = hinge.x + ax * (width + CLEAR_OF_ARC) + nx * OUT_IN_FRONT;
|
|
2619
|
+
const z = hinge.z + az * (width + CLEAR_OF_ARC) + nz * OUT_IN_FRONT;
|
|
2620
|
+
return player.canStand(x, z) ? [x, z] : standFor(handle, null, false);
|
|
2621
|
+
};
|
|
2622
|
+
const face = (handle, dt) => {
|
|
2623
|
+
camera.updateMatrixWorld(true);
|
|
2624
|
+
const eye = camera.getWorldPosition(new THREE.Vector3());
|
|
2625
|
+
const dx = handle.x - player.x;
|
|
2626
|
+
const dz = handle.z - player.z;
|
|
2627
|
+
const flat = Math.hypot(dx, dz);
|
|
2628
|
+
if (flat < 0.05) return;
|
|
2629
|
+
const yaw = Math.atan2(-dx, -dz);
|
|
2630
|
+
const pitch = Math.atan2(handle.y - eye.y, flat);
|
|
2631
|
+
let dYaw = yaw - player.yaw;
|
|
2632
|
+
dYaw = Math.atan2(Math.sin(dYaw), Math.cos(dYaw));
|
|
2633
|
+
const k = Math.min(1, dt * TURN_RATE);
|
|
2634
|
+
player.setPose([player.x, eye.y, player.z], player.yaw + dYaw * k, player.pitch + (pitch - player.pitch) * k);
|
|
2635
|
+
};
|
|
2636
|
+
const approach = (handle, hinge, slide, dt, walked) => {
|
|
2637
|
+
const [x, z] = standFor(handle, hinge, slide);
|
|
2638
|
+
const dx = x - player.x;
|
|
2639
|
+
const dz = z - player.z;
|
|
2640
|
+
const d = Math.hypot(dx, dz);
|
|
2641
|
+
if (d < ARRIVED || walked > MAX_WALK_SECONDS) return true;
|
|
2642
|
+
const go = Math.min(d, APPROACH_SPEED2 * dt);
|
|
2643
|
+
if (go < 1e-4) return false;
|
|
2644
|
+
const x0 = player.x;
|
|
2645
|
+
const z0 = player.z;
|
|
2646
|
+
player.moveBy(dx / d * go, dz / d * go);
|
|
2647
|
+
return Math.hypot(player.x - x0, player.z - z0) < go * 0.2;
|
|
2648
|
+
};
|
|
2649
|
+
const openingOf = (objectId, part) => openings.get(key(objectId, part)) ?? 0;
|
|
2650
|
+
const lastGoals = /* @__PURE__ */ new Map();
|
|
2651
|
+
const leafAt = (objectId, part, point) => {
|
|
2652
|
+
const leaves = options.leaves(objectId);
|
|
2653
|
+
if (leaves.length === 0) return null;
|
|
2654
|
+
const exact = leaves.find((l) => l.part === part);
|
|
2655
|
+
if (exact) return exact;
|
|
2656
|
+
let best = null;
|
|
2657
|
+
let bestD = NEAR_LEAF;
|
|
2658
|
+
let open = null;
|
|
2659
|
+
let openD = NEAR_OPEN_LEAF;
|
|
2660
|
+
for (const l of leaves) {
|
|
2661
|
+
const obj = options.objectOf(objectId, l.part);
|
|
2662
|
+
if (!obj) continue;
|
|
2663
|
+
box2.setFromObject(obj);
|
|
2664
|
+
const d = box2.distanceToPoint(tmp.set(point[0], point[1], point[2]));
|
|
2665
|
+
if (d < bestD) {
|
|
2666
|
+
bestD = d;
|
|
2667
|
+
best = l;
|
|
2668
|
+
}
|
|
2669
|
+
if (openingOf(objectId, l.part) > 0.5 && d < openD) {
|
|
2670
|
+
openD = d;
|
|
2671
|
+
open = l;
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
return best ?? open;
|
|
2675
|
+
};
|
|
2676
|
+
const picked = () => {
|
|
2677
|
+
for (const hit of options.pick()) {
|
|
2678
|
+
const leaf = leafAt(hit.objectId, hit.part, hit.point);
|
|
2679
|
+
if (leaf) return { hit, leaf };
|
|
2680
|
+
}
|
|
2681
|
+
return null;
|
|
2682
|
+
};
|
|
2683
|
+
const massOf = (objectId, part) => world.part(objectId, part)?.mass ?? 10;
|
|
2684
|
+
const event = (type, u, by) => options.onEvent({ type, objectId: u.objectId, part: u.leaf.part, name: options.nameOf(u.objectId), opening: openingOf(u.objectId, u.leaf.part), ...by ? { by } : {} });
|
|
2685
|
+
const overlaps2d = (b, c) => b.max.x > c[0] && b.min.x < c[2] && b.max.z > c[1] && b.min.z < c[3];
|
|
2686
|
+
const bodyOverlap = (b) => {
|
|
2687
|
+
if (b.min.y > player.feet + BODY_HEIGHT || b.max.y < player.feet + 0.05) return null;
|
|
2688
|
+
const cx = Math.max(b.min.x, Math.min(b.max.x, player.x));
|
|
2689
|
+
const cz = Math.max(b.min.z, Math.min(b.max.z, player.z));
|
|
2690
|
+
const dx = player.x - cx;
|
|
2691
|
+
const dz = player.z - cz;
|
|
2692
|
+
const d = Math.hypot(dx, dz);
|
|
2693
|
+
if (d >= PLAYER_RADIUS) return null;
|
|
2694
|
+
if (d < 1e-4) {
|
|
2695
|
+
const outs = [
|
|
2696
|
+
[b.max.x - player.x, 1, 0],
|
|
2697
|
+
[player.x - b.min.x, -1, 0],
|
|
2698
|
+
[b.max.z - player.z, 0, 1],
|
|
2699
|
+
[player.z - b.min.z, 0, -1]
|
|
2700
|
+
];
|
|
2701
|
+
outs.sort((a, c) => a[0] - c[0]);
|
|
2702
|
+
const o = outs[0];
|
|
2703
|
+
return [o[1] * (o[0] + PLAYER_RADIUS), o[2] * (o[0] + PLAYER_RADIUS)];
|
|
2704
|
+
}
|
|
2705
|
+
const push = PLAYER_RADIUS - d + SKIN;
|
|
2706
|
+
return [dx / d * push, dz / d * push];
|
|
2707
|
+
};
|
|
2708
|
+
const isFree = (u, from, to) => {
|
|
2709
|
+
const { objectId, leaf, obj } = u;
|
|
2710
|
+
obj.updateMatrixWorld(true);
|
|
2711
|
+
before.setFromObject(obj);
|
|
2712
|
+
options.pose(objectId, leaf.part, leaf.clip, to);
|
|
2713
|
+
obj.updateMatrixWorld(true);
|
|
2714
|
+
box2.setFromObject(obj);
|
|
2715
|
+
box2.expandByScalar(-SKIN);
|
|
2716
|
+
let by = null;
|
|
2717
|
+
if (Math.max(Math.abs(box2.min.x), Math.abs(box2.max.x), Math.abs(box2.min.z), Math.abs(box2.max.z)) > arena.limit) by = "wall";
|
|
2718
|
+
else
|
|
2719
|
+
for (let i = 0; i < arena.colliders.length; i++) {
|
|
2720
|
+
const c = arena.colliders[i];
|
|
2721
|
+
if (arena.ids[i] === objectId) continue;
|
|
2722
|
+
if (overlaps2d(box2, c) && !overlaps2d(before, c)) {
|
|
2723
|
+
by = arena.ids[i] ?? "wall";
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
if (!by) {
|
|
2728
|
+
before.getCenter(centre);
|
|
2729
|
+
box2.getCenter(tmp);
|
|
2730
|
+
const dir = [tmp.x - centre.x, tmp.y - centre.y, tmp.z - centre.z];
|
|
2731
|
+
const len = Math.hypot(dir[0], dir[1], dir[2]);
|
|
2732
|
+
if (len > 1e-5) {
|
|
2733
|
+
const hit = world.sweepBox({ min: [before.min.x + SKIN, before.min.y + FLOOR_GAP, before.min.z + SKIN], max: [before.max.x - SKIN, before.max.y - SKIN, before.max.z - SKIN] }, dir, len * 2, { ignore: objectId, spacing: 0.12 });
|
|
2734
|
+
if (hit && hit.normal[1] < 0.7) by = hit.object;
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
if (!by) {
|
|
2738
|
+
const out = bodyOverlap(box2);
|
|
2739
|
+
if (out) {
|
|
2740
|
+
const x0 = player.x;
|
|
2741
|
+
const z0 = player.z;
|
|
2742
|
+
player.moveBy(out[0], out[1]);
|
|
2743
|
+
if (Math.hypot(player.x - x0, player.z - z0) < Math.hypot(out[0], out[1]) * 0.5) by = "player";
|
|
2744
|
+
else if (u.phase !== "swing" || from === 0) event("stepped_back", u);
|
|
2745
|
+
}
|
|
2746
|
+
}
|
|
2747
|
+
if (by) {
|
|
2748
|
+
options.pose(objectId, leaf.part, leaf.clip, from);
|
|
2749
|
+
const at = [box2.getCenter(tmp).x, tmp.y, tmp.z];
|
|
2750
|
+
options.sound(options.impactKey(objectId, leaf.part), at, Math.min(0.6, 0.15 + massOf(objectId, leaf.part) * 0.02));
|
|
2751
|
+
event("blocked", u, by);
|
|
2752
|
+
return false;
|
|
2753
|
+
}
|
|
2754
|
+
return true;
|
|
2755
|
+
};
|
|
2756
|
+
const handleOf = (obj, leaf, point) => {
|
|
2757
|
+
obj.updateMatrixWorld(true);
|
|
2758
|
+
box2.setFromObject(obj);
|
|
2759
|
+
const lo = new THREE.Vector3(Infinity, Infinity, Infinity);
|
|
2760
|
+
const hi = new THREE.Vector3(-Infinity, -Infinity, -Infinity);
|
|
2761
|
+
for (let i = 0; i < 8; i++) {
|
|
2762
|
+
tmp.set(i & 1 ? box2.max.x : box2.min.x, i & 2 ? box2.max.y : box2.min.y, i & 4 ? box2.max.z : box2.min.z);
|
|
2763
|
+
obj.worldToLocal(tmp);
|
|
2764
|
+
lo.min(tmp);
|
|
2765
|
+
hi.max(tmp);
|
|
2766
|
+
}
|
|
2767
|
+
const aimed = point ? obj.worldToLocal(new THREE.Vector3(point[0], point[1], point[2])) : lo.clone().add(hi).multiplyScalar(0.5);
|
|
2768
|
+
if (!point) {
|
|
2769
|
+
const local = obj.worldToLocal(obj.getWorldPosition(new THREE.Vector3()).setY(player.feet + 1));
|
|
2770
|
+
aimed.y = local.y;
|
|
2771
|
+
}
|
|
2772
|
+
aimed.clamp(lo, hi);
|
|
2773
|
+
if (slides(leaf.clip, leaf.part)) return aimed;
|
|
2774
|
+
const last = leaf.clip.keys[leaf.clip.keys.length - 1]?.parts?.[leaf.part]?.rotation ?? [0, 1, 0];
|
|
2775
|
+
const axis = Math.abs(last[0]) >= Math.abs(last[1]) && Math.abs(last[0]) >= Math.abs(last[2]) ? 0 : Math.abs(last[1]) >= Math.abs(last[2]) ? 1 : 2;
|
|
2776
|
+
const size = [hi.x - lo.x, hi.y - lo.y, hi.z - lo.z];
|
|
2777
|
+
const reachOf = (k) => Math.max(Math.abs(lo.getComponent(k)), Math.abs(hi.getComponent(k)));
|
|
2778
|
+
let across = -1;
|
|
2779
|
+
for (let k = 0; k < 3; k++) {
|
|
2780
|
+
if (k === axis || size[k] < Math.min(size[0], size[1], size[2]) + 1e-6) continue;
|
|
2781
|
+
if (across < 0 || reachOf(k) > reachOf(across)) across = k;
|
|
2782
|
+
}
|
|
2783
|
+
if (across < 0) return aimed;
|
|
2784
|
+
const far = Math.abs(hi.getComponent(across)) >= Math.abs(lo.getComponent(across)) ? hi.getComponent(across) : lo.getComponent(across);
|
|
2785
|
+
aimed.setComponent(across, far - Math.sign(far) * Math.min(HANDLE_INSET, size[across] * 0.25));
|
|
2786
|
+
return aimed;
|
|
2787
|
+
};
|
|
2788
|
+
const start = (objectId, leaf, point) => {
|
|
2789
|
+
reaching = null;
|
|
2790
|
+
const obj = options.objectOf(objectId, leaf.part);
|
|
2791
|
+
if (!obj) return false;
|
|
2792
|
+
if (using && (using.objectId !== objectId || using.leaf.part !== leaf.part)) control.cancel();
|
|
2793
|
+
obj.updateMatrixWorld(true);
|
|
2794
|
+
const grip = handleOf(obj, leaf, point);
|
|
2795
|
+
const opening = openingOf(objectId, leaf.part);
|
|
2796
|
+
const key2 = `${objectId}|${leaf.part}`;
|
|
2797
|
+
const lastGoal = lastGoals.get(key2);
|
|
2798
|
+
const goal = control.begin({ opening, massKg: massOf(objectId, leaf.part), locked: options.isLocked(objectId, leaf.part), ...lastGoal !== void 0 ? { lastGoal } : {} });
|
|
2799
|
+
if (goal !== null) lastGoals.set(key2, goal);
|
|
2800
|
+
let pull = true;
|
|
2801
|
+
if (goal !== null) {
|
|
2802
|
+
obj.updateMatrixWorld(true);
|
|
2803
|
+
options.pose(objectId, leaf.part, leaf.clip, 1);
|
|
2804
|
+
obj.updateMatrixWorld(true);
|
|
2805
|
+
const openAt = obj.localToWorld(grip.clone());
|
|
2806
|
+
options.pose(objectId, leaf.part, leaf.clip, 0);
|
|
2807
|
+
obj.updateMatrixWorld(true);
|
|
2808
|
+
const shutAt = obj.localToWorld(grip.clone());
|
|
2809
|
+
options.pose(objectId, leaf.part, leaf.clip, opening);
|
|
2810
|
+
const dOpen = Math.hypot(openAt.x - player.x, openAt.z - player.z);
|
|
2811
|
+
const dShut = Math.hypot(shutAt.x - player.x, shutAt.z - player.z);
|
|
2812
|
+
pull = goal === 1 ? dOpen <= dShut : dShut <= dOpen;
|
|
2813
|
+
}
|
|
2814
|
+
using = { objectId, leaf, obj, grip, pull, phase: control.phase, progress: 0, walked: 0, there: goal === null };
|
|
2815
|
+
const at = obj.localToWorld(grip.clone());
|
|
2816
|
+
if (goal === null) {
|
|
2817
|
+
options.sound("leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
2818
|
+
event("locked", using);
|
|
2819
|
+
} else event("reach", using);
|
|
2820
|
+
return true;
|
|
2821
|
+
};
|
|
2822
|
+
return {
|
|
2823
|
+
get active() {
|
|
2824
|
+
return reaching !== null || using !== null && control.phase !== "idle";
|
|
2825
|
+
},
|
|
2826
|
+
get state() {
|
|
2827
|
+
return using ? { objectId: using.objectId, part: using.leaf.part, phase: control.phase, opening: openingOf(using.objectId, using.leaf.part), goal: control.goal } : null;
|
|
2828
|
+
},
|
|
2829
|
+
get avatarState() {
|
|
2830
|
+
const h = holding();
|
|
2831
|
+
return h ? { phase: h.phase, progress: h.progress, height: h.at.y - player.feet, pull: h.pull } : null;
|
|
2832
|
+
},
|
|
2833
|
+
aimed() {
|
|
2834
|
+
const found = picked();
|
|
2835
|
+
if (!found) return null;
|
|
2836
|
+
const { hit, leaf } = found;
|
|
2837
|
+
return { objectId: hit.objectId, part: leaf.part, open: openingOf(hit.objectId, leaf.part) > 0.5, locked: options.isLocked(hit.objectId, leaf.part) };
|
|
2838
|
+
},
|
|
2839
|
+
use() {
|
|
2840
|
+
const found = picked();
|
|
2841
|
+
return found ? start(found.hit.objectId, found.leaf, found.hit.point) : false;
|
|
2842
|
+
},
|
|
2843
|
+
useLeaf(objectId, part) {
|
|
2844
|
+
const leaves = options.leaves(objectId);
|
|
2845
|
+
const leaf = part === void 0 ? leaves[0] : leaves.find((l) => l.part === part);
|
|
2846
|
+
return leaf ? start(objectId, leaf, null) : false;
|
|
2847
|
+
},
|
|
2848
|
+
opening: openingOf,
|
|
2849
|
+
step(dt) {
|
|
2850
|
+
const r = reaching;
|
|
2851
|
+
if (r) {
|
|
2852
|
+
r.time += dt;
|
|
2853
|
+
if (r.phase === "reach" && !r.there) {
|
|
2854
|
+
const h = r.reach.handle();
|
|
2855
|
+
const hinge = r.reach.hinge?.();
|
|
2856
|
+
r.walked += dt;
|
|
2857
|
+
const g = new THREE.Vector3(h[0], h[1], h[2]);
|
|
2858
|
+
r.there = approach(g, hinge ? new THREE.Vector3(hinge[0], hinge[1], hinge[2]) : null, false, dt, r.walked);
|
|
2859
|
+
face(g, dt);
|
|
2860
|
+
if (r.there) r.time = 0;
|
|
2861
|
+
} else if (r.phase === "reach") {
|
|
2862
|
+
r.progress = Math.min(1, r.time / rules.reachSeconds);
|
|
2863
|
+
if (r.progress >= 1) {
|
|
2864
|
+
r.reach.onGrip?.();
|
|
2865
|
+
r.phase = r.reach.locked ? "rattle" : "swing";
|
|
2866
|
+
r.time = 0;
|
|
2867
|
+
r.progress = 0;
|
|
2868
|
+
}
|
|
2869
|
+
} else if (r.phase === "swing" || r.phase === "rattle") {
|
|
2870
|
+
r.progress = Math.min(1, r.time / (r.phase === "rattle" ? rules.rattleSeconds : Math.max(0.05, r.reach.seconds)));
|
|
2871
|
+
if (r.progress >= 1) {
|
|
2872
|
+
r.phase = "release";
|
|
2873
|
+
r.time = 0;
|
|
2874
|
+
r.progress = 0;
|
|
2875
|
+
}
|
|
2876
|
+
} else {
|
|
2877
|
+
r.progress = Math.min(1, r.time / rules.releaseSeconds);
|
|
2878
|
+
if (r.progress >= 1) reaching = null;
|
|
2879
|
+
}
|
|
2880
|
+
return;
|
|
2881
|
+
}
|
|
2882
|
+
const u = using;
|
|
2883
|
+
if (!u || control.phase === "idle") return;
|
|
2884
|
+
const was = openingOf(u.objectId, u.leaf.part);
|
|
2885
|
+
const wasPhase = control.phase;
|
|
2886
|
+
if (wasPhase === "reach" && !u.there) {
|
|
2887
|
+
u.obj.updateMatrixWorld(true);
|
|
2888
|
+
u.walked += dt;
|
|
2889
|
+
const g = u.obj.localToWorld(new THREE.Vector3().copy(u.grip));
|
|
2890
|
+
u.there = approach(g, u.obj.getWorldPosition(new THREE.Vector3()), slides(u.leaf.clip, u.leaf.part), dt, u.walked);
|
|
2891
|
+
face(g, dt);
|
|
2892
|
+
if (!u.there) return;
|
|
2893
|
+
}
|
|
2894
|
+
const s = control.step(dt, (from, to) => isFree(u, from, to));
|
|
2895
|
+
u.phase = s.phase;
|
|
2896
|
+
u.progress = s.progress;
|
|
2897
|
+
openings.set(key(u.objectId, u.leaf.part), s.opening);
|
|
2898
|
+
u.obj.updateMatrixWorld(true);
|
|
2899
|
+
const at = u.obj.localToWorld(tmp.copy(u.grip));
|
|
2900
|
+
const point = [at.x, at.y, at.z];
|
|
2901
|
+
const slide = slides(u.leaf.clip, u.leaf.part);
|
|
2902
|
+
const shut = options.impactKey(u.objectId, u.leaf.part).replace("impact_", "leaf_shut_");
|
|
2903
|
+
if (wasPhase === "reach" && s.phase !== "reach") {
|
|
2904
|
+
if (slide) options.sound("leaf_slide", point, 0.5);
|
|
2905
|
+
else {
|
|
2906
|
+
if (was < 0.02) options.sound("leaf_latch", point, 0.5);
|
|
2907
|
+
options.sound(/metal|glass/.test(shut) ? "leaf_creak_metal" : "leaf_creak_wood", point, 0.35);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
if (s.arrived) {
|
|
2911
|
+
if (s.opening <= 0) {
|
|
2912
|
+
options.sound(slide ? "leaf_slide" : shut, point, Math.min(1, 0.3 + massOf(u.objectId, u.leaf.part) * 0.03));
|
|
2913
|
+
event("closed", u);
|
|
2914
|
+
} else event("opened", u);
|
|
2915
|
+
}
|
|
2916
|
+
if (s.phase === "idle") using = null;
|
|
2917
|
+
},
|
|
2918
|
+
handTargets() {
|
|
2919
|
+
const h = holding();
|
|
2920
|
+
if (!h || h.phase === "rattle" && h.progress > 0.92) return null;
|
|
2921
|
+
const { at, phase, progress } = h;
|
|
2922
|
+
if (phase === "rattle") at.y += Math.sin(progress * 60) * 0.012;
|
|
2923
|
+
camera.updateMatrixWorld(true);
|
|
2924
|
+
const rest = camera.localToWorld(new THREE.Vector3(HAND_REST[0], HAND_REST[1], HAND_REST[2]));
|
|
2925
|
+
const k = phase === "reach" ? progress : phase === "release" ? 1 - progress : 1;
|
|
2926
|
+
const ease = k * k * (3 - 2 * k);
|
|
2927
|
+
const pos = rest.lerp(at, ease);
|
|
2928
|
+
const way = new THREE.Vector3(at.x - player.x, 0, at.z - player.z);
|
|
2929
|
+
if (way.lengthSq() < 1e-6) way.set(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
|
|
2930
|
+
way.normalize();
|
|
2931
|
+
const down = new THREE.Vector3(0, -1, 0);
|
|
2932
|
+
const back = new THREE.Vector3().crossVectors(down, way);
|
|
2933
|
+
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(way, back, down));
|
|
2934
|
+
const grip = phase === "reach" ? 0.15 + 0.75 * progress : phase === "release" ? 0.9 * (1 - progress) : 0.9;
|
|
2935
|
+
return { R: { position: [pos.x, pos.y, pos.z], quaternion: [q.x, q.y, q.z, q.w] }, L: null, grip };
|
|
2936
|
+
},
|
|
2937
|
+
reachFor(reach) {
|
|
2938
|
+
if (using) {
|
|
2939
|
+
control.cancel();
|
|
2940
|
+
using = null;
|
|
2941
|
+
}
|
|
2942
|
+
reaching = { reach, phase: "reach", time: 0, progress: 0, walked: 0, there: false };
|
|
2943
|
+
},
|
|
2944
|
+
forget(objectId) {
|
|
2945
|
+
for (const k of [...openings.keys()]) if (k.startsWith(`${objectId}\0`)) openings.delete(k);
|
|
2946
|
+
if (using?.objectId === objectId) {
|
|
2947
|
+
control.cancel();
|
|
2948
|
+
using = null;
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2443
2954
|
// src/room.ts
|
|
2955
|
+
import { releaseObject3D as releaseObject3D2 } from "@adatechnology/game-shapes-three";
|
|
2444
2956
|
import { createTerrain, createTerrainImpl, DEFAULT_PROFILE } from "@adatechnology/game-terrain";
|
|
2445
2957
|
import { createTerrainRenderer } from "@adatechnology/game-terrain-three";
|
|
2446
2958
|
|
|
@@ -2660,17 +3172,24 @@ var en = {
|
|
|
2660
3172
|
"loading.open_area": "Opening {area}\u2026",
|
|
2661
3173
|
"loading.spawn": "Building {name}\u2026",
|
|
2662
3174
|
"act.loosened": "{name} was heavy but free: the blast moved it",
|
|
3175
|
+
"act.came_apart": "{name} came apart: what stayed is loose and falls",
|
|
2663
3176
|
"act.shoved": "{name} stands free: the shots moved it",
|
|
2664
3177
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
2665
3178
|
"act.use_item": "{name}: {use}",
|
|
3179
|
+
"act.open_with": "Opened {name} with {tool}",
|
|
3180
|
+
"act.open_by_hand": "Opened {name} by hand",
|
|
3181
|
+
"act.ate": "Ate {grams} g of {name}",
|
|
3182
|
+
"act.ate_all": "Ate all of {name} ({grams} g): the packaging is empty",
|
|
2666
3183
|
"act.tap.on": "Tap opened",
|
|
2667
3184
|
"act.tap.off": "Tap closed",
|
|
2668
3185
|
"use.drink": "drink",
|
|
2669
3186
|
"use.sip": "sip",
|
|
2670
3187
|
"use.chug": "chug",
|
|
3188
|
+
"use.eat": "eat",
|
|
2671
3189
|
"use.pour": "pour",
|
|
2672
3190
|
"use.pour_out": "pour out",
|
|
2673
3191
|
"use.open": "open",
|
|
3192
|
+
"use.unscrew": "unscrew",
|
|
2674
3193
|
"use.close": "close",
|
|
2675
3194
|
"hud.reloading": "Reloading...",
|
|
2676
3195
|
"hud.empty": "Empty",
|
|
@@ -2687,15 +3206,32 @@ var en = {
|
|
|
2687
3206
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
2688
3207
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
2689
3208
|
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
3209
|
+
"hud.door_locked": "{name} is locked",
|
|
3210
|
+
"hud.door_blocked": "{name} hits something and stops ({percent}% open)",
|
|
3211
|
+
"hud.door_hint_open": "E: open",
|
|
3212
|
+
"hud.door_hint_close": "E: close",
|
|
3213
|
+
"hud.door_hint_locked": "Locked",
|
|
3214
|
+
"act.door_reach": "reaches for the handle of {name}",
|
|
3215
|
+
"act.door_opened": "opens {name}",
|
|
3216
|
+
"act.door_closed": "closes {name}",
|
|
3217
|
+
"act.door_blocked": "{name} hits something and stops at {percent}%",
|
|
3218
|
+
"act.door_locked": "tries {name}: locked, it only rattles",
|
|
3219
|
+
"act.door_stepped_back": "steps back out of the way of {name}",
|
|
2690
3220
|
"hud.push_on": "Pushing {name}: W pushes, S pulls, let go of E to stop",
|
|
2691
3221
|
"hud.push_too_heavy": "{name} does not budge ({kg} kg)",
|
|
2692
3222
|
"hud.push_blocked": "{name} is stuck: there is something in the way",
|
|
2693
3223
|
"act.push_grip": "leans on {name} ({kg} kg)",
|
|
2694
3224
|
"act.push_engaged": "starts pushing {name} ({kg} kg)",
|
|
3225
|
+
"act.sit": "sits on {name}",
|
|
3226
|
+
"act.lie": "lies down on {name}",
|
|
3227
|
+
"act.stand": "gets up from {name}",
|
|
2695
3228
|
"act.push_too_heavy": "strains against {name}: {kg} kg is too much",
|
|
2696
3229
|
"act.push_obstacle": "{name} hits something and locks",
|
|
2697
3230
|
"act.push_release": "lets go of {name}",
|
|
2698
3231
|
"hud.empty_container": "{name} is empty",
|
|
3232
|
+
"hud.needs_opener": "Opening {name} needs {tools} (put one within reach or in the loadout)",
|
|
3233
|
+
"hud.spoiled_hole": "{name}: the packaging is holed through, what is inside is contaminated; do not eat or drink it",
|
|
3234
|
+
"hud.spoiled_leak": "{name} leaked: what is left is contaminated; do not eat or drink it",
|
|
2699
3235
|
"hud.refused": "Wrong tool for the {kind}",
|
|
2700
3236
|
"hud.drop": "drop",
|
|
2701
3237
|
"hud.rotate": "rotate",
|
|
@@ -2984,17 +3520,24 @@ var ptBR = {
|
|
|
2984
3520
|
"loading.open_area": "Abrindo {area}\u2026",
|
|
2985
3521
|
"loading.spawn": "Montando {name}\u2026",
|
|
2986
3522
|
"act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
|
|
3523
|
+
"act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
|
|
2987
3524
|
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
2988
3525
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
2989
3526
|
"act.use_item": "{name}: {use}",
|
|
3527
|
+
"act.open_with": "Abriu {name} com {tool}",
|
|
3528
|
+
"act.open_by_hand": "Abriu {name} com a m\xE3o",
|
|
3529
|
+
"act.ate": "Comeu {grams} g de {name}",
|
|
3530
|
+
"act.ate_all": "Comeu todo o conte\xFAdo de {name} ({grams} g): a embalagem ficou vazia",
|
|
2990
3531
|
"act.tap.on": "Abriu a torneira",
|
|
2991
3532
|
"act.tap.off": "Fechou a torneira",
|
|
2992
3533
|
"use.drink": "beber",
|
|
2993
3534
|
"use.sip": "golinho",
|
|
2994
3535
|
"use.chug": "virar",
|
|
3536
|
+
"use.eat": "comer",
|
|
2995
3537
|
"use.pour": "despejar",
|
|
2996
3538
|
"use.pour_out": "esvaziar",
|
|
2997
3539
|
"use.open": "abrir",
|
|
3540
|
+
"use.unscrew": "desenroscar",
|
|
2998
3541
|
"use.close": "fechar",
|
|
2999
3542
|
"hud.reloading": "Recarregando...",
|
|
3000
3543
|
"hud.empty": "Sem muni\xE7\xE3o",
|
|
@@ -3011,15 +3554,32 @@ var ptBR = {
|
|
|
3011
3554
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
3012
3555
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
3013
3556
|
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
3557
|
+
"hud.door_locked": "{name} est\xE1 trancado(a)",
|
|
3558
|
+
"hud.door_blocked": "{name} bateu em algo e parou ({percent}% aberto)",
|
|
3559
|
+
"hud.door_hint_open": "E: abrir",
|
|
3560
|
+
"hud.door_hint_close": "E: fechar",
|
|
3561
|
+
"hud.door_hint_locked": "Trancado",
|
|
3562
|
+
"act.door_reach": "leva a m\xE3o \xE0 ma\xE7aneta de {name}",
|
|
3563
|
+
"act.door_opened": "abre {name}",
|
|
3564
|
+
"act.door_closed": "fecha {name}",
|
|
3565
|
+
"act.door_blocked": "{name} bate em algo e para em {percent}%",
|
|
3566
|
+
"act.door_locked": "tenta abrir {name}: trancado, s\xF3 chacoalha",
|
|
3567
|
+
"act.door_stepped_back": "d\xE1 um passo para tr\xE1s para {name} passar",
|
|
3014
3568
|
"hud.push_on": "Empurrando {name}: W empurra, S puxa, solte o E para parar",
|
|
3015
3569
|
"hud.push_too_heavy": "{name} n\xE3o se mexe ({kg} kg)",
|
|
3016
3570
|
"hud.push_blocked": "{name} travou: tem algo no caminho",
|
|
3017
3571
|
"act.push_grip": "encosta em {name} ({kg} kg)",
|
|
3018
3572
|
"act.push_engaged": "come\xE7a a empurrar {name} ({kg} kg)",
|
|
3573
|
+
"act.sit": "senta em {name}",
|
|
3574
|
+
"act.lie": "deita em {name}",
|
|
3575
|
+
"act.stand": "levanta de {name}",
|
|
3019
3576
|
"act.push_too_heavy": "faz for\xE7a contra {name}: {kg} kg \xE9 demais",
|
|
3020
3577
|
"act.push_obstacle": "{name} bate em algo e trava",
|
|
3021
3578
|
"act.push_release": "solta {name}",
|
|
3022
3579
|
"hud.empty_container": "{name} est\xE1 vazio",
|
|
3580
|
+
"hud.needs_opener": "Para abrir {name} precisa de {tools} (deixe um ao alcance ou no invent\xE1rio)",
|
|
3581
|
+
"hud.spoiled_hole": "{name}: a embalagem furou, o conte\xFAdo est\xE1 contaminado; n\xE3o d\xE1 para comer nem beber",
|
|
3582
|
+
"hud.spoiled_leak": "{name} vazou: o que sobrou est\xE1 contaminado; n\xE3o d\xE1 para comer nem beber",
|
|
3023
3583
|
"hud.refused": "Ferramenta errada para {kind}",
|
|
3024
3584
|
"hud.drop": "soltar",
|
|
3025
3585
|
"hud.rotate": "girar",
|
|
@@ -3384,9 +3944,9 @@ function createBenchPanel(o) {
|
|
|
3384
3944
|
hasRoom = rows.room.length > 0;
|
|
3385
3945
|
syncDisplay();
|
|
3386
3946
|
roomLine.textContent = rows.room.map((row) => `${row.name} ${row.text}`).join(" \xB7 ");
|
|
3387
|
-
const
|
|
3388
|
-
if (
|
|
3389
|
-
metricKey =
|
|
3947
|
+
const key2 = rows.module.map((row) => `${row.id}:${row.ratio !== void 0}`).join("|");
|
|
3948
|
+
if (key2 !== metricKey) {
|
|
3949
|
+
metricKey = key2;
|
|
3390
3950
|
renderMetrics(rows.module);
|
|
3391
3951
|
}
|
|
3392
3952
|
for (const row of rows.module) {
|
|
@@ -3740,8 +4300,7 @@ function createUi(o) {
|
|
|
3740
4300
|
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
3741
4301
|
logBtn(t("ui.clear_log"), () => {
|
|
3742
4302
|
o.log.clear();
|
|
3743
|
-
|
|
3744
|
-
nodes.clear();
|
|
4303
|
+
rebuildLog();
|
|
3745
4304
|
});
|
|
3746
4305
|
logPanel.append(logTitle, logBar, logBody);
|
|
3747
4306
|
const MAX_LINES = 200;
|
|
@@ -3754,29 +4313,59 @@ function createUi(o) {
|
|
|
3754
4313
|
const tm = mk3("time", void 0, fmtTime(e.t));
|
|
3755
4314
|
n.append(tm, document.createTextNode(e.text));
|
|
3756
4315
|
};
|
|
3757
|
-
|
|
3758
|
-
|
|
4316
|
+
let pendingAdds = [];
|
|
4317
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
4318
|
+
let isLogStale = false;
|
|
4319
|
+
const isLogVisible = () => !window.matchMedia?.("(max-width:980px)").matches || root.classList.contains("open");
|
|
4320
|
+
const buildNode = (e) => {
|
|
3759
4321
|
const n = mk3("div");
|
|
3760
4322
|
fill(n, e);
|
|
3761
4323
|
nodes.set(e.seq, n);
|
|
3762
|
-
|
|
3763
|
-
while (logBody.childElementCount > MAX_LINES) {
|
|
3764
|
-
const first = logBody.firstElementChild;
|
|
3765
|
-
for (const [k, v] of nodes) if (v === first) nodes.delete(k);
|
|
3766
|
-
first.remove();
|
|
3767
|
-
}
|
|
3768
|
-
if (autoscroll) logBody.scrollTop = logBody.scrollHeight;
|
|
4324
|
+
return n;
|
|
3769
4325
|
};
|
|
3770
4326
|
function rebuildLog() {
|
|
4327
|
+
pendingAdds = [];
|
|
4328
|
+
pendingUpdates.clear();
|
|
4329
|
+
isLogStale = false;
|
|
3771
4330
|
logBody.textContent = "";
|
|
3772
4331
|
nodes.clear();
|
|
3773
|
-
|
|
4332
|
+
const fragment = document.createDocumentFragment();
|
|
4333
|
+
for (const e of o.log.entries().slice(-MAX_LINES * 3)) if (filters[e.channel]) fragment.appendChild(buildNode(e));
|
|
4334
|
+
logBody.appendChild(fragment);
|
|
4335
|
+
trimLog();
|
|
3774
4336
|
}
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
const
|
|
4337
|
+
function trimLog() {
|
|
4338
|
+
while (logBody.childElementCount > MAX_LINES) {
|
|
4339
|
+
const oldest = nodes.keys().next();
|
|
4340
|
+
if (oldest.done) break;
|
|
4341
|
+
nodes.delete(oldest.value);
|
|
4342
|
+
logBody.firstElementChild?.remove();
|
|
4343
|
+
}
|
|
4344
|
+
if (autoscroll) logBody.scrollTop = logBody.scrollHeight;
|
|
4345
|
+
}
|
|
4346
|
+
const flushLog = () => {
|
|
4347
|
+
if (pendingAdds.length === 0 && pendingUpdates.size === 0) return;
|
|
4348
|
+
if (!isLogVisible()) {
|
|
4349
|
+
isLogStale = true;
|
|
4350
|
+
pendingAdds = [];
|
|
4351
|
+
pendingUpdates.clear();
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4354
|
+
if (isLogStale) return rebuildLog();
|
|
4355
|
+
const fragment = document.createDocumentFragment();
|
|
4356
|
+
for (const e of pendingAdds.slice(-MAX_LINES)) if (filters[e.channel]) fragment.appendChild(buildNode(e));
|
|
4357
|
+
pendingAdds = [];
|
|
4358
|
+
logBody.appendChild(fragment);
|
|
4359
|
+
for (const [seq, e] of pendingUpdates) {
|
|
4360
|
+
const n = nodes.get(seq);
|
|
3778
4361
|
if (n) fill(n, e);
|
|
3779
|
-
}
|
|
4362
|
+
}
|
|
4363
|
+
pendingUpdates.clear();
|
|
4364
|
+
trimLog();
|
|
4365
|
+
};
|
|
4366
|
+
const offLog = o.log.on((e, updated) => {
|
|
4367
|
+
if (updated) pendingUpdates.set(e.seq, e);
|
|
4368
|
+
else pendingAdds.push(e);
|
|
3780
4369
|
});
|
|
3781
4370
|
const right = mk3("div", "tr-right");
|
|
3782
4371
|
const cat = mk3("div", "tr-panel tr-cat");
|
|
@@ -3866,8 +4455,8 @@ function createUi(o) {
|
|
|
3866
4455
|
if (o.kick) ctl(o.kick.label, () => o.kick.onKick());
|
|
3867
4456
|
const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
|
|
3868
4457
|
fullscreenBtn.title = t("ui.fullscreen.hint");
|
|
3869
|
-
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((
|
|
3870
|
-
for (const { key, b } of blastBtns) b.title = t(`ui.blast.${
|
|
4458
|
+
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key2) => ({ key: key2, b: ctl(t(`ui.blast.${key2}`), () => o.onBlast(key2)) }));
|
|
4459
|
+
for (const { key: key2, b } of blastBtns) b.title = t(`ui.blast.${key2}.hint`);
|
|
3871
4460
|
const qSel = mk3("select");
|
|
3872
4461
|
for (const q of ["low", "medium", "high"]) {
|
|
3873
4462
|
const op = mk3("option", void 0, t(`ui.quality.${q}`));
|
|
@@ -3943,6 +4532,7 @@ function createUi(o) {
|
|
|
3943
4532
|
toggle.type = "button";
|
|
3944
4533
|
const togglePanels = () => {
|
|
3945
4534
|
root.classList.toggle("open");
|
|
4535
|
+
if (isLogStale && isLogVisible()) rebuildLog();
|
|
3946
4536
|
o.onResize();
|
|
3947
4537
|
};
|
|
3948
4538
|
toggle.onclick = togglePanels;
|
|
@@ -4031,6 +4621,7 @@ function createUi(o) {
|
|
|
4031
4621
|
ring.style.display = value === null ? "none" : "block";
|
|
4032
4622
|
if (value !== null) ringFill.style.strokeDashoffset = String(RING_LENGTH * (1 - value));
|
|
4033
4623
|
},
|
|
4624
|
+
flushLog,
|
|
4034
4625
|
hud(s) {
|
|
4035
4626
|
toolBox.textContent = "";
|
|
4036
4627
|
toolBox.append(document.createTextNode(s.toolName));
|
|
@@ -4061,9 +4652,9 @@ function createUi(o) {
|
|
|
4061
4652
|
held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
4062
4653
|
}
|
|
4063
4654
|
} else held.style.display = "none";
|
|
4064
|
-
const
|
|
4065
|
-
if (
|
|
4066
|
-
lastSlotsKey =
|
|
4655
|
+
const key2 = s.slots.join("|") + s.page;
|
|
4656
|
+
if (key2 !== lastSlotsKey) {
|
|
4657
|
+
lastSlotsKey = key2;
|
|
4067
4658
|
hotbar.textContent = "";
|
|
4068
4659
|
slotsEl.length = 0;
|
|
4069
4660
|
hudPage = s.page - 1;
|
|
@@ -4080,7 +4671,7 @@ function createUi(o) {
|
|
|
4080
4671
|
slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
|
|
4081
4672
|
tapBtn.classList.toggle("on", s.tap);
|
|
4082
4673
|
comboBtn?.classList.toggle("on", !!s.combo);
|
|
4083
|
-
for (const { key:
|
|
4674
|
+
for (const { key: key3, b } of blastBtns) b.classList.toggle("on", s.blast[key3]);
|
|
4084
4675
|
count.textContent = t("ui.objects", { n: s.objects });
|
|
4085
4676
|
const ak = JSON.stringify(s.animations);
|
|
4086
4677
|
if (ak !== lastAnim) {
|
|
@@ -4131,7 +4722,7 @@ function createUi(o) {
|
|
|
4131
4722
|
var FIXED_DT = 1 / 60;
|
|
4132
4723
|
var SOLID_SPAN = 3;
|
|
4133
4724
|
var STEP_UP = 0.45;
|
|
4134
|
-
var
|
|
4725
|
+
var BODY_HEIGHT2 = 1.75;
|
|
4135
4726
|
var KICK_MASS = 6;
|
|
4136
4727
|
var KICK_HEIGHT = 0.6;
|
|
4137
4728
|
var GROUND_ID = "room_ground";
|
|
@@ -4143,9 +4734,8 @@ var GROUND_DUST_RADIUS = 0.2;
|
|
|
4143
4734
|
var GROUND_FIRE_EVERY = 0.3;
|
|
4144
4735
|
var SHOVE_SPEED = 0.04;
|
|
4145
4736
|
var METRICS_INTERVAL_S = 0.25;
|
|
4146
|
-
var CARRY_NEAR = 0.62;
|
|
4147
4737
|
var CARRY_PALM = 0.012;
|
|
4148
|
-
var
|
|
4738
|
+
var CAP_RISE = 0.03;
|
|
4149
4739
|
var MAX_STRIDE_SPEED = 10;
|
|
4150
4740
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
4151
4741
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
@@ -4155,8 +4745,8 @@ function createTestRoom(options) {
|
|
|
4155
4745
|
const S = resolveStrings(locale);
|
|
4156
4746
|
const dStrings = resolveDestructionStrings(locale);
|
|
4157
4747
|
const fStrings = resolveFluidStrings(locale);
|
|
4158
|
-
const t = (
|
|
4159
|
-
let s = S.text[
|
|
4748
|
+
const t = (key2, vars) => {
|
|
4749
|
+
let s = S.text[key2] ?? key2;
|
|
4160
4750
|
if (vars) for (const k of Object.keys(vars)) s = s.split(`{${k}}`).join(String(vars[k]));
|
|
4161
4751
|
return s;
|
|
4162
4752
|
};
|
|
@@ -4241,11 +4831,11 @@ function createTestRoom(options) {
|
|
|
4241
4831
|
const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
|
|
4242
4832
|
log.setClock(() => simTime);
|
|
4243
4833
|
const ownAudio = options.audio === void 0;
|
|
4244
|
-
const audio = options.audio === false ? null : options.audio ?? createAudioEngine();
|
|
4834
|
+
const audio = options.audio === false ? null : options.audio ?? createAudioEngine({ deferredStart: {} });
|
|
4245
4835
|
const destructionSound = audio ? createDestructionSound(audio) : null;
|
|
4246
|
-
const playSound = (
|
|
4247
|
-
destructionSound?.onSound(
|
|
4248
|
-
options.onSound?.(
|
|
4836
|
+
const playSound = (key2, point, volume) => {
|
|
4837
|
+
destructionSound?.onSound(key2, point, volume);
|
|
4838
|
+
options.onSound?.(key2, point, volume);
|
|
4249
4839
|
};
|
|
4250
4840
|
const vfx = createVfxRenderer(THREE, {
|
|
4251
4841
|
scene,
|
|
@@ -4255,6 +4845,7 @@ function createTestRoom(options) {
|
|
|
4255
4845
|
pointScale,
|
|
4256
4846
|
vfx: createVfx({
|
|
4257
4847
|
profile: quality,
|
|
4848
|
+
...options.debris?.particles ? { sprites: { kinds: patchSpriteKinds(SPRITE_KINDS, options.debris.particles) } } : {},
|
|
4258
4849
|
groundAt
|
|
4259
4850
|
})
|
|
4260
4851
|
});
|
|
@@ -4265,6 +4856,9 @@ function createTestRoom(options) {
|
|
|
4265
4856
|
quality,
|
|
4266
4857
|
groundAt,
|
|
4267
4858
|
pointScale,
|
|
4859
|
+
...options.debris?.fragmentLife !== void 0 ? { fragmentLife: options.debris.fragmentLife } : {},
|
|
4860
|
+
...options.debris?.maxFragments !== void 0 ? { maxFragments: options.debris.maxFragments } : {},
|
|
4861
|
+
eventBudgetMs: options.debris?.eventBudgetMs ?? 8,
|
|
4268
4862
|
onSound: playSound,
|
|
4269
4863
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
4270
4864
|
// a blast moves what is heavy but not nailed down: the renderer makes it a piece the moment the shove reaches it
|
|
@@ -4272,13 +4866,13 @@ function createTestRoom(options) {
|
|
|
4272
4866
|
const e = entries.get(objectId);
|
|
4273
4867
|
return e?.free === true && !e.piece;
|
|
4274
4868
|
},
|
|
4275
|
-
onLoosened: (objectId, piece) => {
|
|
4869
|
+
onLoosened: (objectId, piece, cause) => {
|
|
4276
4870
|
const e = entries.get(objectId);
|
|
4277
4871
|
if (!e) return;
|
|
4278
4872
|
e.piece = piece;
|
|
4279
4873
|
leaveFootprint(objectId);
|
|
4280
4874
|
rendererPieces.set(objectId, piece);
|
|
4281
|
-
action("loosened", t("act.loosened", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
4875
|
+
action("loosened", t(cause === "blast" ? "act.loosened" : "act.came_apart", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
4282
4876
|
},
|
|
4283
4877
|
onPiece: (piece) => {
|
|
4284
4878
|
rendererPieces.set(piece.objectId, piece);
|
|
@@ -4494,7 +5088,7 @@ function createTestRoom(options) {
|
|
|
4494
5088
|
const dz = Math.max(s.min[2] - z, 0, z - s.max[2]);
|
|
4495
5089
|
if (dx * dx + dz * dz > radius * radius) continue;
|
|
4496
5090
|
if (s.max[1] <= feet + STEP_UP) floor = Math.max(floor, s.max[1]);
|
|
4497
|
-
else if (s.min[1] < feet +
|
|
5091
|
+
else if (s.min[1] < feet + BODY_HEIGHT2) blocked = true;
|
|
4498
5092
|
}
|
|
4499
5093
|
return { floor, blocked };
|
|
4500
5094
|
};
|
|
@@ -4568,7 +5162,7 @@ function createTestRoom(options) {
|
|
|
4568
5162
|
let best = null;
|
|
4569
5163
|
let bestDistance = PUSH_REACH;
|
|
4570
5164
|
for (const e of entries.values()) {
|
|
4571
|
-
if (e.
|
|
5165
|
+
if (!e.pushable || !e.piece && !e.free) continue;
|
|
4572
5166
|
const b = bodyBox(e);
|
|
4573
5167
|
if (!b) continue;
|
|
4574
5168
|
const dx = Math.max(b.minX, Math.min(b.maxX, player.x)) - player.x;
|
|
@@ -4592,7 +5186,7 @@ function createTestRoom(options) {
|
|
|
4592
5186
|
aim();
|
|
4593
5187
|
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
4594
5188
|
const aimed = r ? rendererPieces.get(r.object)?.root ?? (entries.get(r.object)?.free ? entries.get(r.object)?.so.root : void 0) : void 0;
|
|
4595
|
-
if (r && aimed) return { objectId: r.object, root: aimed, name: nameOf(r.object), piece: rendererPieces.get(r.object) ?? null };
|
|
5189
|
+
if (r && aimed) return entries.get(r.object)?.pushable ? { objectId: r.object, root: aimed, name: nameOf(r.object), piece: rendererPieces.get(r.object) ?? null } : null;
|
|
4596
5190
|
return leanedOn();
|
|
4597
5191
|
},
|
|
4598
5192
|
massOf: (objectId) => massOfObject(objectId),
|
|
@@ -4615,9 +5209,96 @@ function createTestRoom(options) {
|
|
|
4615
5209
|
else if (ev.type === "too_heavy") say(t("hud.push_too_heavy", vars));
|
|
4616
5210
|
else if (ev.type === "obstacle") say(t("hud.push_blocked", vars));
|
|
4617
5211
|
},
|
|
4618
|
-
ring: (value) => ui?.holdRing(value)
|
|
5212
|
+
ring: (value) => ui?.holdRing(value),
|
|
5213
|
+
hasBody: () => !!avatar?.eye
|
|
5214
|
+
});
|
|
5215
|
+
const seatHolder = (objectId) => {
|
|
5216
|
+
const e = entries.get(objectId);
|
|
5217
|
+
const seats = e?.item?.seats;
|
|
5218
|
+
if (!e || !seats?.length || !world.has(objectId)) return null;
|
|
5219
|
+
return { objectId, root: rendererPieces.get(objectId)?.root ?? e.so.root, seats };
|
|
5220
|
+
};
|
|
5221
|
+
const seating = createSeating({
|
|
5222
|
+
THREE,
|
|
5223
|
+
player,
|
|
5224
|
+
pick: () => {
|
|
5225
|
+
aim();
|
|
5226
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
5227
|
+
return r ? seatHolder(r.object) : null;
|
|
5228
|
+
},
|
|
5229
|
+
find: seatHolder,
|
|
5230
|
+
hasBody: () => !!avatar?.eye,
|
|
5231
|
+
groundAt: (x, z) => groundAt(x, z),
|
|
5232
|
+
onSit: (seat) => action("sit", t(seat.pose === "lie" ? "act.lie" : "act.sit", { name: nameOf(seat.objectId) }), { objectId: seat.objectId, itemId: entryOf(seat.objectId)?.so.itemId, data: { seat: seat.seatId, pose: seat.pose } }),
|
|
5233
|
+
onStand: (seat) => action("stand", t("act.stand", { name: nameOf(seat.objectId) }), { objectId: seat.objectId, itemId: entryOf(seat.objectId)?.so.itemId, data: { seat: seat.seatId } })
|
|
4619
5234
|
});
|
|
4620
5235
|
let pushByKey = false;
|
|
5236
|
+
const leafCache = /* @__PURE__ */ new WeakMap();
|
|
5237
|
+
const leavesOf = (objectId) => {
|
|
5238
|
+
const item = entries.get(objectId)?.item;
|
|
5239
|
+
if (!item?.animations?.length) return [];
|
|
5240
|
+
const cached = leafCache.get(item);
|
|
5241
|
+
if (cached) return cached;
|
|
5242
|
+
const byPart = /* @__PURE__ */ new Map();
|
|
5243
|
+
for (const clip of item.animations) {
|
|
5244
|
+
if (!clip.id.startsWith("open_")) continue;
|
|
5245
|
+
const parts = /* @__PURE__ */ new Set();
|
|
5246
|
+
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) parts.add(id);
|
|
5247
|
+
for (const part of parts) {
|
|
5248
|
+
const had = byPart.get(part);
|
|
5249
|
+
if (!had || parts.size < had.n) byPart.set(part, { clip, n: parts.size });
|
|
5250
|
+
}
|
|
5251
|
+
}
|
|
5252
|
+
const leaves = [...byPart].map(([part, v]) => ({ part, clip: v.clip }));
|
|
5253
|
+
leafCache.set(item, leaves);
|
|
5254
|
+
return leaves;
|
|
5255
|
+
};
|
|
5256
|
+
const lockedLeaves = /* @__PURE__ */ new Set();
|
|
5257
|
+
const doors = createRoomDoors({
|
|
5258
|
+
THREE,
|
|
5259
|
+
player,
|
|
5260
|
+
world,
|
|
5261
|
+
camera,
|
|
5262
|
+
arena: { limit: arenaSize, colliders: standingColliders, ids: standingIds },
|
|
5263
|
+
...options.doors ? { rules: options.doors } : {},
|
|
5264
|
+
pick: () => {
|
|
5265
|
+
if (heldPiece) return [];
|
|
5266
|
+
aim();
|
|
5267
|
+
const hits = [];
|
|
5268
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], DOOR_REACH);
|
|
5269
|
+
if (r) hits.push({ objectId: r.object, part: r.part, point: r.point });
|
|
5270
|
+
if (thirdActive()) {
|
|
5271
|
+
camera.getWorldDirection(tmpV2);
|
|
5272
|
+
const e = rayWorld([aimO[0], aimO[1], aimO[2]], [tmpV2.x, tmpV2.y, tmpV2.z], DOOR_REACH);
|
|
5273
|
+
if (e) hits.push({ objectId: e.object, part: e.part, point: e.point });
|
|
5274
|
+
}
|
|
5275
|
+
return hits;
|
|
5276
|
+
},
|
|
5277
|
+
leaves: leavesOf,
|
|
5278
|
+
objectOf: (objectId, part) => entries.get(objectId)?.so.root.getObjectByName(part) ?? null,
|
|
5279
|
+
pose: (objectId, part, clip, fraction) => {
|
|
5280
|
+
const e = entries.get(objectId);
|
|
5281
|
+
const obj = e?.so.root.getObjectByName(part);
|
|
5282
|
+
if (!e || !obj) return;
|
|
5283
|
+
if (!restPose.has(obj)) restPose.set(obj, { p: [obj.position.x, obj.position.y, obj.position.z], r: [obj.rotation.x, obj.rotation.y, obj.rotation.z] });
|
|
5284
|
+
const rest = restPose.get(obj);
|
|
5285
|
+
const pose = poseAt(clip, part, Math.max(0, Math.min(1, fraction)) * clip.duration);
|
|
5286
|
+
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
5287
|
+
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
5288
|
+
followPart(e, part, obj, rest);
|
|
5289
|
+
},
|
|
5290
|
+
isLocked: (objectId, part) => lockedLeaves.has(`${objectId}\0*`) || lockedLeaves.has(`${objectId}\0${part}`),
|
|
5291
|
+
nameOf,
|
|
5292
|
+
impactKey: (objectId, part) => impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")),
|
|
5293
|
+
sound: playSound,
|
|
5294
|
+
onEvent: (ev) => {
|
|
5295
|
+
const vars = { name: ev.name, percent: Math.round(ev.opening * 100) };
|
|
5296
|
+
action(`door_${ev.type}`, t(`act.door_${ev.type}`, vars), { objectId: ev.objectId, itemId: entryOf(ev.objectId)?.so.itemId, part: ev.part, data: { opening: Math.round(ev.opening * 1e3) / 1e3, ...ev.by ? { by: ev.by } : {} } });
|
|
5297
|
+
fire("door", ev);
|
|
5298
|
+
if (ev.type === "locked") say(t("hud.door_locked", vars));
|
|
5299
|
+
else if (ev.type === "blocked") say(t("hud.door_blocked", vars));
|
|
5300
|
+
}
|
|
5301
|
+
});
|
|
4621
5302
|
const massOfObject = (id) => {
|
|
4622
5303
|
const st = world.state(id);
|
|
4623
5304
|
let m = 0;
|
|
@@ -4643,6 +5324,8 @@ function createTestRoom(options) {
|
|
|
4643
5324
|
const q = root.getWorldQuaternion(tmpQ);
|
|
4644
5325
|
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(root.matrixWorld);
|
|
4645
5326
|
e.containerId = fluids.addContainer(f.container, { id: `${objectId}_fluid`, position: [p.x, p.y, p.z], rotation: [q.x, q.y, q.z, q.w] });
|
|
5327
|
+
e.fluidStart = f.container.liters;
|
|
5328
|
+
e.fluidUsed = 0;
|
|
4646
5329
|
fluidView.bindContainer(e.containerId, root, { innerOffset: e.fluidOffset });
|
|
4647
5330
|
}
|
|
4648
5331
|
return e;
|
|
@@ -4663,6 +5346,7 @@ function createTestRoom(options) {
|
|
|
4663
5346
|
world.add(structuredCloneSpec(o.spec), { id: o.id, position: o.position, ...o.rotationY ? { rotation: [0, Math.sin(o.rotationY / 2), 0, Math.cos(o.rotationY / 2)] } : {} });
|
|
4664
5347
|
const e = registerEntry(o.id, o.id, root, o.spec, o.label ?? S.text[`arena.${o.id}`] ?? o.id, void 0, void 0, true, false);
|
|
4665
5348
|
if (o.free) e.free = true;
|
|
5349
|
+
if (o.pushable) e.pushable = true;
|
|
4666
5350
|
e.follower = o.follow?.(root, world, o.id) || null;
|
|
4667
5351
|
}
|
|
4668
5352
|
};
|
|
@@ -4762,7 +5446,7 @@ function createTestRoom(options) {
|
|
|
4762
5446
|
if (movable) {
|
|
4763
5447
|
const box2 = new THREE.Box3().setFromObject(root);
|
|
4764
5448
|
const base = Math.max(pos[1], groundAt(pos[0], pos[2]));
|
|
4765
|
-
pos = [pos[0], pos[1] + (base - box2.min.y), pos[2]];
|
|
5449
|
+
if (!box2.isEmpty()) pos = [pos[0], pos[1] + (base - box2.min.y), pos[2]];
|
|
4766
5450
|
root.position.set(pos[0], pos[1], pos[2]);
|
|
4767
5451
|
root.updateMatrixWorld(true);
|
|
4768
5452
|
}
|
|
@@ -4771,6 +5455,7 @@ function createTestRoom(options) {
|
|
|
4771
5455
|
else world.add(spec, { id: objectId, position: pos, rotation: [q.x, q.y, q.z, q.w] });
|
|
4772
5456
|
const e = registerEntry(objectId, item.id, root, spec, item.name, parts, item, false, movable);
|
|
4773
5457
|
if (!own && !movable && item.movable === void 0) e.free = true;
|
|
5458
|
+
if (item.pushable) e.pushable = true;
|
|
4774
5459
|
action("spawn", t("act.spawn", { name: item.name, x: num(pos[0], 1), z: num(pos[2], 1) }), { objectId, itemId: item.id, data: { position: pos, movable, massKg: Math.round(massOfObject(objectId) * 10) / 10 } });
|
|
4775
5460
|
fire("spawn", e.so);
|
|
4776
5461
|
return e.so;
|
|
@@ -4855,6 +5540,23 @@ function createTestRoom(options) {
|
|
|
4855
5540
|
}
|
|
4856
5541
|
return m;
|
|
4857
5542
|
};
|
|
5543
|
+
let releaseBatch = null;
|
|
5544
|
+
const dropTree = (root) => {
|
|
5545
|
+
if (!root) return;
|
|
5546
|
+
if (releaseBatch) releaseBatch.push(root);
|
|
5547
|
+
else releaseObject3D2(root, { keep: scene });
|
|
5548
|
+
};
|
|
5549
|
+
const releasingTogether = (fn) => {
|
|
5550
|
+
if (releaseBatch) return fn();
|
|
5551
|
+
releaseBatch = [];
|
|
5552
|
+
try {
|
|
5553
|
+
fn();
|
|
5554
|
+
} finally {
|
|
5555
|
+
const roots = releaseBatch;
|
|
5556
|
+
releaseBatch = null;
|
|
5557
|
+
releaseObject3D2(roots, { keep: scene });
|
|
5558
|
+
}
|
|
5559
|
+
};
|
|
4858
5560
|
const removeEntry = (id) => {
|
|
4859
5561
|
const e = entries.get(id);
|
|
4860
5562
|
if (!e) return;
|
|
@@ -4871,8 +5573,12 @@ function createTestRoom(options) {
|
|
|
4871
5573
|
e.follower?.dispose?.();
|
|
4872
5574
|
if (world.has(id)) world.remove(id);
|
|
4873
5575
|
e.so.root.removeFromParent();
|
|
5576
|
+
const inner = wrapped.get(e.so.root);
|
|
4874
5577
|
letGo(e.so.root);
|
|
5578
|
+
dropTree(e.so.root);
|
|
5579
|
+
dropTree(inner);
|
|
4875
5580
|
entries.delete(id);
|
|
5581
|
+
doors.forget(id);
|
|
4876
5582
|
rendererPieces.delete(id);
|
|
4877
5583
|
specs.delete(id);
|
|
4878
5584
|
};
|
|
@@ -4886,12 +5592,14 @@ function createTestRoom(options) {
|
|
|
4886
5592
|
}
|
|
4887
5593
|
if (world.has(id)) world.remove(id);
|
|
4888
5594
|
p?.root.removeFromParent();
|
|
5595
|
+
dropTree(p?.root);
|
|
4889
5596
|
rendererPieces.delete(id);
|
|
4890
5597
|
splitOf.delete(id);
|
|
4891
5598
|
}
|
|
4892
5599
|
loose.length = 0;
|
|
4893
5600
|
};
|
|
4894
|
-
const clearEverything = () =>
|
|
5601
|
+
const clearEverything = () => releasingTogether(clearEverythingNow);
|
|
5602
|
+
const clearEverythingNow = () => {
|
|
4895
5603
|
if (heldEntry || heldPiece) releaseHeld();
|
|
4896
5604
|
useItemPlayer = null;
|
|
4897
5605
|
playing.length = 0;
|
|
@@ -4914,6 +5622,7 @@ function createTestRoom(options) {
|
|
|
4914
5622
|
fluids.setTap(TAP_ID, false);
|
|
4915
5623
|
};
|
|
4916
5624
|
const clearListeners = /* @__PURE__ */ new Set();
|
|
5625
|
+
const resetListeners = /* @__PURE__ */ new Set();
|
|
4917
5626
|
const clear = () => {
|
|
4918
5627
|
clearEverything();
|
|
4919
5628
|
for (const listener of [...clearListeners]) listener();
|
|
@@ -4936,10 +5645,15 @@ function createTestRoom(options) {
|
|
|
4936
5645
|
action("reset", t("act.reset"));
|
|
4937
5646
|
};
|
|
4938
5647
|
const hardReset = () => {
|
|
5648
|
+
seating.release();
|
|
4939
5649
|
pushByKey = false;
|
|
4940
5650
|
push.end();
|
|
4941
|
-
|
|
4942
|
-
|
|
5651
|
+
releasingTogether(() => {
|
|
5652
|
+
clearEverythingNow();
|
|
5653
|
+
for (const id of [...entries.keys()]) removeEntry(id);
|
|
5654
|
+
});
|
|
5655
|
+
renderer.reset();
|
|
5656
|
+
for (const listener of [...resetListeners]) listener();
|
|
4943
5657
|
tools.clear();
|
|
4944
5658
|
tool = toolState(toolId);
|
|
4945
5659
|
if (pristineWorld) {
|
|
@@ -4954,6 +5668,8 @@ function createTestRoom(options) {
|
|
|
4954
5668
|
pending = [];
|
|
4955
5669
|
strikes = [];
|
|
4956
5670
|
rng = 12345;
|
|
5671
|
+
filmRandom.seed = 1;
|
|
5672
|
+
groundFireClock = 0;
|
|
4957
5673
|
counter = 0;
|
|
4958
5674
|
slot = 0;
|
|
4959
5675
|
prevVy.clear();
|
|
@@ -4987,8 +5703,8 @@ function createTestRoom(options) {
|
|
|
4987
5703
|
const thirdActive = () => cameraMode === "third" && avatar !== null;
|
|
4988
5704
|
const syncAvatarVisible = () => {
|
|
4989
5705
|
if (!avatar) return;
|
|
4990
|
-
avatar.root.visible = cameraMode === "third";
|
|
4991
|
-
|
|
5706
|
+
avatar.root.visible = cameraMode === "third" || !!avatar.eye;
|
|
5707
|
+
avatar.setView?.(cameraMode, camera);
|
|
4992
5708
|
};
|
|
4993
5709
|
const setCameraMode = (mode) => {
|
|
4994
5710
|
if (mode !== "first" && mode !== "third") return;
|
|
@@ -5001,13 +5717,13 @@ function createTestRoom(options) {
|
|
|
5001
5717
|
};
|
|
5002
5718
|
const setAvatar = (next) => {
|
|
5003
5719
|
if (avatar === next) return;
|
|
5720
|
+
avatar?.setView?.("third", null);
|
|
5004
5721
|
avatar?.root.removeFromParent();
|
|
5005
|
-
avatar?.firstPerson?.root.removeFromParent();
|
|
5006
5722
|
avatar = next;
|
|
5007
5723
|
feetPrev = null;
|
|
5008
5724
|
if (avatar) {
|
|
5725
|
+
avatar.root.userData.viewmodel = true;
|
|
5009
5726
|
scene.add(avatar.root);
|
|
5010
|
-
if (avatar.firstPerson) scene.add(avatar.firstPerson.root);
|
|
5011
5727
|
syncAvatarVisible();
|
|
5012
5728
|
stepAvatar(0);
|
|
5013
5729
|
}
|
|
@@ -5029,38 +5745,73 @@ function createTestRoom(options) {
|
|
|
5029
5745
|
avatarState.pitch = player.pitch;
|
|
5030
5746
|
avatarState.holding = heldPiece !== null;
|
|
5031
5747
|
avatarState.push = push.avatarState;
|
|
5032
|
-
|
|
5748
|
+
const seat = seating.seat;
|
|
5749
|
+
avatarState.seat = seat ? { pose: seat.pose, hip: seat.hips[1] - player.feet } : null;
|
|
5750
|
+
avatarState.door = doors.avatarState;
|
|
5751
|
+
avatarState.itemUse = useItemPlayer ? { id: useItemName, ...useItemPlayer.pose(), ...twistHand?.gripL !== void 0 ? { gripL: twistHand.gripL } : {} } : heldPiece && heldTilt > 0.01 ? { id: "tilt", position: [0.2, -0.1, -0.38], rotation: [-heldTilt, 0, 0] } : null;
|
|
5033
5752
|
avatar.root.position.set(x, player.feet, z);
|
|
5034
5753
|
avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
|
|
5035
5754
|
avatar.update(dt, avatarState);
|
|
5036
5755
|
avatar.root.updateMatrixWorld(true);
|
|
5756
|
+
placeEye();
|
|
5757
|
+
};
|
|
5758
|
+
let scriptedAt = null;
|
|
5759
|
+
const lookAt = (eye, target) => {
|
|
5760
|
+
player.look(eye, target);
|
|
5761
|
+
scriptedAt = [player.x, player.z];
|
|
5762
|
+
};
|
|
5763
|
+
const EYE_CLEARANCE = 0.1;
|
|
5764
|
+
const eyeAt = new THREE.Vector3();
|
|
5765
|
+
const placeEye = () => {
|
|
5766
|
+
if (!avatar?.eye) return;
|
|
5767
|
+
if (scriptedAt) {
|
|
5768
|
+
const [walkX, walkZ] = player.walkIntent;
|
|
5769
|
+
if (!walkX && !walkZ && Math.hypot(player.x - scriptedAt[0], player.z - scriptedAt[1]) < 1e-6) return;
|
|
5770
|
+
scriptedAt = null;
|
|
5771
|
+
}
|
|
5772
|
+
avatar.eye(eyeAt);
|
|
5773
|
+
const dx = eyeAt.x - player.x;
|
|
5774
|
+
const dz = eyeAt.z - player.z;
|
|
5775
|
+
const ahead = Math.hypot(dx, dz);
|
|
5776
|
+
if (ahead > 1e-4) {
|
|
5777
|
+
const dir = [dx / ahead, 0, dz / ahead];
|
|
5778
|
+
const hit = rayWorld([player.x, eyeAt.y, player.z], dir, ahead + EYE_CLEARANCE);
|
|
5779
|
+
if (hit) {
|
|
5780
|
+
const keep = Math.max(0, hit.distance - EYE_CLEARANCE);
|
|
5781
|
+
eyeAt.set(player.x + dir[0] * keep, eyeAt.y, player.z + dir[2] * keep);
|
|
5782
|
+
}
|
|
5783
|
+
}
|
|
5784
|
+
camera.position.copy(eyeAt);
|
|
5785
|
+
camera.updateMatrixWorld(true);
|
|
5037
5786
|
};
|
|
5038
5787
|
let handSource = null;
|
|
5039
|
-
let carriedHand = null;
|
|
5040
5788
|
let debugView = null;
|
|
5041
5789
|
let lastHand = { stance: "relaxed", triggerFinger: null, targets: null };
|
|
5042
5790
|
const strikeClips = options.strikeClips ?? DEFAULT_STRIKE_CLIPS;
|
|
5043
5791
|
const handStances = options.handStances ?? DEFAULT_HAND_STANCES;
|
|
5044
|
-
const
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
triggerFinger: targets?.fingers?.index ?? null
|
|
5057
|
-
};
|
|
5058
|
-
lastHand = { stance: state.stance, triggerFinger: state.triggerFinger, targets };
|
|
5059
|
-
arms.update(dt, state);
|
|
5792
|
+
const SHOULDER_POCKET_IN = 0.1;
|
|
5793
|
+
const pocketAt = new THREE.Vector3();
|
|
5794
|
+
const aimTurn = new THREE.Quaternion();
|
|
5795
|
+
const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
5796
|
+
const bodyHoldFrame = () => {
|
|
5797
|
+
const shoulder = avatar?.shoulder?.("R");
|
|
5798
|
+
if (!shoulder) return null;
|
|
5799
|
+
shoulder.getWorldPosition(pocketAt);
|
|
5800
|
+
pocketAt.x -= Math.cos(player.yaw) * SHOULDER_POCKET_IN;
|
|
5801
|
+
pocketAt.z += Math.sin(player.yaw) * SHOULDER_POCKET_IN;
|
|
5802
|
+
aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
5803
|
+
return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
|
|
5060
5804
|
};
|
|
5061
5805
|
const reachAvatar = () => {
|
|
5062
|
-
|
|
5063
|
-
|
|
5806
|
+
const doorHand = doors.handTargets();
|
|
5807
|
+
const targets = push.active ? null : doorHand ?? (heldPiece ? twistHand : handSource?.() ?? null);
|
|
5808
|
+
lastHand = {
|
|
5809
|
+
stance: push.active || doorHand ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
5810
|
+
triggerFinger: targets?.fingers?.index ?? null,
|
|
5811
|
+
targets: push.handTargets() ?? targets
|
|
5812
|
+
};
|
|
5813
|
+
if (!avatar?.reach) return;
|
|
5814
|
+
avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
|
|
5064
5815
|
};
|
|
5065
5816
|
const applyThirdView = () => {
|
|
5066
5817
|
if (!thirdActive()) return () => void 0;
|
|
@@ -5566,9 +6317,9 @@ function createTestRoom(options) {
|
|
|
5566
6317
|
}
|
|
5567
6318
|
let blastOptions = { ...options.blast };
|
|
5568
6319
|
let lastBlast;
|
|
5569
|
-
const blastOn = (
|
|
5570
|
-
const toggleBlast = (
|
|
5571
|
-
blastOptions = { ...blastOptions, [
|
|
6320
|
+
const blastOn = (key2) => Boolean(blastOptions[key2]);
|
|
6321
|
+
const toggleBlast = (key2) => {
|
|
6322
|
+
blastOptions = { ...blastOptions, [key2]: blastOn(key2) ? false : true };
|
|
5572
6323
|
};
|
|
5573
6324
|
const explosives = createExplosives({
|
|
5574
6325
|
THREE,
|
|
@@ -5699,10 +6450,12 @@ function createTestRoom(options) {
|
|
|
5699
6450
|
let tiltKey = 0;
|
|
5700
6451
|
const heldLocal = new THREE.Vector3();
|
|
5701
6452
|
const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
|
|
5702
|
-
let heldRadius = 0.2;
|
|
5703
6453
|
const heldVel = [0, 0, 0];
|
|
5704
6454
|
let useItemPlayer = null;
|
|
5705
6455
|
let useItemName = "";
|
|
6456
|
+
let afterOpening = null;
|
|
6457
|
+
let twisting = null;
|
|
6458
|
+
let twistHand = null;
|
|
5706
6459
|
const releaseHeld = () => {
|
|
5707
6460
|
if (heldPiece) {
|
|
5708
6461
|
heldPiece.held = false;
|
|
@@ -5711,6 +6464,9 @@ function createTestRoom(options) {
|
|
|
5711
6464
|
}
|
|
5712
6465
|
useItemPlayer?.cancel();
|
|
5713
6466
|
useItemPlayer = null;
|
|
6467
|
+
afterOpening = null;
|
|
6468
|
+
twisting = null;
|
|
6469
|
+
twistHand = null;
|
|
5714
6470
|
heldEntry = null;
|
|
5715
6471
|
heldPiece = null;
|
|
5716
6472
|
heldTilt = 0;
|
|
@@ -5757,7 +6513,6 @@ function createTestRoom(options) {
|
|
|
5757
6513
|
box2.getSize(heldSize);
|
|
5758
6514
|
piece.root.quaternion.copy(savedQ);
|
|
5759
6515
|
piece.root.updateMatrixWorld(true);
|
|
5760
|
-
heldRadius = heldSize.length() / 3;
|
|
5761
6516
|
heldYaw = 0;
|
|
5762
6517
|
heldTilt = 0;
|
|
5763
6518
|
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
@@ -5778,52 +6533,28 @@ function createTestRoom(options) {
|
|
|
5778
6533
|
fire("throw", { objectId: id, speed });
|
|
5779
6534
|
return true;
|
|
5780
6535
|
};
|
|
5781
|
-
const
|
|
5782
|
-
|
|
5783
|
-
const
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
const palm = center2.clone().addScaledVector(handY, radius + CARRY_PALM).addScaledVector(handX, -0.022).addScaledVector(up, heldSize.y > 0.16 ? -heldSize.y * 0.08 : -5e-3);
|
|
5793
|
-
if (useItemName === "open" || useItemName === "close") {
|
|
5794
|
-
const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
|
|
5795
|
-
const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
|
|
5796
|
-
const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
|
|
5797
|
-
const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
|
|
5798
|
-
const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
|
|
5799
|
-
return {
|
|
5800
|
-
R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
|
|
5801
|
-
L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
|
|
5802
|
-
grip: 0.85,
|
|
5803
|
-
gripL: 0.8
|
|
5804
|
-
};
|
|
5805
|
-
}
|
|
5806
|
-
return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
|
|
5807
|
-
}
|
|
5808
|
-
const box2 = new THREE.Box3().setFromObject(root);
|
|
5809
|
-
const center = box2.getCenter(new THREE.Vector3());
|
|
5810
|
-
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
|
|
5811
|
-
const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
|
|
5812
|
-
const z = new THREE.Vector3().crossVectors(forward, back).normalize();
|
|
5813
|
-
const y = new THREE.Vector3().crossVectors(z, forward).normalize();
|
|
5814
|
-
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
|
|
5815
|
-
return { R: { position: [center.x, box2.min.y - CARRY_PALM, center.z], quaternion: [q.x, q.y, q.z, q.w] }, grip: CARRY_GRIP };
|
|
6536
|
+
const twistTargets = (root, gripQ) => {
|
|
6537
|
+
if (!twisting) return null;
|
|
6538
|
+
const { stroke, lift } = twisting;
|
|
6539
|
+
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(root.quaternion);
|
|
6540
|
+
const top = heldLocal.clone().applyQuaternion(root.quaternion).add(root.position).addScaledVector(up, heldSize.y * 0.5 + lift * CAP_RISE + stroke.lift * TWIST_LIFT + CARRY_PALM);
|
|
6541
|
+
const knuckles = new THREE.Vector3(0.85, 0, -0.5).normalize().applyQuaternion(gripQ);
|
|
6542
|
+
knuckles.addScaledVector(up, -knuckles.dot(up)).normalize().applyAxisAngle(up, stroke.angle);
|
|
6543
|
+
const side = new THREE.Vector3().crossVectors(knuckles, up).normalize();
|
|
6544
|
+
const palm = top.addScaledVector(knuckles, -0.025);
|
|
6545
|
+
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(knuckles, up, side));
|
|
6546
|
+
return { L: { position: [palm.x, palm.y, palm.z], quaternion: [q.x, q.y, q.z, q.w] }, gripL: 0.35 + 0.5 * stroke.grip };
|
|
5816
6547
|
};
|
|
5817
6548
|
const gripQuat = new THREE.Quaternion();
|
|
5818
6549
|
const followHeld = (dt) => {
|
|
5819
6550
|
const piece = heldPiece;
|
|
5820
|
-
|
|
6551
|
+
twistHand = null;
|
|
5821
6552
|
if (!piece) return;
|
|
5822
6553
|
camera.updateMatrixWorld(true);
|
|
5823
6554
|
const root = piece.root;
|
|
5824
6555
|
let pos;
|
|
5825
6556
|
const targetQ = tmpQ;
|
|
5826
|
-
const hand =
|
|
6557
|
+
const hand = avatar?.hand?.("R");
|
|
5827
6558
|
const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
|
|
5828
6559
|
if (hand) {
|
|
5829
6560
|
const handWorld = hand.getWorldPosition(tmpHand);
|
|
@@ -5839,7 +6570,7 @@ function createTestRoom(options) {
|
|
|
5839
6570
|
pos = tmpV.copy(handWorld).add(gripOffset);
|
|
5840
6571
|
} else if (useItemPlayer) {
|
|
5841
6572
|
const pose = useItemPlayer.pose();
|
|
5842
|
-
const frameQ =
|
|
6573
|
+
const frameQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5843
6574
|
const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
|
|
5844
6575
|
const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
|
|
5845
6576
|
const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
|
|
@@ -5852,19 +6583,10 @@ function createTestRoom(options) {
|
|
|
5852
6583
|
targetQ.copy(frameQ).multiply(eq);
|
|
5853
6584
|
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5854
6585
|
} else {
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
|
|
5858
|
-
const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
|
|
5859
|
-
pos = tmpV.set(holdX, holdY, holdZ).applyMatrix4(camera.matrixWorld);
|
|
5860
|
-
const baseQ = avatar?.firstPerson && small ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5861
|
-
gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
|
|
6586
|
+
pos = tmpV.set(0.05, -0.28, -1.15).applyMatrix4(camera.matrixWorld);
|
|
6587
|
+
gripQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
|
|
5862
6588
|
targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
|
|
5863
|
-
|
|
5864
|
-
targetQ.copy(camera.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5865
|
-
} else {
|
|
5866
|
-
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5867
|
-
}
|
|
6589
|
+
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5868
6590
|
}
|
|
5869
6591
|
const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
|
|
5870
6592
|
const px = root.position.x;
|
|
@@ -5879,8 +6601,50 @@ function createTestRoom(options) {
|
|
|
5879
6601
|
heldVel[2] = (root.position.z - pz) / dt;
|
|
5880
6602
|
}
|
|
5881
6603
|
root.updateMatrixWorld(true);
|
|
5882
|
-
|
|
6604
|
+
twistHand = twistTargets(root, gripQuat);
|
|
6605
|
+
};
|
|
6606
|
+
const TOOL_REACH = 1.6;
|
|
6607
|
+
const toolsAtHand = () => {
|
|
6608
|
+
const here = player.position;
|
|
6609
|
+
const near = [...loadout];
|
|
6610
|
+
for (const other of entries.values()) {
|
|
6611
|
+
if (other === heldEntry || other.arena) continue;
|
|
6612
|
+
const p = other.so.root.position;
|
|
6613
|
+
if (Math.hypot(p.x - here[0], p.z - here[2]) <= TOOL_REACH) near.push(other.so.itemId);
|
|
6614
|
+
}
|
|
6615
|
+
return near;
|
|
6616
|
+
};
|
|
6617
|
+
const sealLoss = (e) => {
|
|
6618
|
+
if (e.sealLoss !== void 0) return e.sealLoss;
|
|
6619
|
+
const c = e.containerId ? fluids.container(e.containerId) : void 0;
|
|
6620
|
+
return c && e.fluidStart !== void 0 ? Math.max(0, e.fluidStart - c.liters - (e.fluidUsed ?? 0)) : 0;
|
|
6621
|
+
};
|
|
6622
|
+
const sealState = (e) => {
|
|
6623
|
+
const breached = world.state(e.so.objectId)?.parts.some((p) => p.holes.some((h) => h.through && !h.vent)) ?? false;
|
|
6624
|
+
const leaked = e.fluidStart !== void 0 && sealLoss(e) > Math.max(2e-3, e.fluidStart * 0.02);
|
|
6625
|
+
return { opened: !!e.opened, breached, leaked };
|
|
6626
|
+
};
|
|
6627
|
+
const openSeal = (e) => {
|
|
6628
|
+
const seal = e.item?.seal;
|
|
6629
|
+
if (!seal || e.opened) return true;
|
|
6630
|
+
const opener = sealOpener(seal, toolsAtHand());
|
|
6631
|
+
if (!opener.ok) {
|
|
6632
|
+
const listed = (seal.openWith ?? []).map((id) => catalogMap.get(id)?.name).filter((n) => !!n);
|
|
6633
|
+
const names = [...new Set(listed.length ? listed : (seal.openWith ?? []).filter((id) => arsenal.weapons.has(id)).map((id) => toolName(id)))];
|
|
6634
|
+
const why = t("hud.needs_opener", { name: e.name, tools: names.join(", ") });
|
|
6635
|
+
say(why);
|
|
6636
|
+
action("sealed", why, { objectId: e.so.objectId, itemId: e.so.itemId, data: { openWith: [...seal.openWith ?? []] } });
|
|
6637
|
+
return false;
|
|
6638
|
+
}
|
|
6639
|
+
e.sealLoss = sealLoss(e);
|
|
6640
|
+
e.opened = true;
|
|
6641
|
+
if (e.containerId) fluids.open(e.containerId);
|
|
6642
|
+
seal.open?.(wrapped.get(e.so.root) ?? e.so.root);
|
|
6643
|
+
const tool2 = opener.tool ? catalogMap.get(opener.tool)?.name ?? (arsenal.weapons.has(opener.tool) ? toolName(opener.tool) : opener.tool) : void 0;
|
|
6644
|
+
action("open_seal", tool2 ? t("act.open_with", { name: e.name, tool: tool2 }) : t("act.open_by_hand", { name: e.name }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { tool: opener.tool ?? "hand" } });
|
|
6645
|
+
return true;
|
|
5883
6646
|
};
|
|
6647
|
+
const crouchOnC = (options.crouch?.keys ?? ["KeyC"]).includes("KeyC");
|
|
5884
6648
|
const useItem = (useId) => {
|
|
5885
6649
|
const e = heldEntry;
|
|
5886
6650
|
if (!e?.containerId) return false;
|
|
@@ -5889,25 +6653,90 @@ function createTestRoom(options) {
|
|
|
5889
6653
|
const c = fluids.container(e.containerId);
|
|
5890
6654
|
if (!c) return false;
|
|
5891
6655
|
if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
|
|
5892
|
-
const
|
|
6656
|
+
const eating = useId === "drink" || useId === "sip" || useId === "chug" || useId === "eat";
|
|
6657
|
+
const consuming = eating || useId === "pour" || useId === "pour_out";
|
|
6658
|
+
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
6659
|
+
const seal = e.item?.seal;
|
|
6660
|
+
if (seal && (consuming || useId === "open")) {
|
|
6661
|
+
const state = sealState(e);
|
|
6662
|
+
const spoiled = sealBlock(seal, { ...state, opened: true });
|
|
6663
|
+
if (eating && spoiled) {
|
|
6664
|
+
say(t(spoiled === "leaked" ? "hud.spoiled_leak" : "hud.spoiled_hole", { name: e.name }));
|
|
6665
|
+
action("spoiled", t(spoiled === "leaked" ? "hud.spoiled_leak" : "hud.spoiled_hole", { name: e.name }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { reason: spoiled } });
|
|
6666
|
+
return false;
|
|
6667
|
+
}
|
|
6668
|
+
if (!e.opened) {
|
|
6669
|
+
const opening = seal.byHand && seal.openUse ? itemUseDefs.find((u) => u.id === seal.openUse) : void 0;
|
|
6670
|
+
if (opening) {
|
|
6671
|
+
useItemPlayer = createItemUse(fluids, e.containerId, opening);
|
|
6672
|
+
useItemName = opening.id;
|
|
6673
|
+
afterOpening = useId === "open" ? null : useId;
|
|
6674
|
+
return true;
|
|
6675
|
+
}
|
|
6676
|
+
if (!openSeal(e)) return false;
|
|
6677
|
+
if (!seal.byHand || useId === "open") {
|
|
6678
|
+
useItemPlayer = createItemUse(fluids, e.containerId, itemUseDefs.find((u) => u.id === "open") ?? def);
|
|
6679
|
+
useItemName = "open";
|
|
6680
|
+
return true;
|
|
6681
|
+
}
|
|
6682
|
+
} else if (useId === "open") return false;
|
|
6683
|
+
}
|
|
5893
6684
|
if (consuming && c.liters <= 1e-6) {
|
|
5894
6685
|
say(t("hud.empty_container", { name: e.name }));
|
|
5895
6686
|
return false;
|
|
5896
6687
|
}
|
|
5897
|
-
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
5898
6688
|
if (consuming && c.closed) fluids.open(e.containerId);
|
|
5899
6689
|
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
5900
6690
|
useItemName = def.id;
|
|
6691
|
+
if (eating) useLiters = c.liters;
|
|
5901
6692
|
action("use_item", t("act.use_item", { name: e.name, use: S.text[`use.${def.id}`] ?? def.id }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { use: def.id } });
|
|
5902
6693
|
return true;
|
|
5903
6694
|
};
|
|
6695
|
+
let useLiters = null;
|
|
5904
6696
|
const updateItemUse = (dt) => {
|
|
5905
6697
|
if (!useItemPlayer) return;
|
|
6698
|
+
const e = heldEntry;
|
|
6699
|
+
const c = e?.containerId ? fluids.container(e.containerId) : void 0;
|
|
6700
|
+
const before = c?.liters;
|
|
5906
6701
|
useItemPlayer.update(dt);
|
|
6702
|
+
const seal = e?.item?.seal;
|
|
6703
|
+
twisting = null;
|
|
6704
|
+
if (e && seal?.openUse === useItemName && !e.opened) {
|
|
6705
|
+
twisting = openingProgress(useItemPlayer.def, useItemPlayer.time);
|
|
6706
|
+
seal.turn?.(wrapped.get(e.so.root) ?? e.so.root, { turned: twisting.stroke.turned, lift: twisting.lift });
|
|
6707
|
+
if (twisting.lift >= 1 || useItemPlayer.done) {
|
|
6708
|
+
openSeal(e);
|
|
6709
|
+
twisting = null;
|
|
6710
|
+
}
|
|
6711
|
+
}
|
|
6712
|
+
if (e && useLiters !== null && before !== void 0) {
|
|
6713
|
+
const after = (e.containerId ? fluids.container(e.containerId)?.liters : void 0) ?? before;
|
|
6714
|
+
e.fluidUsed = (e.fluidUsed ?? 0) + Math.max(0, before - after);
|
|
6715
|
+
if (e.opened && e.fluidStart) e.item?.seal?.contents?.(wrapped.get(e.so.root) ?? e.so.root, after / e.fluidStart);
|
|
6716
|
+
}
|
|
5907
6717
|
if (useItemPlayer.done) {
|
|
6718
|
+
if (e && useLiters !== null && useItemName === "eat") {
|
|
6719
|
+
const left = (e.containerId ? fluids.container(e.containerId)?.liters : void 0) ?? 0;
|
|
6720
|
+
action("ate", t(left <= 1e-4 ? "act.ate_all" : "act.ate", { name: e.name, grams: Math.round((useLiters - left) * 1e3) }), { objectId: e.so.objectId, itemId: e.so.itemId, data: { liters: Math.round((useLiters - left) * 1e3) / 1e3 } });
|
|
6721
|
+
}
|
|
6722
|
+
useLiters = null;
|
|
5908
6723
|
useItemPlayer = null;
|
|
5909
6724
|
useItemName = "";
|
|
5910
|
-
|
|
6725
|
+
const next = afterOpening;
|
|
6726
|
+
afterOpening = null;
|
|
6727
|
+
if (next && e?.opened) useItem(next);
|
|
6728
|
+
}
|
|
6729
|
+
};
|
|
6730
|
+
const openingProgress = (def, time) => {
|
|
6731
|
+
const keys = [...def.keys].sort((a, b) => a.t - b.t);
|
|
6732
|
+
const turning = keys.findIndex((k) => k.action === "turn");
|
|
6733
|
+
const lastTurn = keys.map((k) => k.action).lastIndexOf("turn");
|
|
6734
|
+
const start = turning >= 0 ? keys[turning].t : 0;
|
|
6735
|
+
const end = lastTurn >= 0 ? keys[lastTurn + 1]?.t ?? def.duration : start;
|
|
6736
|
+
const openAt = keys.find((k) => k.action === "open")?.t ?? def.duration;
|
|
6737
|
+
const share = end > start ? (time - start) / (end - start) : 1;
|
|
6738
|
+
const lift = time <= end ? 0 : openAt > end ? Math.min(1, (time - end) / (openAt - end)) : 1;
|
|
6739
|
+
return { stroke: twistStroke(share), lift };
|
|
5911
6740
|
};
|
|
5912
6741
|
const TAP_ID = "room_tap";
|
|
5913
6742
|
const TAP_RATE = 0.25;
|
|
@@ -5986,7 +6815,7 @@ function createTestRoom(options) {
|
|
|
5986
6815
|
if (code === "KeyZ") tiltKey = 0;
|
|
5987
6816
|
if (code === "KeyE" && pushByKey) {
|
|
5988
6817
|
pushByKey = false;
|
|
5989
|
-
if (push.end()) grab();
|
|
6818
|
+
if (push.end() && !doors.use()) grab();
|
|
5990
6819
|
}
|
|
5991
6820
|
return;
|
|
5992
6821
|
}
|
|
@@ -6006,7 +6835,7 @@ function createTestRoom(options) {
|
|
|
6006
6835
|
case "KeyE":
|
|
6007
6836
|
if (e.repeat) break;
|
|
6008
6837
|
pushByKey = !heldPiece && push.begin();
|
|
6009
|
-
if (!pushByKey) grab();
|
|
6838
|
+
if (!pushByKey && (heldPiece || !doors.use())) grab();
|
|
6010
6839
|
break;
|
|
6011
6840
|
case "KeyG":
|
|
6012
6841
|
throwHeld();
|
|
@@ -6032,11 +6861,13 @@ function createTestRoom(options) {
|
|
|
6032
6861
|
case "KeyZ":
|
|
6033
6862
|
tiltKey = 1;
|
|
6034
6863
|
break;
|
|
6035
|
-
case "KeyF":
|
|
6036
|
-
|
|
6864
|
+
case "KeyF": {
|
|
6865
|
+
const use = heldEntry?.uses[e.shiftKey && crouchOnC ? 1 : 0];
|
|
6866
|
+
if (use) useItem(use);
|
|
6037
6867
|
break;
|
|
6868
|
+
}
|
|
6038
6869
|
case "KeyC":
|
|
6039
|
-
if (heldEntry?.uses[1] && !
|
|
6870
|
+
if (heldEntry?.uses[1] && !crouchOnC) useItem(heldEntry.uses[1]);
|
|
6040
6871
|
break;
|
|
6041
6872
|
case "KeyV":
|
|
6042
6873
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
@@ -6081,6 +6912,12 @@ function createTestRoom(options) {
|
|
|
6081
6912
|
aim();
|
|
6082
6913
|
return rayWorld(aimO, aimD, 14);
|
|
6083
6914
|
};
|
|
6915
|
+
const leafNote = (distance) => {
|
|
6916
|
+
if (distance > DOOR_REACH) return void 0;
|
|
6917
|
+
const leaf = doors.aimed();
|
|
6918
|
+
if (!leaf) return void 0;
|
|
6919
|
+
return t(leaf.locked ? "hud.door_hint_locked" : leaf.open ? "hud.door_hint_close" : "hud.door_hint_open");
|
|
6920
|
+
};
|
|
6084
6921
|
const inspectInfo = () => {
|
|
6085
6922
|
const r = inspectTarget();
|
|
6086
6923
|
if (!r) return null;
|
|
@@ -6116,7 +6953,7 @@ function createTestRoom(options) {
|
|
|
6116
6953
|
load: ext.load,
|
|
6117
6954
|
stress: ext.stress,
|
|
6118
6955
|
joints,
|
|
6119
|
-
note: e?.item?.note,
|
|
6956
|
+
note: leafNote(r.distance) ?? e?.item?.note,
|
|
6120
6957
|
thumbnail: e?.item?.thumbnail,
|
|
6121
6958
|
thumbnailCredit: e?.item?.thumbnailCredit,
|
|
6122
6959
|
distance: r.distance
|
|
@@ -6180,6 +7017,7 @@ function createTestRoom(options) {
|
|
|
6180
7017
|
}
|
|
6181
7018
|
syncLiters();
|
|
6182
7019
|
renderer.update(dt);
|
|
7020
|
+
for (const e of entries.values()) e.follower?.update?.();
|
|
6183
7021
|
terrainView.sync(camera.position);
|
|
6184
7022
|
showGroundFire(dt);
|
|
6185
7023
|
vfx.update(dt);
|
|
@@ -6207,12 +7045,14 @@ function createTestRoom(options) {
|
|
|
6207
7045
|
const update = (dt) => {
|
|
6208
7046
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
6209
7047
|
frameStamp++;
|
|
7048
|
+
seating.update();
|
|
6210
7049
|
player.update(dt);
|
|
6211
7050
|
if (pushByKey && !player.keys.has("KeyE")) {
|
|
6212
7051
|
pushByKey = false;
|
|
6213
7052
|
push.end();
|
|
6214
7053
|
}
|
|
6215
7054
|
push.step(paused ? 0 : dt);
|
|
7055
|
+
doors.step(paused ? 0 : dt);
|
|
6216
7056
|
if (!paused) kickLight(dt);
|
|
6217
7057
|
if (!paused) pushByPlayer(dt);
|
|
6218
7058
|
else player.takeWish();
|
|
@@ -6228,7 +7068,6 @@ function createTestRoom(options) {
|
|
|
6228
7068
|
}
|
|
6229
7069
|
if (!paused) followHeld(dt);
|
|
6230
7070
|
stepSim(dt * timeScale);
|
|
6231
|
-
stepFirstPerson(paused ? 0 : dt);
|
|
6232
7071
|
reachAvatar();
|
|
6233
7072
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
6234
7073
|
hudAcc += dt;
|
|
@@ -6236,6 +7075,7 @@ function createTestRoom(options) {
|
|
|
6236
7075
|
hudAcc = 0;
|
|
6237
7076
|
ui.hud(hudState());
|
|
6238
7077
|
}
|
|
7078
|
+
ui?.flushLog();
|
|
6239
7079
|
metricsAcc += dt;
|
|
6240
7080
|
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
6241
7081
|
metricsAcc = 0;
|
|
@@ -6256,7 +7096,7 @@ function createTestRoom(options) {
|
|
|
6256
7096
|
pages: Math.max(1, Math.ceil(loadout.length / 10)),
|
|
6257
7097
|
status: simTime - statusAt < 2 ? statusLine : "",
|
|
6258
7098
|
progress: progress ? { label: progress.label, value: progress.value } : null,
|
|
6259
|
-
held: heldEntry || heldPiece ? { name: heldName, uses: (heldEntry?.uses ?? []).map((u, i) => ({ key: ["F", "C", "V"][i] ?? "", label: S.text[`use.${u}`] ?? u })), weapon: heldEntry?.item?.weapon ? arsenal.name(heldEntry.item.weapon) : void 0, using: useItemName } : null,
|
|
7099
|
+
held: heldEntry || heldPiece ? { name: heldName, uses: (heldEntry?.uses ?? []).map((u, i) => ({ key: ["F", crouchOnC ? "Shift+F" : "C", "V"][i] ?? "", label: S.text[`use.${u}`] ?? u })), weapon: heldEntry?.item?.weapon ? arsenal.name(heldEntry.item.weapon) : void 0, using: useItemName } : null,
|
|
6260
7100
|
inspect: inspectInfo(),
|
|
6261
7101
|
animations: animationsOf(),
|
|
6262
7102
|
locked: player.locked,
|
|
@@ -6297,17 +7137,33 @@ function createTestRoom(options) {
|
|
|
6297
7137
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
6298
7138
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
6299
7139
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
7140
|
+
if (id !== "") followPart(pl.entry, id, obj, rest);
|
|
7141
|
+
}
|
|
7142
|
+
};
|
|
7143
|
+
const fpRest = new THREE.Matrix4();
|
|
7144
|
+
const fpNow = new THREE.Matrix4();
|
|
7145
|
+
const fpRoot = new THREE.Matrix4();
|
|
7146
|
+
const fpPart = new THREE.Matrix4();
|
|
7147
|
+
const fpPos = new THREE.Vector3();
|
|
7148
|
+
const fpQuat = new THREE.Quaternion();
|
|
7149
|
+
const fpScale = new THREE.Vector3();
|
|
7150
|
+
const fpEuler = new THREE.Euler();
|
|
7151
|
+
const followPart = (e, id, obj, rest) => {
|
|
7152
|
+
const ps = specs.get(e.so.objectId)?.parts.find((p) => p.id === id);
|
|
7153
|
+
if (!ps || !obj.parent) return;
|
|
7154
|
+
const root = e.so.root;
|
|
7155
|
+
root.updateMatrixWorld(true);
|
|
7156
|
+
fpRoot.copy(root.matrixWorld).invert();
|
|
7157
|
+
fpNow.multiplyMatrices(fpRoot, obj.matrixWorld);
|
|
7158
|
+
fpEuler.set(rest.r[0], rest.r[1], rest.r[2]);
|
|
7159
|
+
fpRest.compose(fpPos.set(rest.p[0], rest.p[1], rest.p[2]), fpQuat.setFromEuler(fpEuler), obj.scale);
|
|
7160
|
+
fpRest.premultiply(obj.parent.matrixWorld).premultiply(fpRoot);
|
|
7161
|
+
fpNow.multiply(fpRest.invert());
|
|
7162
|
+
const r0 = ps.rotation ?? ZERO;
|
|
7163
|
+
fpPart.compose(fpPos.set(ps.position[0], ps.position[1], ps.position[2]), fpQuat.setFromEuler(fpEuler.set(r0[0], r0[1], r0[2])), fpScale.set(1, 1, 1));
|
|
7164
|
+
fpPart.premultiply(fpNow).decompose(fpPos, fpQuat, fpScale);
|
|
7165
|
+
fpEuler.setFromQuaternion(fpQuat);
|
|
7166
|
+
world.setPartPose(e.so.objectId, id, [fpPos.x, fpPos.y, fpPos.z], [fpEuler.x, fpEuler.y, fpEuler.z]);
|
|
6311
7167
|
};
|
|
6312
7168
|
const updateAnimations = (dt) => {
|
|
6313
7169
|
for (let i = playing.length - 1; i >= 0; i--) {
|
|
@@ -6318,8 +7174,8 @@ function createTestRoom(options) {
|
|
|
6318
7174
|
while (pl.fired < evs.length && evs[pl.fired].t <= pl.t) {
|
|
6319
7175
|
const ev = evs[pl.fired++];
|
|
6320
7176
|
action("animation_event", t("act.anim_event", { name: pl.entry.name, clip: pl.clip.id, type: ev.type }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id, type: ev.type, at: ev.t, ...ev.data ?? {} } });
|
|
6321
|
-
const
|
|
6322
|
-
if (typeof
|
|
7177
|
+
const key2 = ev.type === "sound" ? ev.data?.key : void 0;
|
|
7178
|
+
if (typeof key2 === "string") playSound(key2, v3(pl.entry.so.root.getWorldPosition(tmpV)), 1);
|
|
6323
7179
|
}
|
|
6324
7180
|
if (pl.t >= pl.clip.duration) {
|
|
6325
7181
|
playing.splice(i, 1);
|
|
@@ -6341,10 +7197,10 @@ function createTestRoom(options) {
|
|
|
6341
7197
|
const targets = /* @__PURE__ */ new Map();
|
|
6342
7198
|
const ids = /* @__PURE__ */ new Set();
|
|
6343
7199
|
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) ids.add(id);
|
|
6344
|
-
const grab2 = (
|
|
7200
|
+
const grab2 = (key2, obj) => {
|
|
6345
7201
|
if (!obj) return;
|
|
6346
7202
|
if (!restPose.has(obj)) restPose.set(obj, { p: [obj.position.x, obj.position.y, obj.position.z], r: [obj.rotation.x, obj.rotation.y, obj.rotation.z] });
|
|
6347
|
-
targets.set(
|
|
7203
|
+
targets.set(key2, obj);
|
|
6348
7204
|
};
|
|
6349
7205
|
if (clip.keys.some((k) => k.position || k.rotation)) grab2("", e.so.root);
|
|
6350
7206
|
for (const id of ids) grab2(id, e.so.root.getObjectByName(id) ?? void 0);
|
|
@@ -6371,9 +7227,9 @@ function createTestRoom(options) {
|
|
|
6371
7227
|
const METRIC_UNAVAILABLE = "n/a";
|
|
6372
7228
|
const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
|
|
6373
7229
|
const reportedFailures = /* @__PURE__ */ new Set();
|
|
6374
|
-
const reportOnce = (
|
|
6375
|
-
if (reportedFailures.has(
|
|
6376
|
-
reportedFailures.add(
|
|
7230
|
+
const reportOnce = (key2, text) => {
|
|
7231
|
+
if (reportedFailures.has(key2)) return;
|
|
7232
|
+
reportedFailures.add(key2);
|
|
6377
7233
|
host.log(text);
|
|
6378
7234
|
};
|
|
6379
7235
|
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -6523,6 +7379,14 @@ function createTestRoom(options) {
|
|
|
6523
7379
|
frameListeners.add(update2);
|
|
6524
7380
|
return () => frameListeners.delete(update2);
|
|
6525
7381
|
},
|
|
7382
|
+
holdFrame: () => bodyHoldFrame(),
|
|
7383
|
+
reachFor(reach) {
|
|
7384
|
+
if (!avatar) {
|
|
7385
|
+
reach.onGrip?.();
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
doors.reachFor(reach);
|
|
7389
|
+
},
|
|
6526
7390
|
setHandTargets(source) {
|
|
6527
7391
|
handSource = source;
|
|
6528
7392
|
return () => {
|
|
@@ -6540,6 +7404,10 @@ function createTestRoom(options) {
|
|
|
6540
7404
|
clearListeners.add(listener);
|
|
6541
7405
|
return () => clearListeners.delete(listener);
|
|
6542
7406
|
},
|
|
7407
|
+
onReset(listener) {
|
|
7408
|
+
resetListeners.add(listener);
|
|
7409
|
+
return () => resetListeners.delete(listener);
|
|
7410
|
+
},
|
|
6543
7411
|
addObject(itemId, place) {
|
|
6544
7412
|
return addScenery(itemId, place.position, place.rotationY);
|
|
6545
7413
|
}
|
|
@@ -6567,13 +7435,32 @@ function createTestRoom(options) {
|
|
|
6567
7435
|
return r.ok ? store : null;
|
|
6568
7436
|
};
|
|
6569
7437
|
const LIGHTS_CHECK_MS = 250;
|
|
7438
|
+
const pinnedPrograms = /* @__PURE__ */ new WeakSet();
|
|
7439
|
+
const pinPrograms = () => {
|
|
7440
|
+
for (const program of gl.info?.programs ?? []) {
|
|
7441
|
+
if (pinnedPrograms.has(program)) continue;
|
|
7442
|
+
pinnedPrograms.add(program);
|
|
7443
|
+
program.usedTimes++;
|
|
7444
|
+
}
|
|
7445
|
+
};
|
|
6570
7446
|
const warmUp = () => {
|
|
6571
7447
|
const probes = [renderer.warmUpProbes(), vfx.warmUpProbes(), fluidView.warmUpProbes()];
|
|
6572
7448
|
const groups = probes.map((probe) => probe.group);
|
|
7449
|
+
const spriteMap = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
|
|
7450
|
+
spriteMap.needsUpdate = true;
|
|
7451
|
+
const spriteProbes = [true, false].map((fog) => {
|
|
7452
|
+
const probe = new THREE.Sprite(new THREE.SpriteMaterial({ map: spriteMap, transparent: true, fog }));
|
|
7453
|
+
probe.frustumCulled = false;
|
|
7454
|
+
return probe;
|
|
7455
|
+
});
|
|
7456
|
+
groups.push(...spriteProbes);
|
|
6573
7457
|
scene.add(...groups);
|
|
6574
7458
|
gl.render(scene, camera);
|
|
7459
|
+
pinPrograms();
|
|
6575
7460
|
scene.remove(...groups);
|
|
6576
7461
|
for (const probe of probes) probe.dispose();
|
|
7462
|
+
for (const probe of spriteProbes) probe.material.dispose();
|
|
7463
|
+
spriteMap.dispose();
|
|
6577
7464
|
};
|
|
6578
7465
|
const warmUpEffects = () => {
|
|
6579
7466
|
const ids = vfx.catalog?.() ?? [];
|
|
@@ -6604,11 +7491,14 @@ function createTestRoom(options) {
|
|
|
6604
7491
|
build: (T) => ti.build(T),
|
|
6605
7492
|
spec: ti.spec,
|
|
6606
7493
|
movable: ti.movable,
|
|
7494
|
+
pushable: ti.pushable,
|
|
6607
7495
|
slow: ti.slow,
|
|
6608
7496
|
fluid: ti.fluid,
|
|
7497
|
+
seal: ti.seal,
|
|
6609
7498
|
weapon: ti.weapon,
|
|
6610
7499
|
impl: ti.impl,
|
|
6611
7500
|
animations: ti.animations,
|
|
7501
|
+
seats: ti.seats,
|
|
6612
7502
|
thumbnail: ti.thumbnail,
|
|
6613
7503
|
thumbnailCredit: ti.thumbnailCredit
|
|
6614
7504
|
};
|
|
@@ -6665,15 +7555,20 @@ function createTestRoom(options) {
|
|
|
6665
7555
|
const kept = previous?.encode();
|
|
6666
7556
|
previous?.dispose();
|
|
6667
7557
|
benches.delete(mod.id);
|
|
6668
|
-
for (const
|
|
7558
|
+
for (const key2 of [...reportedFailures]) if (key2.startsWith(`${mod.id}:`)) reportedFailures.delete(key2);
|
|
6669
7559
|
modules.set(mod.id, mod);
|
|
6670
7560
|
if (bench) {
|
|
6671
7561
|
const seed = options.bench?.state?.[mod.id];
|
|
6672
7562
|
const restored = kept ?? (typeof seed === "string" ? seed : void 0);
|
|
6673
7563
|
if (restored !== void 0) bench.restore(restored);
|
|
6674
7564
|
benches.set(mod.id, bench);
|
|
7565
|
+
let reportedState = bench.encode();
|
|
6675
7566
|
bench.watch(() => {
|
|
6676
|
-
|
|
7567
|
+
const encoded = bench.encode();
|
|
7568
|
+
if (encoded !== reportedState) {
|
|
7569
|
+
reportedState = encoded;
|
|
7570
|
+
options.bench?.onChange?.(mod.id, encoded);
|
|
7571
|
+
}
|
|
6677
7572
|
if (activeBench === mod.id) ui?.bench(benchView());
|
|
6678
7573
|
});
|
|
6679
7574
|
if (!activeBench || !benches.has(activeBench)) activeBench = mod.id;
|
|
@@ -6695,8 +7590,10 @@ function createTestRoom(options) {
|
|
|
6695
7590
|
const dt = seconds > 6 ? 1 / 30 : FIXED_DT;
|
|
6696
7591
|
const n = Math.max(1, Math.round(seconds / dt));
|
|
6697
7592
|
for (let i = 0; i < n; i++) {
|
|
7593
|
+
seating.update();
|
|
6698
7594
|
player.update(0);
|
|
6699
7595
|
push.step(dt);
|
|
7596
|
+
doors.step(dt);
|
|
6700
7597
|
followHeld(dt);
|
|
6701
7598
|
stepSim(dt);
|
|
6702
7599
|
}
|
|
@@ -6831,7 +7728,7 @@ function createTestRoom(options) {
|
|
|
6831
7728
|
room.waitUntilReady();
|
|
6832
7729
|
return true;
|
|
6833
7730
|
},
|
|
6834
|
-
look: (eye, target) =>
|
|
7731
|
+
look: (eye, target) => lookAt(eye, target),
|
|
6835
7732
|
use: (hold) => room.use(hold),
|
|
6836
7733
|
blastAt: (weapon, point) => {
|
|
6837
7734
|
const def = arsenal.weapons.get(weapon);
|
|
@@ -6887,7 +7784,7 @@ function createTestRoom(options) {
|
|
|
6887
7784
|
grab,
|
|
6888
7785
|
throwHeld,
|
|
6889
7786
|
look(position, target) {
|
|
6890
|
-
|
|
7787
|
+
lookAt(position, target);
|
|
6891
7788
|
},
|
|
6892
7789
|
inspect() {
|
|
6893
7790
|
const r = inspectTarget();
|
|
@@ -6954,11 +7851,23 @@ function createTestRoom(options) {
|
|
|
6954
7851
|
busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
|
|
6955
7852
|
loadModule,
|
|
6956
7853
|
warmUp: () => {
|
|
7854
|
+
audio?.warmUp();
|
|
6957
7855
|
warmUp();
|
|
6958
7856
|
warmUpEffects();
|
|
6959
7857
|
},
|
|
6960
7858
|
animations: animationsOf,
|
|
6961
7859
|
playAnimation,
|
|
7860
|
+
useLeaf: (objectId, part) => doors.useLeaf(objectId, part),
|
|
7861
|
+
leafOpening: (objectId, part) => doors.opening(objectId, part ?? leavesOf(objectId)[0]?.part ?? ""),
|
|
7862
|
+
leaves: (objectId) => leavesOf(objectId).map((l) => l.part),
|
|
7863
|
+
lockLeaf(objectId, locked = true, part) {
|
|
7864
|
+
const k = `${objectId}\0${part ?? "*"}`;
|
|
7865
|
+
if (locked) lockedLeaves.add(k);
|
|
7866
|
+
else lockedLeaves.delete(k);
|
|
7867
|
+
},
|
|
7868
|
+
get door() {
|
|
7869
|
+
return doors.state;
|
|
7870
|
+
},
|
|
6962
7871
|
bench(moduleId) {
|
|
6963
7872
|
const id = moduleId ?? activeBench;
|
|
6964
7873
|
return (id ? benches.get(id) : void 0) ?? null;
|
|
@@ -7001,6 +7910,11 @@ function createTestRoom(options) {
|
|
|
7001
7910
|
get playerPosition() {
|
|
7002
7911
|
return player.position;
|
|
7003
7912
|
},
|
|
7913
|
+
sit: (objectId, seatId) => seating.sit(objectId, seatId),
|
|
7914
|
+
stand: () => seating.stand(),
|
|
7915
|
+
get seat() {
|
|
7916
|
+
return seating.seat;
|
|
7917
|
+
},
|
|
7004
7918
|
push: { begin: () => push.begin(), end: () => push.end(), setWalk: (walk) => push.setWalk(walk), get state() {
|
|
7005
7919
|
return push.state;
|
|
7006
7920
|
} },
|
|
@@ -7103,8 +8017,8 @@ function createTestRoom(options) {
|
|
|
7103
8017
|
},
|
|
7104
8018
|
onTap: () => toggleTap(),
|
|
7105
8019
|
...combo ? { combo: { label: combo.name?.[locale] ?? combo.id, onToggle: () => setStrikeCombo(!comboOn) } } : {},
|
|
7106
|
-
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((
|
|
7107
|
-
onBlast: (
|
|
8020
|
+
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((key2) => key2.replace(/^Key/, "")).join("/")})`, onKick: () => void kick() } } : {},
|
|
8021
|
+
onBlast: (key2) => toggleBlast(key2),
|
|
7108
8022
|
onPlay: (objectId, clipId) => {
|
|
7109
8023
|
playAnimation(clipId, objectId);
|
|
7110
8024
|
},
|
|
@@ -7130,19 +8044,19 @@ function createTestRoom(options) {
|
|
|
7130
8044
|
let nextLightsCheck = 0;
|
|
7131
8045
|
let warmedLights = "";
|
|
7132
8046
|
const lightsKey = () => {
|
|
7133
|
-
let
|
|
8047
|
+
let key2 = "";
|
|
7134
8048
|
scene.traverseVisible((object) => {
|
|
7135
|
-
if (object.isLight)
|
|
8049
|
+
if (object.isLight) key2 += `${object.type[0]}${object.castShadow ? "s" : ""}`;
|
|
7136
8050
|
});
|
|
7137
|
-
return
|
|
8051
|
+
return key2;
|
|
7138
8052
|
};
|
|
7139
8053
|
const loop = (now) => {
|
|
7140
8054
|
raf = requestAnimationFrame(loop);
|
|
7141
8055
|
if (now >= nextLightsCheck) {
|
|
7142
8056
|
nextLightsCheck = now + LIGHTS_CHECK_MS;
|
|
7143
|
-
const
|
|
7144
|
-
if (
|
|
7145
|
-
warmedLights =
|
|
8057
|
+
const key2 = lightsKey();
|
|
8058
|
+
if (key2 !== warmedLights) {
|
|
8059
|
+
warmedLights = key2;
|
|
7146
8060
|
warmUp();
|
|
7147
8061
|
}
|
|
7148
8062
|
}
|
|
@@ -7153,6 +8067,7 @@ function createTestRoom(options) {
|
|
|
7153
8067
|
if (!scenarioRunning) update(dt);
|
|
7154
8068
|
const restoreCamera = room.applyCamera();
|
|
7155
8069
|
gl.render(scene, debugView ?? camera);
|
|
8070
|
+
pinPrograms();
|
|
7156
8071
|
restoreCamera();
|
|
7157
8072
|
roomMetrics.rendered();
|
|
7158
8073
|
};
|