@adatechnology/game-test-room 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/index.d.ts +127 -12
- package/dist/index.global.js +2587 -612
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1090 -199
- package/dist/index.js.map +1 -1
- package/package.json +13 -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
|
};
|
|
@@ -569,8 +569,8 @@ function createExplosives(context) {
|
|
|
569
569
|
age: 0,
|
|
570
570
|
...use.explosive !== void 0 ? { body: createExplosiveBody(use, arsenal.explosives, arsenal.rules.explosives), aligned: isRocket } : {}
|
|
571
571
|
});
|
|
572
|
-
const
|
|
573
|
-
action(isRocket ? "launch_projectile" : "throw_projectile", translate(
|
|
572
|
+
const key2 = isRocket ? "act.launch" : isMolotov ? "act.throw.molotov" : "act.throw.grenade";
|
|
573
|
+
action(isRocket ? "launch_projectile" : "throw_projectile", translate(key2, { weapon: context.toolName(), fuse: format(use.fuse, 1), arm: format(use.armDistance ?? 0, 0) }), {
|
|
574
574
|
data: { weapon: context.toolId(), fuse: use.fuse, explosive: use.explosive }
|
|
575
575
|
});
|
|
576
576
|
};
|
|
@@ -1229,9 +1229,9 @@ function createLog(options = {}) {
|
|
|
1229
1229
|
};
|
|
1230
1230
|
const log = {
|
|
1231
1231
|
add,
|
|
1232
|
-
coalesce(
|
|
1232
|
+
coalesce(key2, entry, windowS = 1) {
|
|
1233
1233
|
const now = entry.t ?? clock();
|
|
1234
|
-
const k = keyed.get(
|
|
1234
|
+
const k = keyed.get(key2);
|
|
1235
1235
|
if (k && now - k.last <= windowS && list.includes(k.entry)) {
|
|
1236
1236
|
const { t: _t, ...rest } = entry;
|
|
1237
1237
|
k.count++;
|
|
@@ -1242,7 +1242,7 @@ function createLog(options = {}) {
|
|
|
1242
1242
|
return k.entry;
|
|
1243
1243
|
}
|
|
1244
1244
|
const e = add(entry);
|
|
1245
|
-
keyed.set(
|
|
1245
|
+
keyed.set(key2, { entry: e, last: now, count: 1 });
|
|
1246
1246
|
return e;
|
|
1247
1247
|
},
|
|
1248
1248
|
setBeforeRead(fn) {
|
|
@@ -1378,7 +1378,7 @@ function createNarrator(c) {
|
|
|
1378
1378
|
if (p) materialMemo.set(`${objectId}|${partId}`, p.material);
|
|
1379
1379
|
return p;
|
|
1380
1380
|
};
|
|
1381
|
-
const plural = (
|
|
1381
|
+
const plural = (key2, n) => t(`${key2}.${n === 1 ? "one" : "other"}`, { n });
|
|
1382
1382
|
const pieces = (n) => plural("count.piece", n);
|
|
1383
1383
|
const joints = (n) => plural("count.joint", n);
|
|
1384
1384
|
const xz = (x, z) => t("coords", { x: num(x, 1), z: num(z, 1) });
|
|
@@ -1442,18 +1442,18 @@ function createNarrator(c) {
|
|
|
1442
1442
|
if (ev.by === "part_lost") {
|
|
1443
1443
|
const [a, b] = ev.parts;
|
|
1444
1444
|
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(
|
|
1445
|
+
const key2 = `${ev.object}|${lost}`;
|
|
1446
|
+
const g = lostJoints.get(key2) ?? { obj: ev.object, part: lost, joints: 0, groups: 0, merged: false };
|
|
1447
1447
|
g.joints++;
|
|
1448
1448
|
g.groups = ev.groups.length;
|
|
1449
|
-
lostJoints.set(
|
|
1449
|
+
lostJoints.set(key2, g);
|
|
1450
1450
|
} else {
|
|
1451
|
-
const
|
|
1452
|
-
const g = openJoints.get(
|
|
1451
|
+
const key2 = `${ev.object}|${ev.by}`;
|
|
1452
|
+
const g = openJoints.get(key2) ?? { obj: ev.object, by: ev.by, list: [], groups: 0, done: false };
|
|
1453
1453
|
const [a, b] = ev.parts;
|
|
1454
1454
|
g.list.push({ joint: ev.joint, a, b });
|
|
1455
1455
|
g.groups = ev.groups.length;
|
|
1456
|
-
openJoints.set(
|
|
1456
|
+
openJoints.set(key2, g);
|
|
1457
1457
|
}
|
|
1458
1458
|
} else if (ev.type === "ignite") {
|
|
1459
1459
|
const g = fireParts.get(ev.object) ?? { obj: ev.object, parts: [], temp: 0, done: false };
|
|
@@ -1466,14 +1466,14 @@ function createNarrator(c) {
|
|
|
1466
1466
|
outParts.set(ev.object, g);
|
|
1467
1467
|
}
|
|
1468
1468
|
}
|
|
1469
|
-
const lostTail = (
|
|
1470
|
-
const g = lostJoints.get(
|
|
1469
|
+
const lostTail = (key2) => {
|
|
1470
|
+
const g = lostJoints.get(key2);
|
|
1471
1471
|
if (!g) return "";
|
|
1472
1472
|
g.merged = true;
|
|
1473
1473
|
return t("ev.lost.tail", { joints: joints(g.joints), pieces: pieces(g.groups) });
|
|
1474
1474
|
};
|
|
1475
|
-
const lostData = (
|
|
1476
|
-
const g = lostJoints.get(
|
|
1475
|
+
const lostData = (key2) => {
|
|
1476
|
+
const g = lostJoints.get(key2);
|
|
1477
1477
|
return g ? { jointsOpened: g.joints, pieces: g.groups } : {};
|
|
1478
1478
|
};
|
|
1479
1479
|
for (const ev of batch) {
|
|
@@ -1597,8 +1597,8 @@ function createNarrator(c) {
|
|
|
1597
1597
|
if (ev.by === "part_lost") {
|
|
1598
1598
|
const [a2, b2] = ev.parts;
|
|
1599
1599
|
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(
|
|
1600
|
+
const key3 = `${ev.object}|${lost}`;
|
|
1601
|
+
const g2 = lostJoints.get(key3);
|
|
1602
1602
|
if (!g2 || g2.merged) break;
|
|
1603
1603
|
const hasOwnLine = batch.some((e) => (e.type === "fracture" || e.type === "burnout") && e.object === ev.object && e.part === lost);
|
|
1604
1604
|
if (hasOwnLine) break;
|
|
@@ -1609,12 +1609,12 @@ function createNarrator(c) {
|
|
|
1609
1609
|
...base(ev, lost),
|
|
1610
1610
|
type: "detach",
|
|
1611
1611
|
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(
|
|
1612
|
+
data: { parts: [a2, b2], by: ev.by, ...lostData(key3) }
|
|
1613
1613
|
});
|
|
1614
1614
|
break;
|
|
1615
1615
|
}
|
|
1616
|
-
const
|
|
1617
|
-
const g = openJoints.get(
|
|
1616
|
+
const key2 = `${ev.object}|${ev.by}`;
|
|
1617
|
+
const g = openJoints.get(key2);
|
|
1618
1618
|
if (!g || g.done) break;
|
|
1619
1619
|
g.done = true;
|
|
1620
1620
|
const [a, b] = ev.parts;
|
|
@@ -1767,13 +1767,13 @@ function createNarrator(c) {
|
|
|
1767
1767
|
};
|
|
1768
1768
|
const kindMemo = /* @__PURE__ */ new Map();
|
|
1769
1769
|
const kindOf = (objectId, jointId) => {
|
|
1770
|
-
const
|
|
1770
|
+
const key2 = `${objectId}|${jointId}`;
|
|
1771
1771
|
const own = c.world.state(objectId)?.joints.find((j) => j.id === jointId)?.kind;
|
|
1772
1772
|
if (own) {
|
|
1773
|
-
kindMemo.set(
|
|
1773
|
+
kindMemo.set(key2, own);
|
|
1774
1774
|
return own;
|
|
1775
1775
|
}
|
|
1776
|
-
const known = kindMemo.get(
|
|
1776
|
+
const known = kindMemo.get(key2);
|
|
1777
1777
|
if (known) return known;
|
|
1778
1778
|
for (const id of c.world.ids()) {
|
|
1779
1779
|
const k = c.world.state(id)?.joints.find((j) => j.id === jointId)?.kind;
|
|
@@ -1809,6 +1809,7 @@ function createNarrator(c) {
|
|
|
1809
1809
|
break;
|
|
1810
1810
|
}
|
|
1811
1811
|
case "drink": {
|
|
1812
|
+
if (ev.eaten) break;
|
|
1812
1813
|
const now = c.now();
|
|
1813
1814
|
let acc = drinkAcc.get(ev.container);
|
|
1814
1815
|
if (!acc || now - acc.last > 0.5) acc = { liters: 0, thirst: 0, burn: false, effects: /* @__PURE__ */ new Set(), last: now, start: now };
|
|
@@ -1842,12 +1843,12 @@ function createNarrator(c) {
|
|
|
1842
1843
|
const surface = t(`surface.${ev.surface}`);
|
|
1843
1844
|
const liquid = c.liquidName(ev.liquid);
|
|
1844
1845
|
stainInfo.set(ev.stain, { liquid: ev.liquid, surface, x: ev.point[0], z: ev.point[2] });
|
|
1845
|
-
const
|
|
1846
|
-
const acc = stainAcc.get(
|
|
1846
|
+
const key2 = `stain:${ev.liquid}:${ev.surface}`;
|
|
1847
|
+
const acc = stainAcc.get(key2);
|
|
1847
1848
|
const n = acc && c.now() - acc.last <= 1 ? acc.n + 1 : 1;
|
|
1848
|
-
stainAcc.set(
|
|
1849
|
+
stainAcc.set(key2, { n, last: c.now() });
|
|
1849
1850
|
c.log.coalesce(
|
|
1850
|
-
|
|
1851
|
+
key2,
|
|
1851
1852
|
{
|
|
1852
1853
|
channel: "event",
|
|
1853
1854
|
type: f + "stain",
|
|
@@ -1860,13 +1861,13 @@ function createNarrator(c) {
|
|
|
1860
1861
|
}
|
|
1861
1862
|
case "dried": {
|
|
1862
1863
|
const info = stainInfo.get(ev.stain);
|
|
1863
|
-
const
|
|
1864
|
-
const acc = driedAcc.get(
|
|
1864
|
+
const key2 = `dried:${info?.liquid ?? "?"}:${ev.mark}`;
|
|
1865
|
+
const acc = driedAcc.get(key2);
|
|
1865
1866
|
const n = acc && c.now() - acc.last <= 1.5 ? acc.n + 1 : 1;
|
|
1866
|
-
driedAcc.set(
|
|
1867
|
+
driedAcc.set(key2, { n, last: c.now() });
|
|
1867
1868
|
const vars = { liquid: info ? c.liquidName(info.liquid) : "", surface: info?.surface ?? "", where: info ? xz(info.x, info.z) : "", n };
|
|
1868
1869
|
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(
|
|
1870
|
+
c.log.coalesce(key2, { channel: "event", type: f + "dried", text, data: { stain: ev.stain, mark: ev.mark, count: n } }, 1.5);
|
|
1870
1871
|
break;
|
|
1871
1872
|
}
|
|
1872
1873
|
case "mix":
|
|
@@ -1979,6 +1980,7 @@ function createPlayer(THREE, o) {
|
|
|
1979
1980
|
let useStarted = false;
|
|
1980
1981
|
let holdTimer = null;
|
|
1981
1982
|
let walkHeld = false;
|
|
1983
|
+
let seated = false;
|
|
1982
1984
|
let lookLimit = null;
|
|
1983
1985
|
const look = (dx, dy) => {
|
|
1984
1986
|
yaw -= dx * LOOK;
|
|
@@ -2119,6 +2121,11 @@ function createPlayer(THREE, o) {
|
|
|
2119
2121
|
setWalkHeld(held) {
|
|
2120
2122
|
walkHeld = held;
|
|
2121
2123
|
},
|
|
2124
|
+
setSeated(value) {
|
|
2125
|
+
seated = value;
|
|
2126
|
+
y = 0;
|
|
2127
|
+
vy = 0;
|
|
2128
|
+
},
|
|
2122
2129
|
setBend(metres) {
|
|
2123
2130
|
bendTarget = metres;
|
|
2124
2131
|
},
|
|
@@ -2171,6 +2178,11 @@ function createPlayer(THREE, o) {
|
|
|
2171
2178
|
if ((walkX || walkZ) && !walkHeld) move(walkX * speed * dt, walkZ * speed * dt);
|
|
2172
2179
|
walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
|
|
2173
2180
|
running = run && walked > 0;
|
|
2181
|
+
if (seated) {
|
|
2182
|
+
followFloor(dt);
|
|
2183
|
+
apply();
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2174
2186
|
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2175
2187
|
if (y < floor && vy <= 0) y = floor;
|
|
2176
2188
|
if (y <= floor && keys.has("Space") && !walkHeld) vy = JUMP;
|
|
@@ -2318,7 +2330,7 @@ function createRoomPush(options) {
|
|
|
2318
2330
|
}
|
|
2319
2331
|
control.release();
|
|
2320
2332
|
player.setWalkHeld(false);
|
|
2321
|
-
player.setBend(0);
|
|
2333
|
+
if (!options.hasBody?.()) player.setBend(0);
|
|
2322
2334
|
player.setLookLimit(null);
|
|
2323
2335
|
options.ring(null);
|
|
2324
2336
|
event("release", lean);
|
|
@@ -2380,7 +2392,7 @@ function createRoomPush(options) {
|
|
|
2380
2392
|
control.begin(material === void 0 ? { massKg } : { massKg, material });
|
|
2381
2393
|
dragged = 0;
|
|
2382
2394
|
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))));
|
|
2395
|
+
if (isLow && !options.hasBody?.()) player.setBend(Math.min(MAX_BEND, Math.max(0, STANDING_EYE - (max[1] - player.feet + LOW_EYE_OVER_TOP))));
|
|
2384
2396
|
player.setLookLimit({ yaw: Math.atan2(-into[0], -into[1]), range: LOOK_RANGE });
|
|
2385
2397
|
event("grip", leaning);
|
|
2386
2398
|
return true;
|
|
@@ -2440,6 +2452,501 @@ function createRoomPush(options) {
|
|
|
2440
2452
|
};
|
|
2441
2453
|
}
|
|
2442
2454
|
|
|
2455
|
+
// src/seating.ts
|
|
2456
|
+
var SIT_EYE = 0.62;
|
|
2457
|
+
var LIE_EYE = 0.18;
|
|
2458
|
+
var STANDING_EYE2 = 1.7;
|
|
2459
|
+
var LOOK_RANGE2 = 1.6;
|
|
2460
|
+
function createSeating(o) {
|
|
2461
|
+
const { THREE, player } = o;
|
|
2462
|
+
const v = new THREE.Vector3();
|
|
2463
|
+
const q = new THREE.Quaternion();
|
|
2464
|
+
const e = new THREE.Euler();
|
|
2465
|
+
let current = null;
|
|
2466
|
+
let facing = 0;
|
|
2467
|
+
const toWorld = (root, local) => {
|
|
2468
|
+
root.updateWorldMatrix(true, false);
|
|
2469
|
+
v.set(local[0], local[1], local[2]).applyMatrix4(root.matrixWorld);
|
|
2470
|
+
return [v.x, v.y, v.z];
|
|
2471
|
+
};
|
|
2472
|
+
const yawOf = (root) => {
|
|
2473
|
+
root.getWorldQuaternion(q);
|
|
2474
|
+
return e.setFromQuaternion(q, "YXZ").y;
|
|
2475
|
+
};
|
|
2476
|
+
const nearest = (holder) => {
|
|
2477
|
+
let best;
|
|
2478
|
+
let bestDistance = Infinity;
|
|
2479
|
+
for (const seat of holder.seats) {
|
|
2480
|
+
const [x, , z] = toWorld(holder.root, seat.pos);
|
|
2481
|
+
const distance = Math.hypot(x - player.x, z - player.z);
|
|
2482
|
+
if (distance < bestDistance) {
|
|
2483
|
+
best = seat;
|
|
2484
|
+
bestDistance = distance;
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
return best;
|
|
2488
|
+
};
|
|
2489
|
+
const hold = () => {
|
|
2490
|
+
player.setSeated(true);
|
|
2491
|
+
player.setWalkHeld(true);
|
|
2492
|
+
player.setLookLimit({ yaw: facing, range: LOOK_RANGE2 });
|
|
2493
|
+
};
|
|
2494
|
+
const release = () => {
|
|
2495
|
+
player.setSeated(false);
|
|
2496
|
+
player.setWalkHeld(false);
|
|
2497
|
+
player.setLookLimit(null);
|
|
2498
|
+
current = null;
|
|
2499
|
+
};
|
|
2500
|
+
const seating = {
|
|
2501
|
+
sit(objectId, seatId) {
|
|
2502
|
+
const holder = objectId ? o.find(objectId) : o.pick();
|
|
2503
|
+
if (!holder) return null;
|
|
2504
|
+
const seat = seatId ? holder.seats.find((s) => s.id === seatId) : nearest(holder);
|
|
2505
|
+
if (!seat) return null;
|
|
2506
|
+
const [x, y, z] = toWorld(holder.root, seat.pos);
|
|
2507
|
+
const eye = o.hasBody?.() ? STANDING_EYE2 : seat.eye ?? (seat.pose === "lie" ? LIE_EYE : SIT_EYE);
|
|
2508
|
+
facing = yawOf(holder.root) + seat.ry + Math.PI;
|
|
2509
|
+
player.setPose([x, y + eye, z], facing, 0);
|
|
2510
|
+
current = { objectId: holder.objectId, seatId: seat.id, pose: seat.pose, hips: [x, y, z] };
|
|
2511
|
+
hold();
|
|
2512
|
+
o.onSit?.(current);
|
|
2513
|
+
return current;
|
|
2514
|
+
},
|
|
2515
|
+
stand() {
|
|
2516
|
+
if (!current) return false;
|
|
2517
|
+
const holder = o.find(current.objectId);
|
|
2518
|
+
const seat = holder?.seats.find((s) => s.id === current?.seatId);
|
|
2519
|
+
if (holder && seat) {
|
|
2520
|
+
const [x, , z] = toWorld(holder.root, seat.exit);
|
|
2521
|
+
if (!player.canStand(x, z)) return false;
|
|
2522
|
+
player.setPose([x, o.groundAt(x, z) + STANDING_EYE2, z], player.yaw, 0);
|
|
2523
|
+
} else player.setPose([player.x, o.groundAt(player.x, player.z) + STANDING_EYE2, player.z], player.yaw, 0);
|
|
2524
|
+
const left = current;
|
|
2525
|
+
release();
|
|
2526
|
+
o.onStand?.(left);
|
|
2527
|
+
return true;
|
|
2528
|
+
},
|
|
2529
|
+
update() {
|
|
2530
|
+
if (!current) return;
|
|
2531
|
+
if (!o.find(current.objectId)) {
|
|
2532
|
+
seating.stand();
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2535
|
+
const [walkX, walkZ] = player.walkIntent;
|
|
2536
|
+
if (walkX || walkZ || player.keys.has("Space")) {
|
|
2537
|
+
seating.stand();
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
hold();
|
|
2541
|
+
},
|
|
2542
|
+
release,
|
|
2543
|
+
get seat() {
|
|
2544
|
+
return current;
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
return seating;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
// src/door-open.ts
|
|
2551
|
+
import { createDoorUse, DOOR_USE_DEFAULTS } from "@adatechnology/game-player";
|
|
2552
|
+
var DOOR_REACH = 2.2;
|
|
2553
|
+
var NEAR_LEAF = 0.25;
|
|
2554
|
+
var NEAR_OPEN_LEAF = 0.9;
|
|
2555
|
+
var BODY_HEIGHT = 1.8;
|
|
2556
|
+
var HAND_REST = [0.22, -0.42, -0.32];
|
|
2557
|
+
var SKIN = 0.01;
|
|
2558
|
+
var HANDLE_INSET = 0.06;
|
|
2559
|
+
var STANDOFF2 = 0.55;
|
|
2560
|
+
var DRAWER_TRAVEL = 0.3;
|
|
2561
|
+
var CLEAR_OF_ARC = 0.3;
|
|
2562
|
+
var OUT_IN_FRONT = 0.42;
|
|
2563
|
+
var ARRIVED = 0.05;
|
|
2564
|
+
var TURN_RATE = 8;
|
|
2565
|
+
var MAX_WALK_SECONDS = 1.5;
|
|
2566
|
+
var APPROACH_SPEED2 = 3;
|
|
2567
|
+
var FLOOR_GAP = 0.03;
|
|
2568
|
+
var key = (objectId, part) => `${objectId}\0${part}`;
|
|
2569
|
+
var slides = (clip, part) => {
|
|
2570
|
+
const last = clip.keys[clip.keys.length - 1]?.parts?.[part];
|
|
2571
|
+
const r = last?.rotation ?? [0, 0, 0];
|
|
2572
|
+
return Math.abs(r[0]) + Math.abs(r[1]) + Math.abs(r[2]) < 1e-3;
|
|
2573
|
+
};
|
|
2574
|
+
function createRoomDoors(options) {
|
|
2575
|
+
const { THREE, player, world, camera, arena } = options;
|
|
2576
|
+
const control = createDoorUse(options.rules);
|
|
2577
|
+
const openings = /* @__PURE__ */ new Map();
|
|
2578
|
+
const box2 = new THREE.Box3();
|
|
2579
|
+
const before = new THREE.Box3();
|
|
2580
|
+
const tmp = new THREE.Vector3();
|
|
2581
|
+
const centre = new THREE.Vector3();
|
|
2582
|
+
let using = null;
|
|
2583
|
+
let reaching = null;
|
|
2584
|
+
const rules = { ...DOOR_USE_DEFAULTS, ...options.rules };
|
|
2585
|
+
const holding = () => {
|
|
2586
|
+
if (reaching) {
|
|
2587
|
+
const h = reaching.reach.handle();
|
|
2588
|
+
return { phase: reaching.phase, progress: reaching.progress, at: new THREE.Vector3(h[0], h[1], h[2]), pull: reaching.reach.pull ?? true };
|
|
2589
|
+
}
|
|
2590
|
+
if (!using || control.phase === "idle") return null;
|
|
2591
|
+
using.obj.updateMatrixWorld(true);
|
|
2592
|
+
return { phase: control.phase, progress: using.progress, at: using.obj.localToWorld(new THREE.Vector3().copy(using.grip)), pull: using.pull };
|
|
2593
|
+
};
|
|
2594
|
+
const standFor = (handle, hinge, slide) => {
|
|
2595
|
+
if (!hinge || slide) {
|
|
2596
|
+
const dx = handle.x - player.x;
|
|
2597
|
+
const dz = handle.z - player.z;
|
|
2598
|
+
const d = Math.hypot(dx, dz) || 1;
|
|
2599
|
+
const off = slide ? STANDOFF2 + DRAWER_TRAVEL : STANDOFF2;
|
|
2600
|
+
return [handle.x - dx / d * off, handle.z - dz / d * off];
|
|
2601
|
+
}
|
|
2602
|
+
let ax = handle.x - hinge.x;
|
|
2603
|
+
let az = handle.z - hinge.z;
|
|
2604
|
+
const width = Math.hypot(ax, az);
|
|
2605
|
+
if (width < 0.05) return standFor(handle, null, false);
|
|
2606
|
+
ax /= width;
|
|
2607
|
+
az /= width;
|
|
2608
|
+
let nx = -az;
|
|
2609
|
+
let nz = ax;
|
|
2610
|
+
if ((player.x - handle.x) * nx + (player.z - handle.z) * nz < 0) {
|
|
2611
|
+
nx = -nx;
|
|
2612
|
+
nz = -nz;
|
|
2613
|
+
}
|
|
2614
|
+
const x = hinge.x + ax * (width + CLEAR_OF_ARC) + nx * OUT_IN_FRONT;
|
|
2615
|
+
const z = hinge.z + az * (width + CLEAR_OF_ARC) + nz * OUT_IN_FRONT;
|
|
2616
|
+
return player.canStand(x, z) ? [x, z] : standFor(handle, null, false);
|
|
2617
|
+
};
|
|
2618
|
+
const face = (handle, dt) => {
|
|
2619
|
+
camera.updateMatrixWorld(true);
|
|
2620
|
+
const eye = camera.getWorldPosition(new THREE.Vector3());
|
|
2621
|
+
const dx = handle.x - player.x;
|
|
2622
|
+
const dz = handle.z - player.z;
|
|
2623
|
+
const flat = Math.hypot(dx, dz);
|
|
2624
|
+
if (flat < 0.05) return;
|
|
2625
|
+
const yaw = Math.atan2(-dx, -dz);
|
|
2626
|
+
const pitch = Math.atan2(handle.y - eye.y, flat);
|
|
2627
|
+
let dYaw = yaw - player.yaw;
|
|
2628
|
+
dYaw = Math.atan2(Math.sin(dYaw), Math.cos(dYaw));
|
|
2629
|
+
const k = Math.min(1, dt * TURN_RATE);
|
|
2630
|
+
player.setPose([player.x, eye.y, player.z], player.yaw + dYaw * k, player.pitch + (pitch - player.pitch) * k);
|
|
2631
|
+
};
|
|
2632
|
+
const approach = (handle, hinge, slide, dt, walked) => {
|
|
2633
|
+
const [x, z] = standFor(handle, hinge, slide);
|
|
2634
|
+
const dx = x - player.x;
|
|
2635
|
+
const dz = z - player.z;
|
|
2636
|
+
const d = Math.hypot(dx, dz);
|
|
2637
|
+
if (d < ARRIVED || walked > MAX_WALK_SECONDS) return true;
|
|
2638
|
+
const go = Math.min(d, APPROACH_SPEED2 * dt);
|
|
2639
|
+
if (go < 1e-4) return false;
|
|
2640
|
+
const x0 = player.x;
|
|
2641
|
+
const z0 = player.z;
|
|
2642
|
+
player.moveBy(dx / d * go, dz / d * go);
|
|
2643
|
+
return Math.hypot(player.x - x0, player.z - z0) < go * 0.2;
|
|
2644
|
+
};
|
|
2645
|
+
const openingOf = (objectId, part) => openings.get(key(objectId, part)) ?? 0;
|
|
2646
|
+
const lastGoals = /* @__PURE__ */ new Map();
|
|
2647
|
+
const leafAt = (objectId, part, point) => {
|
|
2648
|
+
const leaves = options.leaves(objectId);
|
|
2649
|
+
if (leaves.length === 0) return null;
|
|
2650
|
+
const exact = leaves.find((l) => l.part === part);
|
|
2651
|
+
if (exact) return exact;
|
|
2652
|
+
let best = null;
|
|
2653
|
+
let bestD = NEAR_LEAF;
|
|
2654
|
+
let open = null;
|
|
2655
|
+
let openD = NEAR_OPEN_LEAF;
|
|
2656
|
+
for (const l of leaves) {
|
|
2657
|
+
const obj = options.objectOf(objectId, l.part);
|
|
2658
|
+
if (!obj) continue;
|
|
2659
|
+
box2.setFromObject(obj);
|
|
2660
|
+
const d = box2.distanceToPoint(tmp.set(point[0], point[1], point[2]));
|
|
2661
|
+
if (d < bestD) {
|
|
2662
|
+
bestD = d;
|
|
2663
|
+
best = l;
|
|
2664
|
+
}
|
|
2665
|
+
if (openingOf(objectId, l.part) > 0.5 && d < openD) {
|
|
2666
|
+
openD = d;
|
|
2667
|
+
open = l;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return best ?? open;
|
|
2671
|
+
};
|
|
2672
|
+
const picked = () => {
|
|
2673
|
+
for (const hit of options.pick()) {
|
|
2674
|
+
const leaf = leafAt(hit.objectId, hit.part, hit.point);
|
|
2675
|
+
if (leaf) return { hit, leaf };
|
|
2676
|
+
}
|
|
2677
|
+
return null;
|
|
2678
|
+
};
|
|
2679
|
+
const massOf = (objectId, part) => world.part(objectId, part)?.mass ?? 10;
|
|
2680
|
+
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 } : {} });
|
|
2681
|
+
const overlaps2d = (b, c) => b.max.x > c[0] && b.min.x < c[2] && b.max.z > c[1] && b.min.z < c[3];
|
|
2682
|
+
const bodyOverlap = (b) => {
|
|
2683
|
+
if (b.min.y > player.feet + BODY_HEIGHT || b.max.y < player.feet + 0.05) return null;
|
|
2684
|
+
const cx = Math.max(b.min.x, Math.min(b.max.x, player.x));
|
|
2685
|
+
const cz = Math.max(b.min.z, Math.min(b.max.z, player.z));
|
|
2686
|
+
const dx = player.x - cx;
|
|
2687
|
+
const dz = player.z - cz;
|
|
2688
|
+
const d = Math.hypot(dx, dz);
|
|
2689
|
+
if (d >= PLAYER_RADIUS) return null;
|
|
2690
|
+
if (d < 1e-4) {
|
|
2691
|
+
const outs = [
|
|
2692
|
+
[b.max.x - player.x, 1, 0],
|
|
2693
|
+
[player.x - b.min.x, -1, 0],
|
|
2694
|
+
[b.max.z - player.z, 0, 1],
|
|
2695
|
+
[player.z - b.min.z, 0, -1]
|
|
2696
|
+
];
|
|
2697
|
+
outs.sort((a, c) => a[0] - c[0]);
|
|
2698
|
+
const o = outs[0];
|
|
2699
|
+
return [o[1] * (o[0] + PLAYER_RADIUS), o[2] * (o[0] + PLAYER_RADIUS)];
|
|
2700
|
+
}
|
|
2701
|
+
const push = PLAYER_RADIUS - d + SKIN;
|
|
2702
|
+
return [dx / d * push, dz / d * push];
|
|
2703
|
+
};
|
|
2704
|
+
const isFree = (u, from, to) => {
|
|
2705
|
+
const { objectId, leaf, obj } = u;
|
|
2706
|
+
obj.updateMatrixWorld(true);
|
|
2707
|
+
before.setFromObject(obj);
|
|
2708
|
+
options.pose(objectId, leaf.part, leaf.clip, to);
|
|
2709
|
+
obj.updateMatrixWorld(true);
|
|
2710
|
+
box2.setFromObject(obj);
|
|
2711
|
+
box2.expandByScalar(-SKIN);
|
|
2712
|
+
let by = null;
|
|
2713
|
+
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";
|
|
2714
|
+
else
|
|
2715
|
+
for (let i = 0; i < arena.colliders.length; i++) {
|
|
2716
|
+
const c = arena.colliders[i];
|
|
2717
|
+
if (arena.ids[i] === objectId) continue;
|
|
2718
|
+
if (overlaps2d(box2, c) && !overlaps2d(before, c)) {
|
|
2719
|
+
by = arena.ids[i] ?? "wall";
|
|
2720
|
+
break;
|
|
2721
|
+
}
|
|
2722
|
+
}
|
|
2723
|
+
if (!by) {
|
|
2724
|
+
before.getCenter(centre);
|
|
2725
|
+
box2.getCenter(tmp);
|
|
2726
|
+
const dir = [tmp.x - centre.x, tmp.y - centre.y, tmp.z - centre.z];
|
|
2727
|
+
const len = Math.hypot(dir[0], dir[1], dir[2]);
|
|
2728
|
+
if (len > 1e-5) {
|
|
2729
|
+
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 });
|
|
2730
|
+
if (hit && hit.normal[1] < 0.7) by = hit.object;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (!by) {
|
|
2734
|
+
const out = bodyOverlap(box2);
|
|
2735
|
+
if (out) {
|
|
2736
|
+
const x0 = player.x;
|
|
2737
|
+
const z0 = player.z;
|
|
2738
|
+
player.moveBy(out[0], out[1]);
|
|
2739
|
+
if (Math.hypot(player.x - x0, player.z - z0) < Math.hypot(out[0], out[1]) * 0.5) by = "player";
|
|
2740
|
+
else if (u.phase !== "swing" || from === 0) event("stepped_back", u);
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
if (by) {
|
|
2744
|
+
options.pose(objectId, leaf.part, leaf.clip, from);
|
|
2745
|
+
const at = [box2.getCenter(tmp).x, tmp.y, tmp.z];
|
|
2746
|
+
options.sound(options.impactKey(objectId, leaf.part), at, Math.min(0.6, 0.15 + massOf(objectId, leaf.part) * 0.02));
|
|
2747
|
+
event("blocked", u, by);
|
|
2748
|
+
return false;
|
|
2749
|
+
}
|
|
2750
|
+
return true;
|
|
2751
|
+
};
|
|
2752
|
+
const handleOf = (obj, leaf, point) => {
|
|
2753
|
+
obj.updateMatrixWorld(true);
|
|
2754
|
+
box2.setFromObject(obj);
|
|
2755
|
+
const lo = new THREE.Vector3(Infinity, Infinity, Infinity);
|
|
2756
|
+
const hi = new THREE.Vector3(-Infinity, -Infinity, -Infinity);
|
|
2757
|
+
for (let i = 0; i < 8; i++) {
|
|
2758
|
+
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);
|
|
2759
|
+
obj.worldToLocal(tmp);
|
|
2760
|
+
lo.min(tmp);
|
|
2761
|
+
hi.max(tmp);
|
|
2762
|
+
}
|
|
2763
|
+
const aimed = point ? obj.worldToLocal(new THREE.Vector3(point[0], point[1], point[2])) : lo.clone().add(hi).multiplyScalar(0.5);
|
|
2764
|
+
if (!point) {
|
|
2765
|
+
const local = obj.worldToLocal(obj.getWorldPosition(new THREE.Vector3()).setY(player.feet + 1));
|
|
2766
|
+
aimed.y = local.y;
|
|
2767
|
+
}
|
|
2768
|
+
aimed.clamp(lo, hi);
|
|
2769
|
+
if (slides(leaf.clip, leaf.part)) return aimed;
|
|
2770
|
+
const last = leaf.clip.keys[leaf.clip.keys.length - 1]?.parts?.[leaf.part]?.rotation ?? [0, 1, 0];
|
|
2771
|
+
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;
|
|
2772
|
+
const size = [hi.x - lo.x, hi.y - lo.y, hi.z - lo.z];
|
|
2773
|
+
const reachOf = (k) => Math.max(Math.abs(lo.getComponent(k)), Math.abs(hi.getComponent(k)));
|
|
2774
|
+
let across = -1;
|
|
2775
|
+
for (let k = 0; k < 3; k++) {
|
|
2776
|
+
if (k === axis || size[k] < Math.min(size[0], size[1], size[2]) + 1e-6) continue;
|
|
2777
|
+
if (across < 0 || reachOf(k) > reachOf(across)) across = k;
|
|
2778
|
+
}
|
|
2779
|
+
if (across < 0) return aimed;
|
|
2780
|
+
const far = Math.abs(hi.getComponent(across)) >= Math.abs(lo.getComponent(across)) ? hi.getComponent(across) : lo.getComponent(across);
|
|
2781
|
+
aimed.setComponent(across, far - Math.sign(far) * Math.min(HANDLE_INSET, size[across] * 0.25));
|
|
2782
|
+
return aimed;
|
|
2783
|
+
};
|
|
2784
|
+
const start = (objectId, leaf, point) => {
|
|
2785
|
+
reaching = null;
|
|
2786
|
+
const obj = options.objectOf(objectId, leaf.part);
|
|
2787
|
+
if (!obj) return false;
|
|
2788
|
+
if (using && (using.objectId !== objectId || using.leaf.part !== leaf.part)) control.cancel();
|
|
2789
|
+
obj.updateMatrixWorld(true);
|
|
2790
|
+
const grip = handleOf(obj, leaf, point);
|
|
2791
|
+
const opening = openingOf(objectId, leaf.part);
|
|
2792
|
+
const key2 = `${objectId}|${leaf.part}`;
|
|
2793
|
+
const lastGoal = lastGoals.get(key2);
|
|
2794
|
+
const goal = control.begin({ opening, massKg: massOf(objectId, leaf.part), locked: options.isLocked(objectId, leaf.part), ...lastGoal !== void 0 ? { lastGoal } : {} });
|
|
2795
|
+
if (goal !== null) lastGoals.set(key2, goal);
|
|
2796
|
+
let pull = true;
|
|
2797
|
+
if (goal !== null) {
|
|
2798
|
+
obj.updateMatrixWorld(true);
|
|
2799
|
+
options.pose(objectId, leaf.part, leaf.clip, 1);
|
|
2800
|
+
obj.updateMatrixWorld(true);
|
|
2801
|
+
const openAt = obj.localToWorld(grip.clone());
|
|
2802
|
+
options.pose(objectId, leaf.part, leaf.clip, 0);
|
|
2803
|
+
obj.updateMatrixWorld(true);
|
|
2804
|
+
const shutAt = obj.localToWorld(grip.clone());
|
|
2805
|
+
options.pose(objectId, leaf.part, leaf.clip, opening);
|
|
2806
|
+
const dOpen = Math.hypot(openAt.x - player.x, openAt.z - player.z);
|
|
2807
|
+
const dShut = Math.hypot(shutAt.x - player.x, shutAt.z - player.z);
|
|
2808
|
+
pull = goal === 1 ? dOpen <= dShut : dShut <= dOpen;
|
|
2809
|
+
}
|
|
2810
|
+
using = { objectId, leaf, obj, grip, pull, phase: control.phase, progress: 0, walked: 0, there: goal === null };
|
|
2811
|
+
const at = obj.localToWorld(grip.clone());
|
|
2812
|
+
if (goal === null) {
|
|
2813
|
+
options.sound("leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
2814
|
+
event("locked", using);
|
|
2815
|
+
} else event("reach", using);
|
|
2816
|
+
return true;
|
|
2817
|
+
};
|
|
2818
|
+
return {
|
|
2819
|
+
get active() {
|
|
2820
|
+
return reaching !== null || using !== null && control.phase !== "idle";
|
|
2821
|
+
},
|
|
2822
|
+
get state() {
|
|
2823
|
+
return using ? { objectId: using.objectId, part: using.leaf.part, phase: control.phase, opening: openingOf(using.objectId, using.leaf.part), goal: control.goal } : null;
|
|
2824
|
+
},
|
|
2825
|
+
get avatarState() {
|
|
2826
|
+
const h = holding();
|
|
2827
|
+
return h ? { phase: h.phase, progress: h.progress, height: h.at.y - player.feet, pull: h.pull } : null;
|
|
2828
|
+
},
|
|
2829
|
+
aimed() {
|
|
2830
|
+
const found = picked();
|
|
2831
|
+
if (!found) return null;
|
|
2832
|
+
const { hit, leaf } = found;
|
|
2833
|
+
return { objectId: hit.objectId, part: leaf.part, open: openingOf(hit.objectId, leaf.part) > 0.5, locked: options.isLocked(hit.objectId, leaf.part) };
|
|
2834
|
+
},
|
|
2835
|
+
use() {
|
|
2836
|
+
const found = picked();
|
|
2837
|
+
return found ? start(found.hit.objectId, found.leaf, found.hit.point) : false;
|
|
2838
|
+
},
|
|
2839
|
+
useLeaf(objectId, part) {
|
|
2840
|
+
const leaves = options.leaves(objectId);
|
|
2841
|
+
const leaf = part === void 0 ? leaves[0] : leaves.find((l) => l.part === part);
|
|
2842
|
+
return leaf ? start(objectId, leaf, null) : false;
|
|
2843
|
+
},
|
|
2844
|
+
opening: openingOf,
|
|
2845
|
+
step(dt) {
|
|
2846
|
+
const r = reaching;
|
|
2847
|
+
if (r) {
|
|
2848
|
+
r.time += dt;
|
|
2849
|
+
if (r.phase === "reach" && !r.there) {
|
|
2850
|
+
const h = r.reach.handle();
|
|
2851
|
+
const hinge = r.reach.hinge?.();
|
|
2852
|
+
r.walked += dt;
|
|
2853
|
+
const g = new THREE.Vector3(h[0], h[1], h[2]);
|
|
2854
|
+
r.there = approach(g, hinge ? new THREE.Vector3(hinge[0], hinge[1], hinge[2]) : null, false, dt, r.walked);
|
|
2855
|
+
face(g, dt);
|
|
2856
|
+
if (r.there) r.time = 0;
|
|
2857
|
+
} else if (r.phase === "reach") {
|
|
2858
|
+
r.progress = Math.min(1, r.time / rules.reachSeconds);
|
|
2859
|
+
if (r.progress >= 1) {
|
|
2860
|
+
r.reach.onGrip?.();
|
|
2861
|
+
r.phase = r.reach.locked ? "rattle" : "swing";
|
|
2862
|
+
r.time = 0;
|
|
2863
|
+
r.progress = 0;
|
|
2864
|
+
}
|
|
2865
|
+
} else if (r.phase === "swing" || r.phase === "rattle") {
|
|
2866
|
+
r.progress = Math.min(1, r.time / (r.phase === "rattle" ? rules.rattleSeconds : Math.max(0.05, r.reach.seconds)));
|
|
2867
|
+
if (r.progress >= 1) {
|
|
2868
|
+
r.phase = "release";
|
|
2869
|
+
r.time = 0;
|
|
2870
|
+
r.progress = 0;
|
|
2871
|
+
}
|
|
2872
|
+
} else {
|
|
2873
|
+
r.progress = Math.min(1, r.time / rules.releaseSeconds);
|
|
2874
|
+
if (r.progress >= 1) reaching = null;
|
|
2875
|
+
}
|
|
2876
|
+
return;
|
|
2877
|
+
}
|
|
2878
|
+
const u = using;
|
|
2879
|
+
if (!u || control.phase === "idle") return;
|
|
2880
|
+
const was = openingOf(u.objectId, u.leaf.part);
|
|
2881
|
+
const wasPhase = control.phase;
|
|
2882
|
+
if (wasPhase === "reach" && !u.there) {
|
|
2883
|
+
u.obj.updateMatrixWorld(true);
|
|
2884
|
+
u.walked += dt;
|
|
2885
|
+
const g = u.obj.localToWorld(new THREE.Vector3().copy(u.grip));
|
|
2886
|
+
u.there = approach(g, u.obj.getWorldPosition(new THREE.Vector3()), slides(u.leaf.clip, u.leaf.part), dt, u.walked);
|
|
2887
|
+
face(g, dt);
|
|
2888
|
+
if (!u.there) return;
|
|
2889
|
+
}
|
|
2890
|
+
const s = control.step(dt, (from, to) => isFree(u, from, to));
|
|
2891
|
+
u.phase = s.phase;
|
|
2892
|
+
u.progress = s.progress;
|
|
2893
|
+
openings.set(key(u.objectId, u.leaf.part), s.opening);
|
|
2894
|
+
u.obj.updateMatrixWorld(true);
|
|
2895
|
+
const at = u.obj.localToWorld(tmp.copy(u.grip));
|
|
2896
|
+
const point = [at.x, at.y, at.z];
|
|
2897
|
+
const slide = slides(u.leaf.clip, u.leaf.part);
|
|
2898
|
+
const shut = options.impactKey(u.objectId, u.leaf.part).replace("impact_", "leaf_shut_");
|
|
2899
|
+
if (wasPhase === "reach" && s.phase !== "reach") {
|
|
2900
|
+
if (slide) options.sound("leaf_slide", point, 0.5);
|
|
2901
|
+
else {
|
|
2902
|
+
if (was < 0.02) options.sound("leaf_latch", point, 0.5);
|
|
2903
|
+
options.sound(/metal|glass/.test(shut) ? "leaf_creak_metal" : "leaf_creak_wood", point, 0.35);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
if (s.arrived) {
|
|
2907
|
+
if (s.opening <= 0) {
|
|
2908
|
+
options.sound(slide ? "leaf_slide" : shut, point, Math.min(1, 0.3 + massOf(u.objectId, u.leaf.part) * 0.03));
|
|
2909
|
+
event("closed", u);
|
|
2910
|
+
} else event("opened", u);
|
|
2911
|
+
}
|
|
2912
|
+
if (s.phase === "idle") using = null;
|
|
2913
|
+
},
|
|
2914
|
+
handTargets() {
|
|
2915
|
+
const h = holding();
|
|
2916
|
+
if (!h || h.phase === "rattle" && h.progress > 0.92) return null;
|
|
2917
|
+
const { at, phase, progress } = h;
|
|
2918
|
+
if (phase === "rattle") at.y += Math.sin(progress * 60) * 0.012;
|
|
2919
|
+
camera.updateMatrixWorld(true);
|
|
2920
|
+
const rest = camera.localToWorld(new THREE.Vector3(HAND_REST[0], HAND_REST[1], HAND_REST[2]));
|
|
2921
|
+
const k = phase === "reach" ? progress : phase === "release" ? 1 - progress : 1;
|
|
2922
|
+
const ease = k * k * (3 - 2 * k);
|
|
2923
|
+
const pos = rest.lerp(at, ease);
|
|
2924
|
+
const way = new THREE.Vector3(at.x - player.x, 0, at.z - player.z);
|
|
2925
|
+
if (way.lengthSq() < 1e-6) way.set(-Math.sin(player.yaw), 0, -Math.cos(player.yaw));
|
|
2926
|
+
way.normalize();
|
|
2927
|
+
const down = new THREE.Vector3(0, -1, 0);
|
|
2928
|
+
const back = new THREE.Vector3().crossVectors(down, way);
|
|
2929
|
+
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(way, back, down));
|
|
2930
|
+
const grip = phase === "reach" ? 0.15 + 0.75 * progress : phase === "release" ? 0.9 * (1 - progress) : 0.9;
|
|
2931
|
+
return { R: { position: [pos.x, pos.y, pos.z], quaternion: [q.x, q.y, q.z, q.w] }, L: null, grip };
|
|
2932
|
+
},
|
|
2933
|
+
reachFor(reach) {
|
|
2934
|
+
if (using) {
|
|
2935
|
+
control.cancel();
|
|
2936
|
+
using = null;
|
|
2937
|
+
}
|
|
2938
|
+
reaching = { reach, phase: "reach", time: 0, progress: 0, walked: 0, there: false };
|
|
2939
|
+
},
|
|
2940
|
+
forget(objectId) {
|
|
2941
|
+
for (const k of [...openings.keys()]) if (k.startsWith(`${objectId}\0`)) openings.delete(k);
|
|
2942
|
+
if (using?.objectId === objectId) {
|
|
2943
|
+
control.cancel();
|
|
2944
|
+
using = null;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2443
2950
|
// src/room.ts
|
|
2444
2951
|
import { createTerrain, createTerrainImpl, DEFAULT_PROFILE } from "@adatechnology/game-terrain";
|
|
2445
2952
|
import { createTerrainRenderer } from "@adatechnology/game-terrain-three";
|
|
@@ -2660,17 +3167,24 @@ var en = {
|
|
|
2660
3167
|
"loading.open_area": "Opening {area}\u2026",
|
|
2661
3168
|
"loading.spawn": "Building {name}\u2026",
|
|
2662
3169
|
"act.loosened": "{name} was heavy but free: the blast moved it",
|
|
3170
|
+
"act.came_apart": "{name} came apart: what stayed is loose and falls",
|
|
2663
3171
|
"act.shoved": "{name} stands free: the shots moved it",
|
|
2664
3172
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
2665
3173
|
"act.use_item": "{name}: {use}",
|
|
3174
|
+
"act.open_with": "Opened {name} with {tool}",
|
|
3175
|
+
"act.open_by_hand": "Opened {name} by hand",
|
|
3176
|
+
"act.ate": "Ate {grams} g of {name}",
|
|
3177
|
+
"act.ate_all": "Ate all of {name} ({grams} g): the packaging is empty",
|
|
2666
3178
|
"act.tap.on": "Tap opened",
|
|
2667
3179
|
"act.tap.off": "Tap closed",
|
|
2668
3180
|
"use.drink": "drink",
|
|
2669
3181
|
"use.sip": "sip",
|
|
2670
3182
|
"use.chug": "chug",
|
|
3183
|
+
"use.eat": "eat",
|
|
2671
3184
|
"use.pour": "pour",
|
|
2672
3185
|
"use.pour_out": "pour out",
|
|
2673
3186
|
"use.open": "open",
|
|
3187
|
+
"use.unscrew": "unscrew",
|
|
2674
3188
|
"use.close": "close",
|
|
2675
3189
|
"hud.reloading": "Reloading...",
|
|
2676
3190
|
"hud.empty": "Empty",
|
|
@@ -2687,15 +3201,32 @@ var en = {
|
|
|
2687
3201
|
"hud.nothing_to_grab": "Nothing to grab here",
|
|
2688
3202
|
"hud.cannot_grab": "{name} cannot be picked up",
|
|
2689
3203
|
"hud.too_heavy": "{name} is too heavy ({kg} kg)",
|
|
3204
|
+
"hud.door_locked": "{name} is locked",
|
|
3205
|
+
"hud.door_blocked": "{name} hits something and stops ({percent}% open)",
|
|
3206
|
+
"hud.door_hint_open": "E: open",
|
|
3207
|
+
"hud.door_hint_close": "E: close",
|
|
3208
|
+
"hud.door_hint_locked": "Locked",
|
|
3209
|
+
"act.door_reach": "reaches for the handle of {name}",
|
|
3210
|
+
"act.door_opened": "opens {name}",
|
|
3211
|
+
"act.door_closed": "closes {name}",
|
|
3212
|
+
"act.door_blocked": "{name} hits something and stops at {percent}%",
|
|
3213
|
+
"act.door_locked": "tries {name}: locked, it only rattles",
|
|
3214
|
+
"act.door_stepped_back": "steps back out of the way of {name}",
|
|
2690
3215
|
"hud.push_on": "Pushing {name}: W pushes, S pulls, let go of E to stop",
|
|
2691
3216
|
"hud.push_too_heavy": "{name} does not budge ({kg} kg)",
|
|
2692
3217
|
"hud.push_blocked": "{name} is stuck: there is something in the way",
|
|
2693
3218
|
"act.push_grip": "leans on {name} ({kg} kg)",
|
|
2694
3219
|
"act.push_engaged": "starts pushing {name} ({kg} kg)",
|
|
3220
|
+
"act.sit": "sits on {name}",
|
|
3221
|
+
"act.lie": "lies down on {name}",
|
|
3222
|
+
"act.stand": "gets up from {name}",
|
|
2695
3223
|
"act.push_too_heavy": "strains against {name}: {kg} kg is too much",
|
|
2696
3224
|
"act.push_obstacle": "{name} hits something and locks",
|
|
2697
3225
|
"act.push_release": "lets go of {name}",
|
|
2698
3226
|
"hud.empty_container": "{name} is empty",
|
|
3227
|
+
"hud.needs_opener": "Opening {name} needs {tools} (put one within reach or in the loadout)",
|
|
3228
|
+
"hud.spoiled_hole": "{name}: the packaging is holed through, what is inside is contaminated; do not eat or drink it",
|
|
3229
|
+
"hud.spoiled_leak": "{name} leaked: what is left is contaminated; do not eat or drink it",
|
|
2699
3230
|
"hud.refused": "Wrong tool for the {kind}",
|
|
2700
3231
|
"hud.drop": "drop",
|
|
2701
3232
|
"hud.rotate": "rotate",
|
|
@@ -2984,17 +3515,24 @@ var ptBR = {
|
|
|
2984
3515
|
"loading.open_area": "Abrindo {area}\u2026",
|
|
2985
3516
|
"loading.spawn": "Montando {name}\u2026",
|
|
2986
3517
|
"act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
|
|
3518
|
+
"act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
|
|
2987
3519
|
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
2988
3520
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
2989
3521
|
"act.use_item": "{name}: {use}",
|
|
3522
|
+
"act.open_with": "Abriu {name} com {tool}",
|
|
3523
|
+
"act.open_by_hand": "Abriu {name} com a m\xE3o",
|
|
3524
|
+
"act.ate": "Comeu {grams} g de {name}",
|
|
3525
|
+
"act.ate_all": "Comeu todo o conte\xFAdo de {name} ({grams} g): a embalagem ficou vazia",
|
|
2990
3526
|
"act.tap.on": "Abriu a torneira",
|
|
2991
3527
|
"act.tap.off": "Fechou a torneira",
|
|
2992
3528
|
"use.drink": "beber",
|
|
2993
3529
|
"use.sip": "golinho",
|
|
2994
3530
|
"use.chug": "virar",
|
|
3531
|
+
"use.eat": "comer",
|
|
2995
3532
|
"use.pour": "despejar",
|
|
2996
3533
|
"use.pour_out": "esvaziar",
|
|
2997
3534
|
"use.open": "abrir",
|
|
3535
|
+
"use.unscrew": "desenroscar",
|
|
2998
3536
|
"use.close": "fechar",
|
|
2999
3537
|
"hud.reloading": "Recarregando...",
|
|
3000
3538
|
"hud.empty": "Sem muni\xE7\xE3o",
|
|
@@ -3011,15 +3549,32 @@ var ptBR = {
|
|
|
3011
3549
|
"hud.nothing_to_grab": "Nada para pegar aqui",
|
|
3012
3550
|
"hud.cannot_grab": "{name} n\xE3o d\xE1 para pegar",
|
|
3013
3551
|
"hud.too_heavy": "{name} \xE9 pesado demais ({kg} kg)",
|
|
3552
|
+
"hud.door_locked": "{name} est\xE1 trancado(a)",
|
|
3553
|
+
"hud.door_blocked": "{name} bateu em algo e parou ({percent}% aberto)",
|
|
3554
|
+
"hud.door_hint_open": "E: abrir",
|
|
3555
|
+
"hud.door_hint_close": "E: fechar",
|
|
3556
|
+
"hud.door_hint_locked": "Trancado",
|
|
3557
|
+
"act.door_reach": "leva a m\xE3o \xE0 ma\xE7aneta de {name}",
|
|
3558
|
+
"act.door_opened": "abre {name}",
|
|
3559
|
+
"act.door_closed": "fecha {name}",
|
|
3560
|
+
"act.door_blocked": "{name} bate em algo e para em {percent}%",
|
|
3561
|
+
"act.door_locked": "tenta abrir {name}: trancado, s\xF3 chacoalha",
|
|
3562
|
+
"act.door_stepped_back": "d\xE1 um passo para tr\xE1s para {name} passar",
|
|
3014
3563
|
"hud.push_on": "Empurrando {name}: W empurra, S puxa, solte o E para parar",
|
|
3015
3564
|
"hud.push_too_heavy": "{name} n\xE3o se mexe ({kg} kg)",
|
|
3016
3565
|
"hud.push_blocked": "{name} travou: tem algo no caminho",
|
|
3017
3566
|
"act.push_grip": "encosta em {name} ({kg} kg)",
|
|
3018
3567
|
"act.push_engaged": "come\xE7a a empurrar {name} ({kg} kg)",
|
|
3568
|
+
"act.sit": "senta em {name}",
|
|
3569
|
+
"act.lie": "deita em {name}",
|
|
3570
|
+
"act.stand": "levanta de {name}",
|
|
3019
3571
|
"act.push_too_heavy": "faz for\xE7a contra {name}: {kg} kg \xE9 demais",
|
|
3020
3572
|
"act.push_obstacle": "{name} bate em algo e trava",
|
|
3021
3573
|
"act.push_release": "solta {name}",
|
|
3022
3574
|
"hud.empty_container": "{name} est\xE1 vazio",
|
|
3575
|
+
"hud.needs_opener": "Para abrir {name} precisa de {tools} (deixe um ao alcance ou no invent\xE1rio)",
|
|
3576
|
+
"hud.spoiled_hole": "{name}: a embalagem furou, o conte\xFAdo est\xE1 contaminado; n\xE3o d\xE1 para comer nem beber",
|
|
3577
|
+
"hud.spoiled_leak": "{name} vazou: o que sobrou est\xE1 contaminado; n\xE3o d\xE1 para comer nem beber",
|
|
3023
3578
|
"hud.refused": "Ferramenta errada para {kind}",
|
|
3024
3579
|
"hud.drop": "soltar",
|
|
3025
3580
|
"hud.rotate": "girar",
|
|
@@ -3384,9 +3939,9 @@ function createBenchPanel(o) {
|
|
|
3384
3939
|
hasRoom = rows.room.length > 0;
|
|
3385
3940
|
syncDisplay();
|
|
3386
3941
|
roomLine.textContent = rows.room.map((row) => `${row.name} ${row.text}`).join(" \xB7 ");
|
|
3387
|
-
const
|
|
3388
|
-
if (
|
|
3389
|
-
metricKey =
|
|
3942
|
+
const key2 = rows.module.map((row) => `${row.id}:${row.ratio !== void 0}`).join("|");
|
|
3943
|
+
if (key2 !== metricKey) {
|
|
3944
|
+
metricKey = key2;
|
|
3390
3945
|
renderMetrics(rows.module);
|
|
3391
3946
|
}
|
|
3392
3947
|
for (const row of rows.module) {
|
|
@@ -3740,8 +4295,7 @@ function createUi(o) {
|
|
|
3740
4295
|
logBtn(t("ui.download_txt"), () => o.log.download("txt"));
|
|
3741
4296
|
logBtn(t("ui.clear_log"), () => {
|
|
3742
4297
|
o.log.clear();
|
|
3743
|
-
|
|
3744
|
-
nodes.clear();
|
|
4298
|
+
rebuildLog();
|
|
3745
4299
|
});
|
|
3746
4300
|
logPanel.append(logTitle, logBar, logBody);
|
|
3747
4301
|
const MAX_LINES = 200;
|
|
@@ -3754,29 +4308,59 @@ function createUi(o) {
|
|
|
3754
4308
|
const tm = mk3("time", void 0, fmtTime(e.t));
|
|
3755
4309
|
n.append(tm, document.createTextNode(e.text));
|
|
3756
4310
|
};
|
|
3757
|
-
|
|
3758
|
-
|
|
4311
|
+
let pendingAdds = [];
|
|
4312
|
+
const pendingUpdates = /* @__PURE__ */ new Map();
|
|
4313
|
+
let isLogStale = false;
|
|
4314
|
+
const isLogVisible = () => !window.matchMedia?.("(max-width:980px)").matches || root.classList.contains("open");
|
|
4315
|
+
const buildNode = (e) => {
|
|
3759
4316
|
const n = mk3("div");
|
|
3760
4317
|
fill(n, e);
|
|
3761
4318
|
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;
|
|
4319
|
+
return n;
|
|
3769
4320
|
};
|
|
3770
4321
|
function rebuildLog() {
|
|
4322
|
+
pendingAdds = [];
|
|
4323
|
+
pendingUpdates.clear();
|
|
4324
|
+
isLogStale = false;
|
|
3771
4325
|
logBody.textContent = "";
|
|
3772
4326
|
nodes.clear();
|
|
3773
|
-
|
|
4327
|
+
const fragment = document.createDocumentFragment();
|
|
4328
|
+
for (const e of o.log.entries().slice(-MAX_LINES * 3)) if (filters[e.channel]) fragment.appendChild(buildNode(e));
|
|
4329
|
+
logBody.appendChild(fragment);
|
|
4330
|
+
trimLog();
|
|
3774
4331
|
}
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
const
|
|
4332
|
+
function trimLog() {
|
|
4333
|
+
while (logBody.childElementCount > MAX_LINES) {
|
|
4334
|
+
const oldest = nodes.keys().next();
|
|
4335
|
+
if (oldest.done) break;
|
|
4336
|
+
nodes.delete(oldest.value);
|
|
4337
|
+
logBody.firstElementChild?.remove();
|
|
4338
|
+
}
|
|
4339
|
+
if (autoscroll) logBody.scrollTop = logBody.scrollHeight;
|
|
4340
|
+
}
|
|
4341
|
+
const flushLog = () => {
|
|
4342
|
+
if (pendingAdds.length === 0 && pendingUpdates.size === 0) return;
|
|
4343
|
+
if (!isLogVisible()) {
|
|
4344
|
+
isLogStale = true;
|
|
4345
|
+
pendingAdds = [];
|
|
4346
|
+
pendingUpdates.clear();
|
|
4347
|
+
return;
|
|
4348
|
+
}
|
|
4349
|
+
if (isLogStale) return rebuildLog();
|
|
4350
|
+
const fragment = document.createDocumentFragment();
|
|
4351
|
+
for (const e of pendingAdds.slice(-MAX_LINES)) if (filters[e.channel]) fragment.appendChild(buildNode(e));
|
|
4352
|
+
pendingAdds = [];
|
|
4353
|
+
logBody.appendChild(fragment);
|
|
4354
|
+
for (const [seq, e] of pendingUpdates) {
|
|
4355
|
+
const n = nodes.get(seq);
|
|
3778
4356
|
if (n) fill(n, e);
|
|
3779
|
-
}
|
|
4357
|
+
}
|
|
4358
|
+
pendingUpdates.clear();
|
|
4359
|
+
trimLog();
|
|
4360
|
+
};
|
|
4361
|
+
const offLog = o.log.on((e, updated) => {
|
|
4362
|
+
if (updated) pendingUpdates.set(e.seq, e);
|
|
4363
|
+
else pendingAdds.push(e);
|
|
3780
4364
|
});
|
|
3781
4365
|
const right = mk3("div", "tr-right");
|
|
3782
4366
|
const cat = mk3("div", "tr-panel tr-cat");
|
|
@@ -3866,8 +4450,8 @@ function createUi(o) {
|
|
|
3866
4450
|
if (o.kick) ctl(o.kick.label, () => o.kick.onKick());
|
|
3867
4451
|
const fullscreenBtn = ctl(t("ui.fullscreen"), () => void toggleFullscreen());
|
|
3868
4452
|
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.${
|
|
4453
|
+
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key2) => ({ key: key2, b: ctl(t(`ui.blast.${key2}`), () => o.onBlast(key2)) }));
|
|
4454
|
+
for (const { key: key2, b } of blastBtns) b.title = t(`ui.blast.${key2}.hint`);
|
|
3871
4455
|
const qSel = mk3("select");
|
|
3872
4456
|
for (const q of ["low", "medium", "high"]) {
|
|
3873
4457
|
const op = mk3("option", void 0, t(`ui.quality.${q}`));
|
|
@@ -3943,6 +4527,7 @@ function createUi(o) {
|
|
|
3943
4527
|
toggle.type = "button";
|
|
3944
4528
|
const togglePanels = () => {
|
|
3945
4529
|
root.classList.toggle("open");
|
|
4530
|
+
if (isLogStale && isLogVisible()) rebuildLog();
|
|
3946
4531
|
o.onResize();
|
|
3947
4532
|
};
|
|
3948
4533
|
toggle.onclick = togglePanels;
|
|
@@ -4031,6 +4616,7 @@ function createUi(o) {
|
|
|
4031
4616
|
ring.style.display = value === null ? "none" : "block";
|
|
4032
4617
|
if (value !== null) ringFill.style.strokeDashoffset = String(RING_LENGTH * (1 - value));
|
|
4033
4618
|
},
|
|
4619
|
+
flushLog,
|
|
4034
4620
|
hud(s) {
|
|
4035
4621
|
toolBox.textContent = "";
|
|
4036
4622
|
toolBox.append(document.createTextNode(s.toolName));
|
|
@@ -4061,9 +4647,9 @@ function createUi(o) {
|
|
|
4061
4647
|
held.append(mk3("kbd", void 0, k), document.createTextNode(` ${label} `));
|
|
4062
4648
|
}
|
|
4063
4649
|
} else held.style.display = "none";
|
|
4064
|
-
const
|
|
4065
|
-
if (
|
|
4066
|
-
lastSlotsKey =
|
|
4650
|
+
const key2 = s.slots.join("|") + s.page;
|
|
4651
|
+
if (key2 !== lastSlotsKey) {
|
|
4652
|
+
lastSlotsKey = key2;
|
|
4067
4653
|
hotbar.textContent = "";
|
|
4068
4654
|
slotsEl.length = 0;
|
|
4069
4655
|
hudPage = s.page - 1;
|
|
@@ -4080,7 +4666,7 @@ function createUi(o) {
|
|
|
4080
4666
|
slotsEl.forEach((b, i) => b.classList.toggle("on", i === s.slot));
|
|
4081
4667
|
tapBtn.classList.toggle("on", s.tap);
|
|
4082
4668
|
comboBtn?.classList.toggle("on", !!s.combo);
|
|
4083
|
-
for (const { key:
|
|
4669
|
+
for (const { key: key3, b } of blastBtns) b.classList.toggle("on", s.blast[key3]);
|
|
4084
4670
|
count.textContent = t("ui.objects", { n: s.objects });
|
|
4085
4671
|
const ak = JSON.stringify(s.animations);
|
|
4086
4672
|
if (ak !== lastAnim) {
|
|
@@ -4131,7 +4717,7 @@ function createUi(o) {
|
|
|
4131
4717
|
var FIXED_DT = 1 / 60;
|
|
4132
4718
|
var SOLID_SPAN = 3;
|
|
4133
4719
|
var STEP_UP = 0.45;
|
|
4134
|
-
var
|
|
4720
|
+
var BODY_HEIGHT2 = 1.75;
|
|
4135
4721
|
var KICK_MASS = 6;
|
|
4136
4722
|
var KICK_HEIGHT = 0.6;
|
|
4137
4723
|
var GROUND_ID = "room_ground";
|
|
@@ -4143,9 +4729,8 @@ var GROUND_DUST_RADIUS = 0.2;
|
|
|
4143
4729
|
var GROUND_FIRE_EVERY = 0.3;
|
|
4144
4730
|
var SHOVE_SPEED = 0.04;
|
|
4145
4731
|
var METRICS_INTERVAL_S = 0.25;
|
|
4146
|
-
var CARRY_NEAR = 0.62;
|
|
4147
4732
|
var CARRY_PALM = 0.012;
|
|
4148
|
-
var
|
|
4733
|
+
var CAP_RISE = 0.03;
|
|
4149
4734
|
var MAX_STRIDE_SPEED = 10;
|
|
4150
4735
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
4151
4736
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
@@ -4155,8 +4740,8 @@ function createTestRoom(options) {
|
|
|
4155
4740
|
const S = resolveStrings(locale);
|
|
4156
4741
|
const dStrings = resolveDestructionStrings(locale);
|
|
4157
4742
|
const fStrings = resolveFluidStrings(locale);
|
|
4158
|
-
const t = (
|
|
4159
|
-
let s = S.text[
|
|
4743
|
+
const t = (key2, vars) => {
|
|
4744
|
+
let s = S.text[key2] ?? key2;
|
|
4160
4745
|
if (vars) for (const k of Object.keys(vars)) s = s.split(`{${k}}`).join(String(vars[k]));
|
|
4161
4746
|
return s;
|
|
4162
4747
|
};
|
|
@@ -4241,11 +4826,11 @@ function createTestRoom(options) {
|
|
|
4241
4826
|
const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
|
|
4242
4827
|
log.setClock(() => simTime);
|
|
4243
4828
|
const ownAudio = options.audio === void 0;
|
|
4244
|
-
const audio = options.audio === false ? null : options.audio ?? createAudioEngine();
|
|
4829
|
+
const audio = options.audio === false ? null : options.audio ?? createAudioEngine({ deferredStart: {} });
|
|
4245
4830
|
const destructionSound = audio ? createDestructionSound(audio) : null;
|
|
4246
|
-
const playSound = (
|
|
4247
|
-
destructionSound?.onSound(
|
|
4248
|
-
options.onSound?.(
|
|
4831
|
+
const playSound = (key2, point, volume) => {
|
|
4832
|
+
destructionSound?.onSound(key2, point, volume);
|
|
4833
|
+
options.onSound?.(key2, point, volume);
|
|
4249
4834
|
};
|
|
4250
4835
|
const vfx = createVfxRenderer(THREE, {
|
|
4251
4836
|
scene,
|
|
@@ -4255,6 +4840,7 @@ function createTestRoom(options) {
|
|
|
4255
4840
|
pointScale,
|
|
4256
4841
|
vfx: createVfx({
|
|
4257
4842
|
profile: quality,
|
|
4843
|
+
...options.debris?.particles ? { sprites: { kinds: patchSpriteKinds(SPRITE_KINDS, options.debris.particles) } } : {},
|
|
4258
4844
|
groundAt
|
|
4259
4845
|
})
|
|
4260
4846
|
});
|
|
@@ -4265,6 +4851,9 @@ function createTestRoom(options) {
|
|
|
4265
4851
|
quality,
|
|
4266
4852
|
groundAt,
|
|
4267
4853
|
pointScale,
|
|
4854
|
+
...options.debris?.fragmentLife !== void 0 ? { fragmentLife: options.debris.fragmentLife } : {},
|
|
4855
|
+
...options.debris?.maxFragments !== void 0 ? { maxFragments: options.debris.maxFragments } : {},
|
|
4856
|
+
eventBudgetMs: options.debris?.eventBudgetMs ?? 8,
|
|
4268
4857
|
onSound: playSound,
|
|
4269
4858
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
4270
4859
|
// a blast moves what is heavy but not nailed down: the renderer makes it a piece the moment the shove reaches it
|
|
@@ -4272,13 +4861,13 @@ function createTestRoom(options) {
|
|
|
4272
4861
|
const e = entries.get(objectId);
|
|
4273
4862
|
return e?.free === true && !e.piece;
|
|
4274
4863
|
},
|
|
4275
|
-
onLoosened: (objectId, piece) => {
|
|
4864
|
+
onLoosened: (objectId, piece, cause) => {
|
|
4276
4865
|
const e = entries.get(objectId);
|
|
4277
4866
|
if (!e) return;
|
|
4278
4867
|
e.piece = piece;
|
|
4279
4868
|
leaveFootprint(objectId);
|
|
4280
4869
|
rendererPieces.set(objectId, piece);
|
|
4281
|
-
action("loosened", t("act.loosened", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
4870
|
+
action("loosened", t(cause === "blast" ? "act.loosened" : "act.came_apart", { name: e.name }), { objectId, itemId: e.so.itemId });
|
|
4282
4871
|
},
|
|
4283
4872
|
onPiece: (piece) => {
|
|
4284
4873
|
rendererPieces.set(piece.objectId, piece);
|
|
@@ -4494,7 +5083,7 @@ function createTestRoom(options) {
|
|
|
4494
5083
|
const dz = Math.max(s.min[2] - z, 0, z - s.max[2]);
|
|
4495
5084
|
if (dx * dx + dz * dz > radius * radius) continue;
|
|
4496
5085
|
if (s.max[1] <= feet + STEP_UP) floor = Math.max(floor, s.max[1]);
|
|
4497
|
-
else if (s.min[1] < feet +
|
|
5086
|
+
else if (s.min[1] < feet + BODY_HEIGHT2) blocked = true;
|
|
4498
5087
|
}
|
|
4499
5088
|
return { floor, blocked };
|
|
4500
5089
|
};
|
|
@@ -4568,7 +5157,7 @@ function createTestRoom(options) {
|
|
|
4568
5157
|
let best = null;
|
|
4569
5158
|
let bestDistance = PUSH_REACH;
|
|
4570
5159
|
for (const e of entries.values()) {
|
|
4571
|
-
if (e.
|
|
5160
|
+
if (!e.pushable || !e.piece && !e.free) continue;
|
|
4572
5161
|
const b = bodyBox(e);
|
|
4573
5162
|
if (!b) continue;
|
|
4574
5163
|
const dx = Math.max(b.minX, Math.min(b.maxX, player.x)) - player.x;
|
|
@@ -4592,7 +5181,7 @@ function createTestRoom(options) {
|
|
|
4592
5181
|
aim();
|
|
4593
5182
|
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
4594
5183
|
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 };
|
|
5184
|
+
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
5185
|
return leanedOn();
|
|
4597
5186
|
},
|
|
4598
5187
|
massOf: (objectId) => massOfObject(objectId),
|
|
@@ -4615,9 +5204,96 @@ function createTestRoom(options) {
|
|
|
4615
5204
|
else if (ev.type === "too_heavy") say(t("hud.push_too_heavy", vars));
|
|
4616
5205
|
else if (ev.type === "obstacle") say(t("hud.push_blocked", vars));
|
|
4617
5206
|
},
|
|
4618
|
-
ring: (value) => ui?.holdRing(value)
|
|
5207
|
+
ring: (value) => ui?.holdRing(value),
|
|
5208
|
+
hasBody: () => !!avatar?.eye
|
|
5209
|
+
});
|
|
5210
|
+
const seatHolder = (objectId) => {
|
|
5211
|
+
const e = entries.get(objectId);
|
|
5212
|
+
const seats = e?.item?.seats;
|
|
5213
|
+
if (!e || !seats?.length || !world.has(objectId)) return null;
|
|
5214
|
+
return { objectId, root: rendererPieces.get(objectId)?.root ?? e.so.root, seats };
|
|
5215
|
+
};
|
|
5216
|
+
const seating = createSeating({
|
|
5217
|
+
THREE,
|
|
5218
|
+
player,
|
|
5219
|
+
pick: () => {
|
|
5220
|
+
aim();
|
|
5221
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], 3.2);
|
|
5222
|
+
return r ? seatHolder(r.object) : null;
|
|
5223
|
+
},
|
|
5224
|
+
find: seatHolder,
|
|
5225
|
+
hasBody: () => !!avatar?.eye,
|
|
5226
|
+
groundAt: (x, z) => groundAt(x, z),
|
|
5227
|
+
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 } }),
|
|
5228
|
+
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
5229
|
});
|
|
4620
5230
|
let pushByKey = false;
|
|
5231
|
+
const leafCache = /* @__PURE__ */ new WeakMap();
|
|
5232
|
+
const leavesOf = (objectId) => {
|
|
5233
|
+
const item = entries.get(objectId)?.item;
|
|
5234
|
+
if (!item?.animations?.length) return [];
|
|
5235
|
+
const cached = leafCache.get(item);
|
|
5236
|
+
if (cached) return cached;
|
|
5237
|
+
const byPart = /* @__PURE__ */ new Map();
|
|
5238
|
+
for (const clip of item.animations) {
|
|
5239
|
+
if (!clip.id.startsWith("open_")) continue;
|
|
5240
|
+
const parts = /* @__PURE__ */ new Set();
|
|
5241
|
+
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) parts.add(id);
|
|
5242
|
+
for (const part of parts) {
|
|
5243
|
+
const had = byPart.get(part);
|
|
5244
|
+
if (!had || parts.size < had.n) byPart.set(part, { clip, n: parts.size });
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
const leaves = [...byPart].map(([part, v]) => ({ part, clip: v.clip }));
|
|
5248
|
+
leafCache.set(item, leaves);
|
|
5249
|
+
return leaves;
|
|
5250
|
+
};
|
|
5251
|
+
const lockedLeaves = /* @__PURE__ */ new Set();
|
|
5252
|
+
const doors = createRoomDoors({
|
|
5253
|
+
THREE,
|
|
5254
|
+
player,
|
|
5255
|
+
world,
|
|
5256
|
+
camera,
|
|
5257
|
+
arena: { limit: arenaSize, colliders: standingColliders, ids: standingIds },
|
|
5258
|
+
...options.doors ? { rules: options.doors } : {},
|
|
5259
|
+
pick: () => {
|
|
5260
|
+
if (heldPiece) return [];
|
|
5261
|
+
aim();
|
|
5262
|
+
const hits = [];
|
|
5263
|
+
const r = rayWorld([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], DOOR_REACH);
|
|
5264
|
+
if (r) hits.push({ objectId: r.object, part: r.part, point: r.point });
|
|
5265
|
+
if (thirdActive()) {
|
|
5266
|
+
camera.getWorldDirection(tmpV2);
|
|
5267
|
+
const e = rayWorld([aimO[0], aimO[1], aimO[2]], [tmpV2.x, tmpV2.y, tmpV2.z], DOOR_REACH);
|
|
5268
|
+
if (e) hits.push({ objectId: e.object, part: e.part, point: e.point });
|
|
5269
|
+
}
|
|
5270
|
+
return hits;
|
|
5271
|
+
},
|
|
5272
|
+
leaves: leavesOf,
|
|
5273
|
+
objectOf: (objectId, part) => entries.get(objectId)?.so.root.getObjectByName(part) ?? null,
|
|
5274
|
+
pose: (objectId, part, clip, fraction) => {
|
|
5275
|
+
const e = entries.get(objectId);
|
|
5276
|
+
const obj = e?.so.root.getObjectByName(part);
|
|
5277
|
+
if (!e || !obj) return;
|
|
5278
|
+
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] });
|
|
5279
|
+
const rest = restPose.get(obj);
|
|
5280
|
+
const pose = poseAt(clip, part, Math.max(0, Math.min(1, fraction)) * clip.duration);
|
|
5281
|
+
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
5282
|
+
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
5283
|
+
followPart(e, part, obj, rest);
|
|
5284
|
+
},
|
|
5285
|
+
isLocked: (objectId, part) => lockedLeaves.has(`${objectId}\0*`) || lockedLeaves.has(`${objectId}\0${part}`),
|
|
5286
|
+
nameOf,
|
|
5287
|
+
impactKey: (objectId, part) => impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")),
|
|
5288
|
+
sound: playSound,
|
|
5289
|
+
onEvent: (ev) => {
|
|
5290
|
+
const vars = { name: ev.name, percent: Math.round(ev.opening * 100) };
|
|
5291
|
+
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 } : {} } });
|
|
5292
|
+
fire("door", ev);
|
|
5293
|
+
if (ev.type === "locked") say(t("hud.door_locked", vars));
|
|
5294
|
+
else if (ev.type === "blocked") say(t("hud.door_blocked", vars));
|
|
5295
|
+
}
|
|
5296
|
+
});
|
|
4621
5297
|
const massOfObject = (id) => {
|
|
4622
5298
|
const st = world.state(id);
|
|
4623
5299
|
let m = 0;
|
|
@@ -4643,6 +5319,8 @@ function createTestRoom(options) {
|
|
|
4643
5319
|
const q = root.getWorldQuaternion(tmpQ);
|
|
4644
5320
|
const p = tmpV.set(e.fluidOffset[0], e.fluidOffset[1], e.fluidOffset[2]).applyMatrix4(root.matrixWorld);
|
|
4645
5321
|
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] });
|
|
5322
|
+
e.fluidStart = f.container.liters;
|
|
5323
|
+
e.fluidUsed = 0;
|
|
4646
5324
|
fluidView.bindContainer(e.containerId, root, { innerOffset: e.fluidOffset });
|
|
4647
5325
|
}
|
|
4648
5326
|
return e;
|
|
@@ -4663,6 +5341,7 @@ function createTestRoom(options) {
|
|
|
4663
5341
|
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
5342
|
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
5343
|
if (o.free) e.free = true;
|
|
5344
|
+
if (o.pushable) e.pushable = true;
|
|
4666
5345
|
e.follower = o.follow?.(root, world, o.id) || null;
|
|
4667
5346
|
}
|
|
4668
5347
|
};
|
|
@@ -4762,7 +5441,7 @@ function createTestRoom(options) {
|
|
|
4762
5441
|
if (movable) {
|
|
4763
5442
|
const box2 = new THREE.Box3().setFromObject(root);
|
|
4764
5443
|
const base = Math.max(pos[1], groundAt(pos[0], pos[2]));
|
|
4765
|
-
pos = [pos[0], pos[1] + (base - box2.min.y), pos[2]];
|
|
5444
|
+
if (!box2.isEmpty()) pos = [pos[0], pos[1] + (base - box2.min.y), pos[2]];
|
|
4766
5445
|
root.position.set(pos[0], pos[1], pos[2]);
|
|
4767
5446
|
root.updateMatrixWorld(true);
|
|
4768
5447
|
}
|
|
@@ -4771,6 +5450,7 @@ function createTestRoom(options) {
|
|
|
4771
5450
|
else world.add(spec, { id: objectId, position: pos, rotation: [q.x, q.y, q.z, q.w] });
|
|
4772
5451
|
const e = registerEntry(objectId, item.id, root, spec, item.name, parts, item, false, movable);
|
|
4773
5452
|
if (!own && !movable && item.movable === void 0) e.free = true;
|
|
5453
|
+
if (item.pushable) e.pushable = true;
|
|
4774
5454
|
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
5455
|
fire("spawn", e.so);
|
|
4776
5456
|
return e.so;
|
|
@@ -4873,6 +5553,7 @@ function createTestRoom(options) {
|
|
|
4873
5553
|
e.so.root.removeFromParent();
|
|
4874
5554
|
letGo(e.so.root);
|
|
4875
5555
|
entries.delete(id);
|
|
5556
|
+
doors.forget(id);
|
|
4876
5557
|
rendererPieces.delete(id);
|
|
4877
5558
|
specs.delete(id);
|
|
4878
5559
|
};
|
|
@@ -4914,6 +5595,7 @@ function createTestRoom(options) {
|
|
|
4914
5595
|
fluids.setTap(TAP_ID, false);
|
|
4915
5596
|
};
|
|
4916
5597
|
const clearListeners = /* @__PURE__ */ new Set();
|
|
5598
|
+
const resetListeners = /* @__PURE__ */ new Set();
|
|
4917
5599
|
const clear = () => {
|
|
4918
5600
|
clearEverything();
|
|
4919
5601
|
for (const listener of [...clearListeners]) listener();
|
|
@@ -4936,10 +5618,13 @@ function createTestRoom(options) {
|
|
|
4936
5618
|
action("reset", t("act.reset"));
|
|
4937
5619
|
};
|
|
4938
5620
|
const hardReset = () => {
|
|
5621
|
+
seating.release();
|
|
4939
5622
|
pushByKey = false;
|
|
4940
5623
|
push.end();
|
|
4941
5624
|
clearEverything();
|
|
4942
5625
|
for (const id of [...entries.keys()]) removeEntry(id);
|
|
5626
|
+
renderer.reset();
|
|
5627
|
+
for (const listener of [...resetListeners]) listener();
|
|
4943
5628
|
tools.clear();
|
|
4944
5629
|
tool = toolState(toolId);
|
|
4945
5630
|
if (pristineWorld) {
|
|
@@ -4954,6 +5639,8 @@ function createTestRoom(options) {
|
|
|
4954
5639
|
pending = [];
|
|
4955
5640
|
strikes = [];
|
|
4956
5641
|
rng = 12345;
|
|
5642
|
+
filmRandom.seed = 1;
|
|
5643
|
+
groundFireClock = 0;
|
|
4957
5644
|
counter = 0;
|
|
4958
5645
|
slot = 0;
|
|
4959
5646
|
prevVy.clear();
|
|
@@ -4987,8 +5674,8 @@ function createTestRoom(options) {
|
|
|
4987
5674
|
const thirdActive = () => cameraMode === "third" && avatar !== null;
|
|
4988
5675
|
const syncAvatarVisible = () => {
|
|
4989
5676
|
if (!avatar) return;
|
|
4990
|
-
avatar.root.visible = cameraMode === "third";
|
|
4991
|
-
|
|
5677
|
+
avatar.root.visible = cameraMode === "third" || !!avatar.eye;
|
|
5678
|
+
avatar.setView?.(cameraMode, camera);
|
|
4992
5679
|
};
|
|
4993
5680
|
const setCameraMode = (mode) => {
|
|
4994
5681
|
if (mode !== "first" && mode !== "third") return;
|
|
@@ -5001,13 +5688,13 @@ function createTestRoom(options) {
|
|
|
5001
5688
|
};
|
|
5002
5689
|
const setAvatar = (next) => {
|
|
5003
5690
|
if (avatar === next) return;
|
|
5691
|
+
avatar?.setView?.("third", null);
|
|
5004
5692
|
avatar?.root.removeFromParent();
|
|
5005
|
-
avatar?.firstPerson?.root.removeFromParent();
|
|
5006
5693
|
avatar = next;
|
|
5007
5694
|
feetPrev = null;
|
|
5008
5695
|
if (avatar) {
|
|
5696
|
+
avatar.root.userData.viewmodel = true;
|
|
5009
5697
|
scene.add(avatar.root);
|
|
5010
|
-
if (avatar.firstPerson) scene.add(avatar.firstPerson.root);
|
|
5011
5698
|
syncAvatarVisible();
|
|
5012
5699
|
stepAvatar(0);
|
|
5013
5700
|
}
|
|
@@ -5029,34 +5716,73 @@ function createTestRoom(options) {
|
|
|
5029
5716
|
avatarState.pitch = player.pitch;
|
|
5030
5717
|
avatarState.holding = heldPiece !== null;
|
|
5031
5718
|
avatarState.push = push.avatarState;
|
|
5032
|
-
|
|
5719
|
+
const seat = seating.seat;
|
|
5720
|
+
avatarState.seat = seat ? { pose: seat.pose, hip: seat.hips[1] - player.feet } : null;
|
|
5721
|
+
avatarState.door = doors.avatarState;
|
|
5722
|
+
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
5723
|
avatar.root.position.set(x, player.feet, z);
|
|
5034
5724
|
avatar.root.rotation.set(0, player.yaw + Math.PI, 0);
|
|
5035
5725
|
avatar.update(dt, avatarState);
|
|
5036
5726
|
avatar.root.updateMatrixWorld(true);
|
|
5727
|
+
placeEye();
|
|
5728
|
+
};
|
|
5729
|
+
let scriptedAt = null;
|
|
5730
|
+
const lookAt = (eye, target) => {
|
|
5731
|
+
player.look(eye, target);
|
|
5732
|
+
scriptedAt = [player.x, player.z];
|
|
5733
|
+
};
|
|
5734
|
+
const EYE_CLEARANCE = 0.1;
|
|
5735
|
+
const eyeAt = new THREE.Vector3();
|
|
5736
|
+
const placeEye = () => {
|
|
5737
|
+
if (!avatar?.eye) return;
|
|
5738
|
+
if (scriptedAt) {
|
|
5739
|
+
const [walkX, walkZ] = player.walkIntent;
|
|
5740
|
+
if (!walkX && !walkZ && Math.hypot(player.x - scriptedAt[0], player.z - scriptedAt[1]) < 1e-6) return;
|
|
5741
|
+
scriptedAt = null;
|
|
5742
|
+
}
|
|
5743
|
+
avatar.eye(eyeAt);
|
|
5744
|
+
const dx = eyeAt.x - player.x;
|
|
5745
|
+
const dz = eyeAt.z - player.z;
|
|
5746
|
+
const ahead = Math.hypot(dx, dz);
|
|
5747
|
+
if (ahead > 1e-4) {
|
|
5748
|
+
const dir = [dx / ahead, 0, dz / ahead];
|
|
5749
|
+
const hit = rayWorld([player.x, eyeAt.y, player.z], dir, ahead + EYE_CLEARANCE);
|
|
5750
|
+
if (hit) {
|
|
5751
|
+
const keep = Math.max(0, hit.distance - EYE_CLEARANCE);
|
|
5752
|
+
eyeAt.set(player.x + dir[0] * keep, eyeAt.y, player.z + dir[2] * keep);
|
|
5753
|
+
}
|
|
5754
|
+
}
|
|
5755
|
+
camera.position.copy(eyeAt);
|
|
5756
|
+
camera.updateMatrixWorld(true);
|
|
5037
5757
|
};
|
|
5038
5758
|
let handSource = null;
|
|
5039
|
-
let carriedHand = null;
|
|
5040
5759
|
let debugView = null;
|
|
5041
5760
|
let lastHand = { stance: "relaxed", triggerFinger: null, targets: null };
|
|
5042
5761
|
const strikeClips = options.strikeClips ?? DEFAULT_STRIKE_CLIPS;
|
|
5043
5762
|
const handStances = options.handStances ?? DEFAULT_HAND_STANCES;
|
|
5044
|
-
const
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5763
|
+
const SHOULDER_POCKET_IN = 0.1;
|
|
5764
|
+
const pocketAt = new THREE.Vector3();
|
|
5765
|
+
const aimTurn = new THREE.Quaternion();
|
|
5766
|
+
const aimEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
5767
|
+
const bodyHoldFrame = () => {
|
|
5768
|
+
const shoulder = avatar?.shoulder?.("R");
|
|
5769
|
+
if (!shoulder) return null;
|
|
5770
|
+
shoulder.getWorldPosition(pocketAt);
|
|
5771
|
+
pocketAt.x -= Math.cos(player.yaw) * SHOULDER_POCKET_IN;
|
|
5772
|
+
pocketAt.z += Math.sin(player.yaw) * SHOULDER_POCKET_IN;
|
|
5773
|
+
aimTurn.setFromEuler(aimEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
5774
|
+
return { position: [pocketAt.x, pocketAt.y, pocketAt.z], quaternion: [aimTurn.x, aimTurn.y, aimTurn.z, aimTurn.w] };
|
|
5775
|
+
};
|
|
5776
|
+
const reachAvatar = () => {
|
|
5777
|
+
const doorHand = doors.handTargets();
|
|
5778
|
+
const targets = push.active ? null : heldPiece ? twistHand : handSource?.() ?? null;
|
|
5779
|
+
lastHand = {
|
|
5780
|
+
stance: push.active || doorHand ? "hold" : handStances[toolId] ?? (targets ? "hold" : "relaxed"),
|
|
5781
|
+
triggerFinger: targets?.fingers?.index ?? null,
|
|
5782
|
+
targets: push.handTargets() ?? doorHand ?? targets
|
|
5057
5783
|
};
|
|
5058
|
-
|
|
5059
|
-
|
|
5784
|
+
if (!avatar?.reach) return;
|
|
5785
|
+
avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
|
|
5060
5786
|
};
|
|
5061
5787
|
const applyThirdView = () => {
|
|
5062
5788
|
if (!thirdActive()) return () => void 0;
|
|
@@ -5562,9 +6288,9 @@ function createTestRoom(options) {
|
|
|
5562
6288
|
}
|
|
5563
6289
|
let blastOptions = { ...options.blast };
|
|
5564
6290
|
let lastBlast;
|
|
5565
|
-
const blastOn = (
|
|
5566
|
-
const toggleBlast = (
|
|
5567
|
-
blastOptions = { ...blastOptions, [
|
|
6291
|
+
const blastOn = (key2) => Boolean(blastOptions[key2]);
|
|
6292
|
+
const toggleBlast = (key2) => {
|
|
6293
|
+
blastOptions = { ...blastOptions, [key2]: blastOn(key2) ? false : true };
|
|
5568
6294
|
};
|
|
5569
6295
|
const explosives = createExplosives({
|
|
5570
6296
|
THREE,
|
|
@@ -5695,10 +6421,12 @@ function createTestRoom(options) {
|
|
|
5695
6421
|
let tiltKey = 0;
|
|
5696
6422
|
const heldLocal = new THREE.Vector3();
|
|
5697
6423
|
const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
|
|
5698
|
-
let heldRadius = 0.2;
|
|
5699
6424
|
const heldVel = [0, 0, 0];
|
|
5700
6425
|
let useItemPlayer = null;
|
|
5701
6426
|
let useItemName = "";
|
|
6427
|
+
let afterOpening = null;
|
|
6428
|
+
let twisting = null;
|
|
6429
|
+
let twistHand = null;
|
|
5702
6430
|
const releaseHeld = () => {
|
|
5703
6431
|
if (heldPiece) {
|
|
5704
6432
|
heldPiece.held = false;
|
|
@@ -5707,6 +6435,9 @@ function createTestRoom(options) {
|
|
|
5707
6435
|
}
|
|
5708
6436
|
useItemPlayer?.cancel();
|
|
5709
6437
|
useItemPlayer = null;
|
|
6438
|
+
afterOpening = null;
|
|
6439
|
+
twisting = null;
|
|
6440
|
+
twistHand = null;
|
|
5710
6441
|
heldEntry = null;
|
|
5711
6442
|
heldPiece = null;
|
|
5712
6443
|
heldTilt = 0;
|
|
@@ -5753,7 +6484,6 @@ function createTestRoom(options) {
|
|
|
5753
6484
|
box2.getSize(heldSize);
|
|
5754
6485
|
piece.root.quaternion.copy(savedQ);
|
|
5755
6486
|
piece.root.updateMatrixWorld(true);
|
|
5756
|
-
heldRadius = heldSize.length() / 3;
|
|
5757
6487
|
heldYaw = 0;
|
|
5758
6488
|
heldTilt = 0;
|
|
5759
6489
|
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
@@ -5774,52 +6504,28 @@ function createTestRoom(options) {
|
|
|
5774
6504
|
fire("throw", { objectId: id, speed });
|
|
5775
6505
|
return true;
|
|
5776
6506
|
};
|
|
5777
|
-
const
|
|
5778
|
-
|
|
5779
|
-
const
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
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);
|
|
5789
|
-
if (useItemName === "open" || useItemName === "close") {
|
|
5790
|
-
const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
|
|
5791
|
-
const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
|
|
5792
|
-
const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
|
|
5793
|
-
const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
|
|
5794
|
-
const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
|
|
5795
|
-
return {
|
|
5796
|
-
R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
|
|
5797
|
-
L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
|
|
5798
|
-
grip: 0.85,
|
|
5799
|
-
gripL: 0.8
|
|
5800
|
-
};
|
|
5801
|
-
}
|
|
5802
|
-
return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
|
|
5803
|
-
}
|
|
5804
|
-
const box2 = new THREE.Box3().setFromObject(root);
|
|
5805
|
-
const center = box2.getCenter(new THREE.Vector3());
|
|
5806
|
-
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
|
|
5807
|
-
const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
|
|
5808
|
-
const z = new THREE.Vector3().crossVectors(forward, back).normalize();
|
|
5809
|
-
const y = new THREE.Vector3().crossVectors(z, forward).normalize();
|
|
5810
|
-
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
|
|
5811
|
-
return { R: { position: [center.x, box2.min.y - CARRY_PALM, center.z], quaternion: [q.x, q.y, q.z, q.w] }, grip: CARRY_GRIP };
|
|
6507
|
+
const twistTargets = (root, gripQ) => {
|
|
6508
|
+
if (!twisting) return null;
|
|
6509
|
+
const { stroke, lift } = twisting;
|
|
6510
|
+
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(root.quaternion);
|
|
6511
|
+
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);
|
|
6512
|
+
const knuckles = new THREE.Vector3(0.85, 0, -0.5).normalize().applyQuaternion(gripQ);
|
|
6513
|
+
knuckles.addScaledVector(up, -knuckles.dot(up)).normalize().applyAxisAngle(up, stroke.angle);
|
|
6514
|
+
const side = new THREE.Vector3().crossVectors(knuckles, up).normalize();
|
|
6515
|
+
const palm = top.addScaledVector(knuckles, -0.025);
|
|
6516
|
+
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(knuckles, up, side));
|
|
6517
|
+
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 };
|
|
5812
6518
|
};
|
|
5813
6519
|
const gripQuat = new THREE.Quaternion();
|
|
5814
6520
|
const followHeld = (dt) => {
|
|
5815
6521
|
const piece = heldPiece;
|
|
5816
|
-
|
|
6522
|
+
twistHand = null;
|
|
5817
6523
|
if (!piece) return;
|
|
5818
6524
|
camera.updateMatrixWorld(true);
|
|
5819
6525
|
const root = piece.root;
|
|
5820
6526
|
let pos;
|
|
5821
6527
|
const targetQ = tmpQ;
|
|
5822
|
-
const hand =
|
|
6528
|
+
const hand = avatar?.hand?.("R");
|
|
5823
6529
|
const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
|
|
5824
6530
|
if (hand) {
|
|
5825
6531
|
const handWorld = hand.getWorldPosition(tmpHand);
|
|
@@ -5835,7 +6541,7 @@ function createTestRoom(options) {
|
|
|
5835
6541
|
pos = tmpV.copy(handWorld).add(gripOffset);
|
|
5836
6542
|
} else if (useItemPlayer) {
|
|
5837
6543
|
const pose = useItemPlayer.pose();
|
|
5838
|
-
const frameQ =
|
|
6544
|
+
const frameQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5839
6545
|
const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
|
|
5840
6546
|
const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
|
|
5841
6547
|
const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
|
|
@@ -5848,19 +6554,10 @@ function createTestRoom(options) {
|
|
|
5848
6554
|
targetQ.copy(frameQ).multiply(eq);
|
|
5849
6555
|
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5850
6556
|
} else {
|
|
5851
|
-
|
|
5852
|
-
|
|
5853
|
-
const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
|
|
5854
|
-
const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
|
|
5855
|
-
pos = tmpV.set(holdX, holdY, holdZ).applyMatrix4(camera.matrixWorld);
|
|
5856
|
-
const baseQ = avatar?.firstPerson && small ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5857
|
-
gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
|
|
6557
|
+
pos = tmpV.set(0.05, -0.28, -1.15).applyMatrix4(camera.matrixWorld);
|
|
6558
|
+
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)));
|
|
5858
6559
|
targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
|
|
5859
|
-
|
|
5860
|
-
targetQ.copy(camera.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5861
|
-
} else {
|
|
5862
|
-
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5863
|
-
}
|
|
6560
|
+
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5864
6561
|
}
|
|
5865
6562
|
const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
|
|
5866
6563
|
const px = root.position.x;
|
|
@@ -5875,8 +6572,50 @@ function createTestRoom(options) {
|
|
|
5875
6572
|
heldVel[2] = (root.position.z - pz) / dt;
|
|
5876
6573
|
}
|
|
5877
6574
|
root.updateMatrixWorld(true);
|
|
5878
|
-
|
|
6575
|
+
twistHand = twistTargets(root, gripQuat);
|
|
6576
|
+
};
|
|
6577
|
+
const TOOL_REACH = 1.6;
|
|
6578
|
+
const toolsAtHand = () => {
|
|
6579
|
+
const here = player.position;
|
|
6580
|
+
const near = [...loadout];
|
|
6581
|
+
for (const other of entries.values()) {
|
|
6582
|
+
if (other === heldEntry || other.arena) continue;
|
|
6583
|
+
const p = other.so.root.position;
|
|
6584
|
+
if (Math.hypot(p.x - here[0], p.z - here[2]) <= TOOL_REACH) near.push(other.so.itemId);
|
|
6585
|
+
}
|
|
6586
|
+
return near;
|
|
6587
|
+
};
|
|
6588
|
+
const sealLoss = (e) => {
|
|
6589
|
+
if (e.sealLoss !== void 0) return e.sealLoss;
|
|
6590
|
+
const c = e.containerId ? fluids.container(e.containerId) : void 0;
|
|
6591
|
+
return c && e.fluidStart !== void 0 ? Math.max(0, e.fluidStart - c.liters - (e.fluidUsed ?? 0)) : 0;
|
|
6592
|
+
};
|
|
6593
|
+
const sealState = (e) => {
|
|
6594
|
+
const breached = world.state(e.so.objectId)?.parts.some((p) => p.holes.some((h) => h.through && !h.vent)) ?? false;
|
|
6595
|
+
const leaked = e.fluidStart !== void 0 && sealLoss(e) > Math.max(2e-3, e.fluidStart * 0.02);
|
|
6596
|
+
return { opened: !!e.opened, breached, leaked };
|
|
6597
|
+
};
|
|
6598
|
+
const openSeal = (e) => {
|
|
6599
|
+
const seal = e.item?.seal;
|
|
6600
|
+
if (!seal || e.opened) return true;
|
|
6601
|
+
const opener = sealOpener(seal, toolsAtHand());
|
|
6602
|
+
if (!opener.ok) {
|
|
6603
|
+
const listed = (seal.openWith ?? []).map((id) => catalogMap.get(id)?.name).filter((n) => !!n);
|
|
6604
|
+
const names = [...new Set(listed.length ? listed : (seal.openWith ?? []).filter((id) => arsenal.weapons.has(id)).map((id) => toolName(id)))];
|
|
6605
|
+
const why = t("hud.needs_opener", { name: e.name, tools: names.join(", ") });
|
|
6606
|
+
say(why);
|
|
6607
|
+
action("sealed", why, { objectId: e.so.objectId, itemId: e.so.itemId, data: { openWith: [...seal.openWith ?? []] } });
|
|
6608
|
+
return false;
|
|
6609
|
+
}
|
|
6610
|
+
e.sealLoss = sealLoss(e);
|
|
6611
|
+
e.opened = true;
|
|
6612
|
+
if (e.containerId) fluids.open(e.containerId);
|
|
6613
|
+
seal.open?.(wrapped.get(e.so.root) ?? e.so.root);
|
|
6614
|
+
const tool2 = opener.tool ? catalogMap.get(opener.tool)?.name ?? (arsenal.weapons.has(opener.tool) ? toolName(opener.tool) : opener.tool) : void 0;
|
|
6615
|
+
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" } });
|
|
6616
|
+
return true;
|
|
5879
6617
|
};
|
|
6618
|
+
const crouchOnC = (options.crouch?.keys ?? ["KeyC"]).includes("KeyC");
|
|
5880
6619
|
const useItem = (useId) => {
|
|
5881
6620
|
const e = heldEntry;
|
|
5882
6621
|
if (!e?.containerId) return false;
|
|
@@ -5885,25 +6624,90 @@ function createTestRoom(options) {
|
|
|
5885
6624
|
const c = fluids.container(e.containerId);
|
|
5886
6625
|
if (!c) return false;
|
|
5887
6626
|
if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
|
|
5888
|
-
const
|
|
6627
|
+
const eating = useId === "drink" || useId === "sip" || useId === "chug" || useId === "eat";
|
|
6628
|
+
const consuming = eating || useId === "pour" || useId === "pour_out";
|
|
6629
|
+
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
6630
|
+
const seal = e.item?.seal;
|
|
6631
|
+
if (seal && (consuming || useId === "open")) {
|
|
6632
|
+
const state = sealState(e);
|
|
6633
|
+
const spoiled = sealBlock(seal, { ...state, opened: true });
|
|
6634
|
+
if (eating && spoiled) {
|
|
6635
|
+
say(t(spoiled === "leaked" ? "hud.spoiled_leak" : "hud.spoiled_hole", { name: e.name }));
|
|
6636
|
+
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 } });
|
|
6637
|
+
return false;
|
|
6638
|
+
}
|
|
6639
|
+
if (!e.opened) {
|
|
6640
|
+
const opening = seal.byHand && seal.openUse ? itemUseDefs.find((u) => u.id === seal.openUse) : void 0;
|
|
6641
|
+
if (opening) {
|
|
6642
|
+
useItemPlayer = createItemUse(fluids, e.containerId, opening);
|
|
6643
|
+
useItemName = opening.id;
|
|
6644
|
+
afterOpening = useId === "open" ? null : useId;
|
|
6645
|
+
return true;
|
|
6646
|
+
}
|
|
6647
|
+
if (!openSeal(e)) return false;
|
|
6648
|
+
if (!seal.byHand || useId === "open") {
|
|
6649
|
+
useItemPlayer = createItemUse(fluids, e.containerId, itemUseDefs.find((u) => u.id === "open") ?? def);
|
|
6650
|
+
useItemName = "open";
|
|
6651
|
+
return true;
|
|
6652
|
+
}
|
|
6653
|
+
} else if (useId === "open") return false;
|
|
6654
|
+
}
|
|
5889
6655
|
if (consuming && c.liters <= 1e-6) {
|
|
5890
6656
|
say(t("hud.empty_container", { name: e.name }));
|
|
5891
6657
|
return false;
|
|
5892
6658
|
}
|
|
5893
|
-
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
5894
6659
|
if (consuming && c.closed) fluids.open(e.containerId);
|
|
5895
6660
|
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
5896
6661
|
useItemName = def.id;
|
|
6662
|
+
if (eating) useLiters = c.liters;
|
|
5897
6663
|
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 } });
|
|
5898
6664
|
return true;
|
|
5899
6665
|
};
|
|
6666
|
+
let useLiters = null;
|
|
5900
6667
|
const updateItemUse = (dt) => {
|
|
5901
6668
|
if (!useItemPlayer) return;
|
|
6669
|
+
const e = heldEntry;
|
|
6670
|
+
const c = e?.containerId ? fluids.container(e.containerId) : void 0;
|
|
6671
|
+
const before = c?.liters;
|
|
5902
6672
|
useItemPlayer.update(dt);
|
|
6673
|
+
const seal = e?.item?.seal;
|
|
6674
|
+
twisting = null;
|
|
6675
|
+
if (e && seal?.openUse === useItemName && !e.opened) {
|
|
6676
|
+
twisting = openingProgress(useItemPlayer.def, useItemPlayer.time);
|
|
6677
|
+
seal.turn?.(wrapped.get(e.so.root) ?? e.so.root, { turned: twisting.stroke.turned, lift: twisting.lift });
|
|
6678
|
+
if (twisting.lift >= 1 || useItemPlayer.done) {
|
|
6679
|
+
openSeal(e);
|
|
6680
|
+
twisting = null;
|
|
6681
|
+
}
|
|
6682
|
+
}
|
|
6683
|
+
if (e && useLiters !== null && before !== void 0) {
|
|
6684
|
+
const after = (e.containerId ? fluids.container(e.containerId)?.liters : void 0) ?? before;
|
|
6685
|
+
e.fluidUsed = (e.fluidUsed ?? 0) + Math.max(0, before - after);
|
|
6686
|
+
if (e.opened && e.fluidStart) e.item?.seal?.contents?.(wrapped.get(e.so.root) ?? e.so.root, after / e.fluidStart);
|
|
6687
|
+
}
|
|
5903
6688
|
if (useItemPlayer.done) {
|
|
6689
|
+
if (e && useLiters !== null && useItemName === "eat") {
|
|
6690
|
+
const left = (e.containerId ? fluids.container(e.containerId)?.liters : void 0) ?? 0;
|
|
6691
|
+
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 } });
|
|
6692
|
+
}
|
|
6693
|
+
useLiters = null;
|
|
5904
6694
|
useItemPlayer = null;
|
|
5905
6695
|
useItemName = "";
|
|
5906
|
-
|
|
6696
|
+
const next = afterOpening;
|
|
6697
|
+
afterOpening = null;
|
|
6698
|
+
if (next && e?.opened) useItem(next);
|
|
6699
|
+
}
|
|
6700
|
+
};
|
|
6701
|
+
const openingProgress = (def, time) => {
|
|
6702
|
+
const keys = [...def.keys].sort((a, b) => a.t - b.t);
|
|
6703
|
+
const turning = keys.findIndex((k) => k.action === "turn");
|
|
6704
|
+
const lastTurn = keys.map((k) => k.action).lastIndexOf("turn");
|
|
6705
|
+
const start = turning >= 0 ? keys[turning].t : 0;
|
|
6706
|
+
const end = lastTurn >= 0 ? keys[lastTurn + 1]?.t ?? def.duration : start;
|
|
6707
|
+
const openAt = keys.find((k) => k.action === "open")?.t ?? def.duration;
|
|
6708
|
+
const share = end > start ? (time - start) / (end - start) : 1;
|
|
6709
|
+
const lift = time <= end ? 0 : openAt > end ? Math.min(1, (time - end) / (openAt - end)) : 1;
|
|
6710
|
+
return { stroke: twistStroke(share), lift };
|
|
5907
6711
|
};
|
|
5908
6712
|
const TAP_ID = "room_tap";
|
|
5909
6713
|
const TAP_RATE = 0.25;
|
|
@@ -5982,7 +6786,7 @@ function createTestRoom(options) {
|
|
|
5982
6786
|
if (code === "KeyZ") tiltKey = 0;
|
|
5983
6787
|
if (code === "KeyE" && pushByKey) {
|
|
5984
6788
|
pushByKey = false;
|
|
5985
|
-
if (push.end()) grab();
|
|
6789
|
+
if (push.end() && !doors.use()) grab();
|
|
5986
6790
|
}
|
|
5987
6791
|
return;
|
|
5988
6792
|
}
|
|
@@ -6002,7 +6806,7 @@ function createTestRoom(options) {
|
|
|
6002
6806
|
case "KeyE":
|
|
6003
6807
|
if (e.repeat) break;
|
|
6004
6808
|
pushByKey = !heldPiece && push.begin();
|
|
6005
|
-
if (!pushByKey) grab();
|
|
6809
|
+
if (!pushByKey && (heldPiece || !doors.use())) grab();
|
|
6006
6810
|
break;
|
|
6007
6811
|
case "KeyG":
|
|
6008
6812
|
throwHeld();
|
|
@@ -6028,11 +6832,13 @@ function createTestRoom(options) {
|
|
|
6028
6832
|
case "KeyZ":
|
|
6029
6833
|
tiltKey = 1;
|
|
6030
6834
|
break;
|
|
6031
|
-
case "KeyF":
|
|
6032
|
-
|
|
6835
|
+
case "KeyF": {
|
|
6836
|
+
const use = heldEntry?.uses[e.shiftKey && crouchOnC ? 1 : 0];
|
|
6837
|
+
if (use) useItem(use);
|
|
6033
6838
|
break;
|
|
6839
|
+
}
|
|
6034
6840
|
case "KeyC":
|
|
6035
|
-
if (heldEntry?.uses[1] && !
|
|
6841
|
+
if (heldEntry?.uses[1] && !crouchOnC) useItem(heldEntry.uses[1]);
|
|
6036
6842
|
break;
|
|
6037
6843
|
case "KeyV":
|
|
6038
6844
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
@@ -6077,6 +6883,12 @@ function createTestRoom(options) {
|
|
|
6077
6883
|
aim();
|
|
6078
6884
|
return rayWorld(aimO, aimD, 14);
|
|
6079
6885
|
};
|
|
6886
|
+
const leafNote = (distance) => {
|
|
6887
|
+
if (distance > DOOR_REACH) return void 0;
|
|
6888
|
+
const leaf = doors.aimed();
|
|
6889
|
+
if (!leaf) return void 0;
|
|
6890
|
+
return t(leaf.locked ? "hud.door_hint_locked" : leaf.open ? "hud.door_hint_close" : "hud.door_hint_open");
|
|
6891
|
+
};
|
|
6080
6892
|
const inspectInfo = () => {
|
|
6081
6893
|
const r = inspectTarget();
|
|
6082
6894
|
if (!r) return null;
|
|
@@ -6112,7 +6924,7 @@ function createTestRoom(options) {
|
|
|
6112
6924
|
load: ext.load,
|
|
6113
6925
|
stress: ext.stress,
|
|
6114
6926
|
joints,
|
|
6115
|
-
note: e?.item?.note,
|
|
6927
|
+
note: leafNote(r.distance) ?? e?.item?.note,
|
|
6116
6928
|
thumbnail: e?.item?.thumbnail,
|
|
6117
6929
|
thumbnailCredit: e?.item?.thumbnailCredit,
|
|
6118
6930
|
distance: r.distance
|
|
@@ -6176,6 +6988,7 @@ function createTestRoom(options) {
|
|
|
6176
6988
|
}
|
|
6177
6989
|
syncLiters();
|
|
6178
6990
|
renderer.update(dt);
|
|
6991
|
+
for (const e of entries.values()) e.follower?.update?.();
|
|
6179
6992
|
terrainView.sync(camera.position);
|
|
6180
6993
|
showGroundFire(dt);
|
|
6181
6994
|
vfx.update(dt);
|
|
@@ -6203,12 +7016,14 @@ function createTestRoom(options) {
|
|
|
6203
7016
|
const update = (dt) => {
|
|
6204
7017
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
6205
7018
|
frameStamp++;
|
|
7019
|
+
seating.update();
|
|
6206
7020
|
player.update(dt);
|
|
6207
7021
|
if (pushByKey && !player.keys.has("KeyE")) {
|
|
6208
7022
|
pushByKey = false;
|
|
6209
7023
|
push.end();
|
|
6210
7024
|
}
|
|
6211
7025
|
push.step(paused ? 0 : dt);
|
|
7026
|
+
doors.step(paused ? 0 : dt);
|
|
6212
7027
|
if (!paused) kickLight(dt);
|
|
6213
7028
|
if (!paused) pushByPlayer(dt);
|
|
6214
7029
|
else player.takeWish();
|
|
@@ -6224,13 +7039,14 @@ function createTestRoom(options) {
|
|
|
6224
7039
|
}
|
|
6225
7040
|
if (!paused) followHeld(dt);
|
|
6226
7041
|
stepSim(dt * timeScale);
|
|
6227
|
-
|
|
7042
|
+
reachAvatar();
|
|
6228
7043
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
6229
7044
|
hudAcc += dt;
|
|
6230
7045
|
if (hudAcc >= 0.1 && ui) {
|
|
6231
7046
|
hudAcc = 0;
|
|
6232
7047
|
ui.hud(hudState());
|
|
6233
7048
|
}
|
|
7049
|
+
ui?.flushLog();
|
|
6234
7050
|
metricsAcc += dt;
|
|
6235
7051
|
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
6236
7052
|
metricsAcc = 0;
|
|
@@ -6251,7 +7067,7 @@ function createTestRoom(options) {
|
|
|
6251
7067
|
pages: Math.max(1, Math.ceil(loadout.length / 10)),
|
|
6252
7068
|
status: simTime - statusAt < 2 ? statusLine : "",
|
|
6253
7069
|
progress: progress ? { label: progress.label, value: progress.value } : null,
|
|
6254
|
-
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,
|
|
7070
|
+
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,
|
|
6255
7071
|
inspect: inspectInfo(),
|
|
6256
7072
|
animations: animationsOf(),
|
|
6257
7073
|
locked: player.locked,
|
|
@@ -6292,17 +7108,33 @@ function createTestRoom(options) {
|
|
|
6292
7108
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
6293
7109
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
6294
7110
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
7111
|
+
if (id !== "") followPart(pl.entry, id, obj, rest);
|
|
7112
|
+
}
|
|
7113
|
+
};
|
|
7114
|
+
const fpRest = new THREE.Matrix4();
|
|
7115
|
+
const fpNow = new THREE.Matrix4();
|
|
7116
|
+
const fpRoot = new THREE.Matrix4();
|
|
7117
|
+
const fpPart = new THREE.Matrix4();
|
|
7118
|
+
const fpPos = new THREE.Vector3();
|
|
7119
|
+
const fpQuat = new THREE.Quaternion();
|
|
7120
|
+
const fpScale = new THREE.Vector3();
|
|
7121
|
+
const fpEuler = new THREE.Euler();
|
|
7122
|
+
const followPart = (e, id, obj, rest) => {
|
|
7123
|
+
const ps = specs.get(e.so.objectId)?.parts.find((p) => p.id === id);
|
|
7124
|
+
if (!ps || !obj.parent) return;
|
|
7125
|
+
const root = e.so.root;
|
|
7126
|
+
root.updateMatrixWorld(true);
|
|
7127
|
+
fpRoot.copy(root.matrixWorld).invert();
|
|
7128
|
+
fpNow.multiplyMatrices(fpRoot, obj.matrixWorld);
|
|
7129
|
+
fpEuler.set(rest.r[0], rest.r[1], rest.r[2]);
|
|
7130
|
+
fpRest.compose(fpPos.set(rest.p[0], rest.p[1], rest.p[2]), fpQuat.setFromEuler(fpEuler), obj.scale);
|
|
7131
|
+
fpRest.premultiply(obj.parent.matrixWorld).premultiply(fpRoot);
|
|
7132
|
+
fpNow.multiply(fpRest.invert());
|
|
7133
|
+
const r0 = ps.rotation ?? ZERO;
|
|
7134
|
+
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));
|
|
7135
|
+
fpPart.premultiply(fpNow).decompose(fpPos, fpQuat, fpScale);
|
|
7136
|
+
fpEuler.setFromQuaternion(fpQuat);
|
|
7137
|
+
world.setPartPose(e.so.objectId, id, [fpPos.x, fpPos.y, fpPos.z], [fpEuler.x, fpEuler.y, fpEuler.z]);
|
|
6306
7138
|
};
|
|
6307
7139
|
const updateAnimations = (dt) => {
|
|
6308
7140
|
for (let i = playing.length - 1; i >= 0; i--) {
|
|
@@ -6313,8 +7145,8 @@ function createTestRoom(options) {
|
|
|
6313
7145
|
while (pl.fired < evs.length && evs[pl.fired].t <= pl.t) {
|
|
6314
7146
|
const ev = evs[pl.fired++];
|
|
6315
7147
|
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 ?? {} } });
|
|
6316
|
-
const
|
|
6317
|
-
if (typeof
|
|
7148
|
+
const key2 = ev.type === "sound" ? ev.data?.key : void 0;
|
|
7149
|
+
if (typeof key2 === "string") playSound(key2, v3(pl.entry.so.root.getWorldPosition(tmpV)), 1);
|
|
6318
7150
|
}
|
|
6319
7151
|
if (pl.t >= pl.clip.duration) {
|
|
6320
7152
|
playing.splice(i, 1);
|
|
@@ -6336,10 +7168,10 @@ function createTestRoom(options) {
|
|
|
6336
7168
|
const targets = /* @__PURE__ */ new Map();
|
|
6337
7169
|
const ids = /* @__PURE__ */ new Set();
|
|
6338
7170
|
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) ids.add(id);
|
|
6339
|
-
const grab2 = (
|
|
7171
|
+
const grab2 = (key2, obj) => {
|
|
6340
7172
|
if (!obj) return;
|
|
6341
7173
|
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] });
|
|
6342
|
-
targets.set(
|
|
7174
|
+
targets.set(key2, obj);
|
|
6343
7175
|
};
|
|
6344
7176
|
if (clip.keys.some((k) => k.position || k.rotation)) grab2("", e.so.root);
|
|
6345
7177
|
for (const id of ids) grab2(id, e.so.root.getObjectByName(id) ?? void 0);
|
|
@@ -6366,9 +7198,9 @@ function createTestRoom(options) {
|
|
|
6366
7198
|
const METRIC_UNAVAILABLE = "n/a";
|
|
6367
7199
|
const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
|
|
6368
7200
|
const reportedFailures = /* @__PURE__ */ new Set();
|
|
6369
|
-
const reportOnce = (
|
|
6370
|
-
if (reportedFailures.has(
|
|
6371
|
-
reportedFailures.add(
|
|
7201
|
+
const reportOnce = (key2, text) => {
|
|
7202
|
+
if (reportedFailures.has(key2)) return;
|
|
7203
|
+
reportedFailures.add(key2);
|
|
6372
7204
|
host.log(text);
|
|
6373
7205
|
};
|
|
6374
7206
|
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -6518,6 +7350,14 @@ function createTestRoom(options) {
|
|
|
6518
7350
|
frameListeners.add(update2);
|
|
6519
7351
|
return () => frameListeners.delete(update2);
|
|
6520
7352
|
},
|
|
7353
|
+
holdFrame: () => bodyHoldFrame(),
|
|
7354
|
+
reachFor(reach) {
|
|
7355
|
+
if (!avatar) {
|
|
7356
|
+
reach.onGrip?.();
|
|
7357
|
+
return;
|
|
7358
|
+
}
|
|
7359
|
+
doors.reachFor(reach);
|
|
7360
|
+
},
|
|
6521
7361
|
setHandTargets(source) {
|
|
6522
7362
|
handSource = source;
|
|
6523
7363
|
return () => {
|
|
@@ -6535,6 +7375,10 @@ function createTestRoom(options) {
|
|
|
6535
7375
|
clearListeners.add(listener);
|
|
6536
7376
|
return () => clearListeners.delete(listener);
|
|
6537
7377
|
},
|
|
7378
|
+
onReset(listener) {
|
|
7379
|
+
resetListeners.add(listener);
|
|
7380
|
+
return () => resetListeners.delete(listener);
|
|
7381
|
+
},
|
|
6538
7382
|
addObject(itemId, place) {
|
|
6539
7383
|
return addScenery(itemId, place.position, place.rotationY);
|
|
6540
7384
|
}
|
|
@@ -6562,13 +7406,32 @@ function createTestRoom(options) {
|
|
|
6562
7406
|
return r.ok ? store : null;
|
|
6563
7407
|
};
|
|
6564
7408
|
const LIGHTS_CHECK_MS = 250;
|
|
7409
|
+
const pinnedPrograms = /* @__PURE__ */ new WeakSet();
|
|
7410
|
+
const pinPrograms = () => {
|
|
7411
|
+
for (const program of gl.info?.programs ?? []) {
|
|
7412
|
+
if (pinnedPrograms.has(program)) continue;
|
|
7413
|
+
pinnedPrograms.add(program);
|
|
7414
|
+
program.usedTimes++;
|
|
7415
|
+
}
|
|
7416
|
+
};
|
|
6565
7417
|
const warmUp = () => {
|
|
6566
7418
|
const probes = [renderer.warmUpProbes(), vfx.warmUpProbes(), fluidView.warmUpProbes()];
|
|
6567
7419
|
const groups = probes.map((probe) => probe.group);
|
|
7420
|
+
const spriteMap = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
|
|
7421
|
+
spriteMap.needsUpdate = true;
|
|
7422
|
+
const spriteProbes = [true, false].map((fog) => {
|
|
7423
|
+
const probe = new THREE.Sprite(new THREE.SpriteMaterial({ map: spriteMap, transparent: true, fog }));
|
|
7424
|
+
probe.frustumCulled = false;
|
|
7425
|
+
return probe;
|
|
7426
|
+
});
|
|
7427
|
+
groups.push(...spriteProbes);
|
|
6568
7428
|
scene.add(...groups);
|
|
6569
7429
|
gl.render(scene, camera);
|
|
7430
|
+
pinPrograms();
|
|
6570
7431
|
scene.remove(...groups);
|
|
6571
7432
|
for (const probe of probes) probe.dispose();
|
|
7433
|
+
for (const probe of spriteProbes) probe.material.dispose();
|
|
7434
|
+
spriteMap.dispose();
|
|
6572
7435
|
};
|
|
6573
7436
|
const warmUpEffects = () => {
|
|
6574
7437
|
const ids = vfx.catalog?.() ?? [];
|
|
@@ -6599,11 +7462,14 @@ function createTestRoom(options) {
|
|
|
6599
7462
|
build: (T) => ti.build(T),
|
|
6600
7463
|
spec: ti.spec,
|
|
6601
7464
|
movable: ti.movable,
|
|
7465
|
+
pushable: ti.pushable,
|
|
6602
7466
|
slow: ti.slow,
|
|
6603
7467
|
fluid: ti.fluid,
|
|
7468
|
+
seal: ti.seal,
|
|
6604
7469
|
weapon: ti.weapon,
|
|
6605
7470
|
impl: ti.impl,
|
|
6606
7471
|
animations: ti.animations,
|
|
7472
|
+
seats: ti.seats,
|
|
6607
7473
|
thumbnail: ti.thumbnail,
|
|
6608
7474
|
thumbnailCredit: ti.thumbnailCredit
|
|
6609
7475
|
};
|
|
@@ -6660,15 +7526,20 @@ function createTestRoom(options) {
|
|
|
6660
7526
|
const kept = previous?.encode();
|
|
6661
7527
|
previous?.dispose();
|
|
6662
7528
|
benches.delete(mod.id);
|
|
6663
|
-
for (const
|
|
7529
|
+
for (const key2 of [...reportedFailures]) if (key2.startsWith(`${mod.id}:`)) reportedFailures.delete(key2);
|
|
6664
7530
|
modules.set(mod.id, mod);
|
|
6665
7531
|
if (bench) {
|
|
6666
7532
|
const seed = options.bench?.state?.[mod.id];
|
|
6667
7533
|
const restored = kept ?? (typeof seed === "string" ? seed : void 0);
|
|
6668
7534
|
if (restored !== void 0) bench.restore(restored);
|
|
6669
7535
|
benches.set(mod.id, bench);
|
|
7536
|
+
let reportedState = bench.encode();
|
|
6670
7537
|
bench.watch(() => {
|
|
6671
|
-
|
|
7538
|
+
const encoded = bench.encode();
|
|
7539
|
+
if (encoded !== reportedState) {
|
|
7540
|
+
reportedState = encoded;
|
|
7541
|
+
options.bench?.onChange?.(mod.id, encoded);
|
|
7542
|
+
}
|
|
6672
7543
|
if (activeBench === mod.id) ui?.bench(benchView());
|
|
6673
7544
|
});
|
|
6674
7545
|
if (!activeBench || !benches.has(activeBench)) activeBench = mod.id;
|
|
@@ -6690,8 +7561,10 @@ function createTestRoom(options) {
|
|
|
6690
7561
|
const dt = seconds > 6 ? 1 / 30 : FIXED_DT;
|
|
6691
7562
|
const n = Math.max(1, Math.round(seconds / dt));
|
|
6692
7563
|
for (let i = 0; i < n; i++) {
|
|
7564
|
+
seating.update();
|
|
6693
7565
|
player.update(0);
|
|
6694
7566
|
push.step(dt);
|
|
7567
|
+
doors.step(dt);
|
|
6695
7568
|
followHeld(dt);
|
|
6696
7569
|
stepSim(dt);
|
|
6697
7570
|
}
|
|
@@ -6826,7 +7699,7 @@ function createTestRoom(options) {
|
|
|
6826
7699
|
room.waitUntilReady();
|
|
6827
7700
|
return true;
|
|
6828
7701
|
},
|
|
6829
|
-
look: (eye, target) =>
|
|
7702
|
+
look: (eye, target) => lookAt(eye, target),
|
|
6830
7703
|
use: (hold) => room.use(hold),
|
|
6831
7704
|
blastAt: (weapon, point) => {
|
|
6832
7705
|
const def = arsenal.weapons.get(weapon);
|
|
@@ -6882,7 +7755,7 @@ function createTestRoom(options) {
|
|
|
6882
7755
|
grab,
|
|
6883
7756
|
throwHeld,
|
|
6884
7757
|
look(position, target) {
|
|
6885
|
-
|
|
7758
|
+
lookAt(position, target);
|
|
6886
7759
|
},
|
|
6887
7760
|
inspect() {
|
|
6888
7761
|
const r = inspectTarget();
|
|
@@ -6949,11 +7822,23 @@ function createTestRoom(options) {
|
|
|
6949
7822
|
busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
|
|
6950
7823
|
loadModule,
|
|
6951
7824
|
warmUp: () => {
|
|
7825
|
+
audio?.warmUp();
|
|
6952
7826
|
warmUp();
|
|
6953
7827
|
warmUpEffects();
|
|
6954
7828
|
},
|
|
6955
7829
|
animations: animationsOf,
|
|
6956
7830
|
playAnimation,
|
|
7831
|
+
useLeaf: (objectId, part) => doors.useLeaf(objectId, part),
|
|
7832
|
+
leafOpening: (objectId, part) => doors.opening(objectId, part ?? leavesOf(objectId)[0]?.part ?? ""),
|
|
7833
|
+
leaves: (objectId) => leavesOf(objectId).map((l) => l.part),
|
|
7834
|
+
lockLeaf(objectId, locked = true, part) {
|
|
7835
|
+
const k = `${objectId}\0${part ?? "*"}`;
|
|
7836
|
+
if (locked) lockedLeaves.add(k);
|
|
7837
|
+
else lockedLeaves.delete(k);
|
|
7838
|
+
},
|
|
7839
|
+
get door() {
|
|
7840
|
+
return doors.state;
|
|
7841
|
+
},
|
|
6957
7842
|
bench(moduleId) {
|
|
6958
7843
|
const id = moduleId ?? activeBench;
|
|
6959
7844
|
return (id ? benches.get(id) : void 0) ?? null;
|
|
@@ -6996,6 +7881,11 @@ function createTestRoom(options) {
|
|
|
6996
7881
|
get playerPosition() {
|
|
6997
7882
|
return player.position;
|
|
6998
7883
|
},
|
|
7884
|
+
sit: (objectId, seatId) => seating.sit(objectId, seatId),
|
|
7885
|
+
stand: () => seating.stand(),
|
|
7886
|
+
get seat() {
|
|
7887
|
+
return seating.seat;
|
|
7888
|
+
},
|
|
6999
7889
|
push: { begin: () => push.begin(), end: () => push.end(), setWalk: (walk) => push.setWalk(walk), get state() {
|
|
7000
7890
|
return push.state;
|
|
7001
7891
|
} },
|
|
@@ -7098,8 +7988,8 @@ function createTestRoom(options) {
|
|
|
7098
7988
|
},
|
|
7099
7989
|
onTap: () => toggleTap(),
|
|
7100
7990
|
...combo ? { combo: { label: combo.name?.[locale] ?? combo.id, onToggle: () => setStrikeCombo(!comboOn) } } : {},
|
|
7101
|
-
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((
|
|
7102
|
-
onBlast: (
|
|
7991
|
+
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((key2) => key2.replace(/^Key/, "")).join("/")})`, onKick: () => void kick() } } : {},
|
|
7992
|
+
onBlast: (key2) => toggleBlast(key2),
|
|
7103
7993
|
onPlay: (objectId, clipId) => {
|
|
7104
7994
|
playAnimation(clipId, objectId);
|
|
7105
7995
|
},
|
|
@@ -7125,19 +8015,19 @@ function createTestRoom(options) {
|
|
|
7125
8015
|
let nextLightsCheck = 0;
|
|
7126
8016
|
let warmedLights = "";
|
|
7127
8017
|
const lightsKey = () => {
|
|
7128
|
-
let
|
|
8018
|
+
let key2 = "";
|
|
7129
8019
|
scene.traverseVisible((object) => {
|
|
7130
|
-
if (object.isLight)
|
|
8020
|
+
if (object.isLight) key2 += `${object.type[0]}${object.castShadow ? "s" : ""}`;
|
|
7131
8021
|
});
|
|
7132
|
-
return
|
|
8022
|
+
return key2;
|
|
7133
8023
|
};
|
|
7134
8024
|
const loop = (now) => {
|
|
7135
8025
|
raf = requestAnimationFrame(loop);
|
|
7136
8026
|
if (now >= nextLightsCheck) {
|
|
7137
8027
|
nextLightsCheck = now + LIGHTS_CHECK_MS;
|
|
7138
|
-
const
|
|
7139
|
-
if (
|
|
7140
|
-
warmedLights =
|
|
8028
|
+
const key2 = lightsKey();
|
|
8029
|
+
if (key2 !== warmedLights) {
|
|
8030
|
+
warmedLights = key2;
|
|
7141
8031
|
warmUp();
|
|
7142
8032
|
}
|
|
7143
8033
|
}
|
|
@@ -7148,6 +8038,7 @@ function createTestRoom(options) {
|
|
|
7148
8038
|
if (!scenarioRunning) update(dt);
|
|
7149
8039
|
const restoreCamera = room.applyCamera();
|
|
7150
8040
|
gl.render(scene, debugView ?? camera);
|
|
8041
|
+
pinPrograms();
|
|
7151
8042
|
restoreCamera();
|
|
7152
8043
|
roomMetrics.rendered();
|
|
7153
8044
|
};
|