@adatechnology/game-test-room 0.18.0 → 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 +125 -15
- package/dist/index.global.js +2585 -615
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1088 -202
- 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,38 +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
|
-
triggerFinger: targets?.fingers?.index ?? null
|
|
5057
|
-
};
|
|
5058
|
-
lastHand = { stance: state.stance, triggerFinger: state.triggerFinger, targets };
|
|
5059
|
-
arms.update(dt, state);
|
|
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] };
|
|
5060
5775
|
};
|
|
5061
5776
|
const reachAvatar = () => {
|
|
5062
|
-
|
|
5063
|
-
|
|
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
|
|
5783
|
+
};
|
|
5784
|
+
if (!avatar?.reach) return;
|
|
5785
|
+
avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
|
|
5064
5786
|
};
|
|
5065
5787
|
const applyThirdView = () => {
|
|
5066
5788
|
if (!thirdActive()) return () => void 0;
|
|
@@ -5566,9 +6288,9 @@ function createTestRoom(options) {
|
|
|
5566
6288
|
}
|
|
5567
6289
|
let blastOptions = { ...options.blast };
|
|
5568
6290
|
let lastBlast;
|
|
5569
|
-
const blastOn = (
|
|
5570
|
-
const toggleBlast = (
|
|
5571
|
-
blastOptions = { ...blastOptions, [
|
|
6291
|
+
const blastOn = (key2) => Boolean(blastOptions[key2]);
|
|
6292
|
+
const toggleBlast = (key2) => {
|
|
6293
|
+
blastOptions = { ...blastOptions, [key2]: blastOn(key2) ? false : true };
|
|
5572
6294
|
};
|
|
5573
6295
|
const explosives = createExplosives({
|
|
5574
6296
|
THREE,
|
|
@@ -5699,10 +6421,12 @@ function createTestRoom(options) {
|
|
|
5699
6421
|
let tiltKey = 0;
|
|
5700
6422
|
const heldLocal = new THREE.Vector3();
|
|
5701
6423
|
const heldSize = new THREE.Vector3(0.2, 0.2, 0.2);
|
|
5702
|
-
let heldRadius = 0.2;
|
|
5703
6424
|
const heldVel = [0, 0, 0];
|
|
5704
6425
|
let useItemPlayer = null;
|
|
5705
6426
|
let useItemName = "";
|
|
6427
|
+
let afterOpening = null;
|
|
6428
|
+
let twisting = null;
|
|
6429
|
+
let twistHand = null;
|
|
5706
6430
|
const releaseHeld = () => {
|
|
5707
6431
|
if (heldPiece) {
|
|
5708
6432
|
heldPiece.held = false;
|
|
@@ -5711,6 +6435,9 @@ function createTestRoom(options) {
|
|
|
5711
6435
|
}
|
|
5712
6436
|
useItemPlayer?.cancel();
|
|
5713
6437
|
useItemPlayer = null;
|
|
6438
|
+
afterOpening = null;
|
|
6439
|
+
twisting = null;
|
|
6440
|
+
twistHand = null;
|
|
5714
6441
|
heldEntry = null;
|
|
5715
6442
|
heldPiece = null;
|
|
5716
6443
|
heldTilt = 0;
|
|
@@ -5757,7 +6484,6 @@ function createTestRoom(options) {
|
|
|
5757
6484
|
box2.getSize(heldSize);
|
|
5758
6485
|
piece.root.quaternion.copy(savedQ);
|
|
5759
6486
|
piece.root.updateMatrixWorld(true);
|
|
5760
|
-
heldRadius = heldSize.length() / 3;
|
|
5761
6487
|
heldYaw = 0;
|
|
5762
6488
|
heldTilt = 0;
|
|
5763
6489
|
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
@@ -5778,52 +6504,28 @@ function createTestRoom(options) {
|
|
|
5778
6504
|
fire("throw", { objectId: id, speed });
|
|
5779
6505
|
return true;
|
|
5780
6506
|
};
|
|
5781
|
-
const
|
|
5782
|
-
|
|
5783
|
-
const
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
|
|
5792
|
-
const palm = center2.clone().addScaledVector(handY, radius + CARRY_PALM).addScaledVector(handX, -0.022).addScaledVector(up, heldSize.y > 0.16 ? -heldSize.y * 0.08 : -5e-3);
|
|
5793
|
-
if (useItemName === "open" || useItemName === "close") {
|
|
5794
|
-
const leftX = new THREE.Vector3(0.35, 0, -0.94).normalize().applyQuaternion(gripQ);
|
|
5795
|
-
const leftY = new THREE.Vector3(-0.94, 0, -0.35).normalize().applyQuaternion(gripQ);
|
|
5796
|
-
const leftZ = new THREE.Vector3().crossVectors(leftX, leftY).normalize();
|
|
5797
|
-
const lq = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(leftX, leftY, leftZ));
|
|
5798
|
-
const leftPalm = center2.clone().addScaledVector(up, heldSize.y * 0.45).addScaledVector(leftY, radius * 0.65 + CARRY_PALM).addScaledVector(leftX, -0.018);
|
|
5799
|
-
return {
|
|
5800
|
-
R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] },
|
|
5801
|
-
L: { position: [leftPalm.x, leftPalm.y, leftPalm.z], quaternion: [lq.x, lq.y, lq.z, lq.w] },
|
|
5802
|
-
grip: 0.85,
|
|
5803
|
-
gripL: 0.8
|
|
5804
|
-
};
|
|
5805
|
-
}
|
|
5806
|
-
return { R: { position: [palm.x, palm.y, palm.z], quaternion: [q2.x, q2.y, q2.z, q2.w] }, grip: 0.85 };
|
|
5807
|
-
}
|
|
5808
|
-
const box2 = new THREE.Box3().setFromObject(root);
|
|
5809
|
-
const center = box2.getCenter(new THREE.Vector3());
|
|
5810
|
-
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(gripQ).normalize();
|
|
5811
|
-
const back = new THREE.Vector3(0, -1, 0).applyQuaternion(gripQ).normalize();
|
|
5812
|
-
const z = new THREE.Vector3().crossVectors(forward, back).normalize();
|
|
5813
|
-
const y = new THREE.Vector3().crossVectors(z, forward).normalize();
|
|
5814
|
-
const q = new THREE.Quaternion().setFromRotationMatrix(new THREE.Matrix4().makeBasis(forward, y, z));
|
|
5815
|
-
return { R: { position: [center.x, box2.min.y - CARRY_PALM, center.z], quaternion: [q.x, q.y, q.z, q.w] }, grip: CARRY_GRIP };
|
|
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 };
|
|
5816
6518
|
};
|
|
5817
6519
|
const gripQuat = new THREE.Quaternion();
|
|
5818
6520
|
const followHeld = (dt) => {
|
|
5819
6521
|
const piece = heldPiece;
|
|
5820
|
-
|
|
6522
|
+
twistHand = null;
|
|
5821
6523
|
if (!piece) return;
|
|
5822
6524
|
camera.updateMatrixWorld(true);
|
|
5823
6525
|
const root = piece.root;
|
|
5824
6526
|
let pos;
|
|
5825
6527
|
const targetQ = tmpQ;
|
|
5826
|
-
const hand =
|
|
6528
|
+
const hand = avatar?.hand?.("R");
|
|
5827
6529
|
const isDrink = useItemName === "drink" || useItemName === "sip" || useItemName === "chug";
|
|
5828
6530
|
if (hand) {
|
|
5829
6531
|
const handWorld = hand.getWorldPosition(tmpHand);
|
|
@@ -5839,7 +6541,7 @@ function createTestRoom(options) {
|
|
|
5839
6541
|
pos = tmpV.copy(handWorld).add(gripOffset);
|
|
5840
6542
|
} else if (useItemPlayer) {
|
|
5841
6543
|
const pose = useItemPlayer.pose();
|
|
5842
|
-
const frameQ =
|
|
6544
|
+
const frameQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5843
6545
|
const raise = isDrink ? Math.max(0, Math.min(1, (pose.position[1] + 0.2) / 0.22)) : 0;
|
|
5844
6546
|
const px2 = pose.position[0] + (isDrink ? 0.02 * raise : 0);
|
|
5845
6547
|
const py2 = pose.position[1] - (isDrink ? 0.08 * raise : 0);
|
|
@@ -5852,19 +6554,10 @@ function createTestRoom(options) {
|
|
|
5852
6554
|
targetQ.copy(frameQ).multiply(eq);
|
|
5853
6555
|
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5854
6556
|
} else {
|
|
5855
|
-
|
|
5856
|
-
|
|
5857
|
-
const holdY = avatar?.firstPerson ? small ? -0.2 : -0.26 : -0.28;
|
|
5858
|
-
const holdZ = avatar?.firstPerson ? small ? -0.42 : -Math.min(1.15, CARRY_NEAR + heldRadius) : -1.15;
|
|
5859
|
-
pos = tmpV.set(holdX, holdY, holdZ).applyMatrix4(camera.matrixWorld);
|
|
5860
|
-
const baseQ = avatar?.firstPerson && small ? camera.quaternion : new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw);
|
|
5861
|
-
gripQuat.copy(baseQ).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.max(-1.35, -heldTilt)));
|
|
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)));
|
|
5862
6559
|
targetQ.setFromAxisAngle(new THREE.Vector3(0, 1, 0), player.yaw + heldYaw);
|
|
5863
|
-
|
|
5864
|
-
targetQ.copy(camera.quaternion).multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), heldYaw));
|
|
5865
|
-
} else {
|
|
5866
|
-
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5867
|
-
}
|
|
6560
|
+
targetQ.multiply(new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), -heldTilt));
|
|
5868
6561
|
}
|
|
5869
6562
|
const k = Math.min(1, (useItemPlayer ? 30 : 16) * dt);
|
|
5870
6563
|
const px = root.position.x;
|
|
@@ -5879,8 +6572,50 @@ function createTestRoom(options) {
|
|
|
5879
6572
|
heldVel[2] = (root.position.z - pz) / dt;
|
|
5880
6573
|
}
|
|
5881
6574
|
root.updateMatrixWorld(true);
|
|
5882
|
-
|
|
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;
|
|
5883
6617
|
};
|
|
6618
|
+
const crouchOnC = (options.crouch?.keys ?? ["KeyC"]).includes("KeyC");
|
|
5884
6619
|
const useItem = (useId) => {
|
|
5885
6620
|
const e = heldEntry;
|
|
5886
6621
|
if (!e?.containerId) return false;
|
|
@@ -5889,25 +6624,90 @@ function createTestRoom(options) {
|
|
|
5889
6624
|
const c = fluids.container(e.containerId);
|
|
5890
6625
|
if (!c) return false;
|
|
5891
6626
|
if (def.liquids && def.liquids.length > 0 && (!c.liquid || !def.liquids.includes(c.liquid))) return false;
|
|
5892
|
-
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
|
+
}
|
|
5893
6655
|
if (consuming && c.liters <= 1e-6) {
|
|
5894
6656
|
say(t("hud.empty_container", { name: e.name }));
|
|
5895
6657
|
return false;
|
|
5896
6658
|
}
|
|
5897
|
-
if (useItemPlayer && !useItemPlayer.done) return false;
|
|
5898
6659
|
if (consuming && c.closed) fluids.open(e.containerId);
|
|
5899
6660
|
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
5900
6661
|
useItemName = def.id;
|
|
6662
|
+
if (eating) useLiters = c.liters;
|
|
5901
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 } });
|
|
5902
6664
|
return true;
|
|
5903
6665
|
};
|
|
6666
|
+
let useLiters = null;
|
|
5904
6667
|
const updateItemUse = (dt) => {
|
|
5905
6668
|
if (!useItemPlayer) return;
|
|
6669
|
+
const e = heldEntry;
|
|
6670
|
+
const c = e?.containerId ? fluids.container(e.containerId) : void 0;
|
|
6671
|
+
const before = c?.liters;
|
|
5906
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
|
+
}
|
|
5907
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;
|
|
5908
6694
|
useItemPlayer = null;
|
|
5909
6695
|
useItemName = "";
|
|
5910
|
-
|
|
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 };
|
|
5911
6711
|
};
|
|
5912
6712
|
const TAP_ID = "room_tap";
|
|
5913
6713
|
const TAP_RATE = 0.25;
|
|
@@ -5986,7 +6786,7 @@ function createTestRoom(options) {
|
|
|
5986
6786
|
if (code === "KeyZ") tiltKey = 0;
|
|
5987
6787
|
if (code === "KeyE" && pushByKey) {
|
|
5988
6788
|
pushByKey = false;
|
|
5989
|
-
if (push.end()) grab();
|
|
6789
|
+
if (push.end() && !doors.use()) grab();
|
|
5990
6790
|
}
|
|
5991
6791
|
return;
|
|
5992
6792
|
}
|
|
@@ -6006,7 +6806,7 @@ function createTestRoom(options) {
|
|
|
6006
6806
|
case "KeyE":
|
|
6007
6807
|
if (e.repeat) break;
|
|
6008
6808
|
pushByKey = !heldPiece && push.begin();
|
|
6009
|
-
if (!pushByKey) grab();
|
|
6809
|
+
if (!pushByKey && (heldPiece || !doors.use())) grab();
|
|
6010
6810
|
break;
|
|
6011
6811
|
case "KeyG":
|
|
6012
6812
|
throwHeld();
|
|
@@ -6032,11 +6832,13 @@ function createTestRoom(options) {
|
|
|
6032
6832
|
case "KeyZ":
|
|
6033
6833
|
tiltKey = 1;
|
|
6034
6834
|
break;
|
|
6035
|
-
case "KeyF":
|
|
6036
|
-
|
|
6835
|
+
case "KeyF": {
|
|
6836
|
+
const use = heldEntry?.uses[e.shiftKey && crouchOnC ? 1 : 0];
|
|
6837
|
+
if (use) useItem(use);
|
|
6037
6838
|
break;
|
|
6839
|
+
}
|
|
6038
6840
|
case "KeyC":
|
|
6039
|
-
if (heldEntry?.uses[1] && !
|
|
6841
|
+
if (heldEntry?.uses[1] && !crouchOnC) useItem(heldEntry.uses[1]);
|
|
6040
6842
|
break;
|
|
6041
6843
|
case "KeyV":
|
|
6042
6844
|
if (heldEntry?.uses[2]) useItem(heldEntry.uses[2]);
|
|
@@ -6081,6 +6883,12 @@ function createTestRoom(options) {
|
|
|
6081
6883
|
aim();
|
|
6082
6884
|
return rayWorld(aimO, aimD, 14);
|
|
6083
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
|
+
};
|
|
6084
6892
|
const inspectInfo = () => {
|
|
6085
6893
|
const r = inspectTarget();
|
|
6086
6894
|
if (!r) return null;
|
|
@@ -6116,7 +6924,7 @@ function createTestRoom(options) {
|
|
|
6116
6924
|
load: ext.load,
|
|
6117
6925
|
stress: ext.stress,
|
|
6118
6926
|
joints,
|
|
6119
|
-
note: e?.item?.note,
|
|
6927
|
+
note: leafNote(r.distance) ?? e?.item?.note,
|
|
6120
6928
|
thumbnail: e?.item?.thumbnail,
|
|
6121
6929
|
thumbnailCredit: e?.item?.thumbnailCredit,
|
|
6122
6930
|
distance: r.distance
|
|
@@ -6180,6 +6988,7 @@ function createTestRoom(options) {
|
|
|
6180
6988
|
}
|
|
6181
6989
|
syncLiters();
|
|
6182
6990
|
renderer.update(dt);
|
|
6991
|
+
for (const e of entries.values()) e.follower?.update?.();
|
|
6183
6992
|
terrainView.sync(camera.position);
|
|
6184
6993
|
showGroundFire(dt);
|
|
6185
6994
|
vfx.update(dt);
|
|
@@ -6207,12 +7016,14 @@ function createTestRoom(options) {
|
|
|
6207
7016
|
const update = (dt) => {
|
|
6208
7017
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
6209
7018
|
frameStamp++;
|
|
7019
|
+
seating.update();
|
|
6210
7020
|
player.update(dt);
|
|
6211
7021
|
if (pushByKey && !player.keys.has("KeyE")) {
|
|
6212
7022
|
pushByKey = false;
|
|
6213
7023
|
push.end();
|
|
6214
7024
|
}
|
|
6215
7025
|
push.step(paused ? 0 : dt);
|
|
7026
|
+
doors.step(paused ? 0 : dt);
|
|
6216
7027
|
if (!paused) kickLight(dt);
|
|
6217
7028
|
if (!paused) pushByPlayer(dt);
|
|
6218
7029
|
else player.takeWish();
|
|
@@ -6228,7 +7039,6 @@ function createTestRoom(options) {
|
|
|
6228
7039
|
}
|
|
6229
7040
|
if (!paused) followHeld(dt);
|
|
6230
7041
|
stepSim(dt * timeScale);
|
|
6231
|
-
stepFirstPerson(paused ? 0 : dt);
|
|
6232
7042
|
reachAvatar();
|
|
6233
7043
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
6234
7044
|
hudAcc += dt;
|
|
@@ -6236,6 +7046,7 @@ function createTestRoom(options) {
|
|
|
6236
7046
|
hudAcc = 0;
|
|
6237
7047
|
ui.hud(hudState());
|
|
6238
7048
|
}
|
|
7049
|
+
ui?.flushLog();
|
|
6239
7050
|
metricsAcc += dt;
|
|
6240
7051
|
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
6241
7052
|
metricsAcc = 0;
|
|
@@ -6256,7 +7067,7 @@ function createTestRoom(options) {
|
|
|
6256
7067
|
pages: Math.max(1, Math.ceil(loadout.length / 10)),
|
|
6257
7068
|
status: simTime - statusAt < 2 ? statusLine : "",
|
|
6258
7069
|
progress: progress ? { label: progress.label, value: progress.value } : null,
|
|
6259
|
-
held: heldEntry || heldPiece ? { name: heldName, uses: (heldEntry?.uses ?? []).map((u, i) => ({ key: ["F", "C", "V"][i] ?? "", label: S.text[`use.${u}`] ?? u })), weapon: heldEntry?.item?.weapon ? arsenal.name(heldEntry.item.weapon) : void 0, using: useItemName } : null,
|
|
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,
|
|
6260
7071
|
inspect: inspectInfo(),
|
|
6261
7072
|
animations: animationsOf(),
|
|
6262
7073
|
locked: player.locked,
|
|
@@ -6297,17 +7108,33 @@ function createTestRoom(options) {
|
|
|
6297
7108
|
const pose = poseAt(pl.clip, id === "" ? null : id, pl.t);
|
|
6298
7109
|
obj.position.set(rest.p[0] + pose.position[0], rest.p[1] + pose.position[1], rest.p[2] + pose.position[2]);
|
|
6299
7110
|
obj.rotation.set(rest.r[0] + pose.rotation[0], rest.r[1] + pose.rotation[1], rest.r[2] + pose.rotation[2]);
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
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]);
|
|
6311
7138
|
};
|
|
6312
7139
|
const updateAnimations = (dt) => {
|
|
6313
7140
|
for (let i = playing.length - 1; i >= 0; i--) {
|
|
@@ -6318,8 +7145,8 @@ function createTestRoom(options) {
|
|
|
6318
7145
|
while (pl.fired < evs.length && evs[pl.fired].t <= pl.t) {
|
|
6319
7146
|
const ev = evs[pl.fired++];
|
|
6320
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 ?? {} } });
|
|
6321
|
-
const
|
|
6322
|
-
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);
|
|
6323
7150
|
}
|
|
6324
7151
|
if (pl.t >= pl.clip.duration) {
|
|
6325
7152
|
playing.splice(i, 1);
|
|
@@ -6341,10 +7168,10 @@ function createTestRoom(options) {
|
|
|
6341
7168
|
const targets = /* @__PURE__ */ new Map();
|
|
6342
7169
|
const ids = /* @__PURE__ */ new Set();
|
|
6343
7170
|
for (const k of clip.keys) for (const id of Object.keys(k.parts ?? {})) ids.add(id);
|
|
6344
|
-
const grab2 = (
|
|
7171
|
+
const grab2 = (key2, obj) => {
|
|
6345
7172
|
if (!obj) return;
|
|
6346
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] });
|
|
6347
|
-
targets.set(
|
|
7174
|
+
targets.set(key2, obj);
|
|
6348
7175
|
};
|
|
6349
7176
|
if (clip.keys.some((k) => k.position || k.rotation)) grab2("", e.so.root);
|
|
6350
7177
|
for (const id of ids) grab2(id, e.so.root.getObjectByName(id) ?? void 0);
|
|
@@ -6371,9 +7198,9 @@ function createTestRoom(options) {
|
|
|
6371
7198
|
const METRIC_UNAVAILABLE = "n/a";
|
|
6372
7199
|
const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
|
|
6373
7200
|
const reportedFailures = /* @__PURE__ */ new Set();
|
|
6374
|
-
const reportOnce = (
|
|
6375
|
-
if (reportedFailures.has(
|
|
6376
|
-
reportedFailures.add(
|
|
7201
|
+
const reportOnce = (key2, text) => {
|
|
7202
|
+
if (reportedFailures.has(key2)) return;
|
|
7203
|
+
reportedFailures.add(key2);
|
|
6377
7204
|
host.log(text);
|
|
6378
7205
|
};
|
|
6379
7206
|
const failureText = (error) => error instanceof Error ? error.message : String(error);
|
|
@@ -6523,6 +7350,14 @@ function createTestRoom(options) {
|
|
|
6523
7350
|
frameListeners.add(update2);
|
|
6524
7351
|
return () => frameListeners.delete(update2);
|
|
6525
7352
|
},
|
|
7353
|
+
holdFrame: () => bodyHoldFrame(),
|
|
7354
|
+
reachFor(reach) {
|
|
7355
|
+
if (!avatar) {
|
|
7356
|
+
reach.onGrip?.();
|
|
7357
|
+
return;
|
|
7358
|
+
}
|
|
7359
|
+
doors.reachFor(reach);
|
|
7360
|
+
},
|
|
6526
7361
|
setHandTargets(source) {
|
|
6527
7362
|
handSource = source;
|
|
6528
7363
|
return () => {
|
|
@@ -6540,6 +7375,10 @@ function createTestRoom(options) {
|
|
|
6540
7375
|
clearListeners.add(listener);
|
|
6541
7376
|
return () => clearListeners.delete(listener);
|
|
6542
7377
|
},
|
|
7378
|
+
onReset(listener) {
|
|
7379
|
+
resetListeners.add(listener);
|
|
7380
|
+
return () => resetListeners.delete(listener);
|
|
7381
|
+
},
|
|
6543
7382
|
addObject(itemId, place) {
|
|
6544
7383
|
return addScenery(itemId, place.position, place.rotationY);
|
|
6545
7384
|
}
|
|
@@ -6567,13 +7406,32 @@ function createTestRoom(options) {
|
|
|
6567
7406
|
return r.ok ? store : null;
|
|
6568
7407
|
};
|
|
6569
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
|
+
};
|
|
6570
7417
|
const warmUp = () => {
|
|
6571
7418
|
const probes = [renderer.warmUpProbes(), vfx.warmUpProbes(), fluidView.warmUpProbes()];
|
|
6572
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);
|
|
6573
7428
|
scene.add(...groups);
|
|
6574
7429
|
gl.render(scene, camera);
|
|
7430
|
+
pinPrograms();
|
|
6575
7431
|
scene.remove(...groups);
|
|
6576
7432
|
for (const probe of probes) probe.dispose();
|
|
7433
|
+
for (const probe of spriteProbes) probe.material.dispose();
|
|
7434
|
+
spriteMap.dispose();
|
|
6577
7435
|
};
|
|
6578
7436
|
const warmUpEffects = () => {
|
|
6579
7437
|
const ids = vfx.catalog?.() ?? [];
|
|
@@ -6604,11 +7462,14 @@ function createTestRoom(options) {
|
|
|
6604
7462
|
build: (T) => ti.build(T),
|
|
6605
7463
|
spec: ti.spec,
|
|
6606
7464
|
movable: ti.movable,
|
|
7465
|
+
pushable: ti.pushable,
|
|
6607
7466
|
slow: ti.slow,
|
|
6608
7467
|
fluid: ti.fluid,
|
|
7468
|
+
seal: ti.seal,
|
|
6609
7469
|
weapon: ti.weapon,
|
|
6610
7470
|
impl: ti.impl,
|
|
6611
7471
|
animations: ti.animations,
|
|
7472
|
+
seats: ti.seats,
|
|
6612
7473
|
thumbnail: ti.thumbnail,
|
|
6613
7474
|
thumbnailCredit: ti.thumbnailCredit
|
|
6614
7475
|
};
|
|
@@ -6665,15 +7526,20 @@ function createTestRoom(options) {
|
|
|
6665
7526
|
const kept = previous?.encode();
|
|
6666
7527
|
previous?.dispose();
|
|
6667
7528
|
benches.delete(mod.id);
|
|
6668
|
-
for (const
|
|
7529
|
+
for (const key2 of [...reportedFailures]) if (key2.startsWith(`${mod.id}:`)) reportedFailures.delete(key2);
|
|
6669
7530
|
modules.set(mod.id, mod);
|
|
6670
7531
|
if (bench) {
|
|
6671
7532
|
const seed = options.bench?.state?.[mod.id];
|
|
6672
7533
|
const restored = kept ?? (typeof seed === "string" ? seed : void 0);
|
|
6673
7534
|
if (restored !== void 0) bench.restore(restored);
|
|
6674
7535
|
benches.set(mod.id, bench);
|
|
7536
|
+
let reportedState = bench.encode();
|
|
6675
7537
|
bench.watch(() => {
|
|
6676
|
-
|
|
7538
|
+
const encoded = bench.encode();
|
|
7539
|
+
if (encoded !== reportedState) {
|
|
7540
|
+
reportedState = encoded;
|
|
7541
|
+
options.bench?.onChange?.(mod.id, encoded);
|
|
7542
|
+
}
|
|
6677
7543
|
if (activeBench === mod.id) ui?.bench(benchView());
|
|
6678
7544
|
});
|
|
6679
7545
|
if (!activeBench || !benches.has(activeBench)) activeBench = mod.id;
|
|
@@ -6695,8 +7561,10 @@ function createTestRoom(options) {
|
|
|
6695
7561
|
const dt = seconds > 6 ? 1 / 30 : FIXED_DT;
|
|
6696
7562
|
const n = Math.max(1, Math.round(seconds / dt));
|
|
6697
7563
|
for (let i = 0; i < n; i++) {
|
|
7564
|
+
seating.update();
|
|
6698
7565
|
player.update(0);
|
|
6699
7566
|
push.step(dt);
|
|
7567
|
+
doors.step(dt);
|
|
6700
7568
|
followHeld(dt);
|
|
6701
7569
|
stepSim(dt);
|
|
6702
7570
|
}
|
|
@@ -6831,7 +7699,7 @@ function createTestRoom(options) {
|
|
|
6831
7699
|
room.waitUntilReady();
|
|
6832
7700
|
return true;
|
|
6833
7701
|
},
|
|
6834
|
-
look: (eye, target) =>
|
|
7702
|
+
look: (eye, target) => lookAt(eye, target),
|
|
6835
7703
|
use: (hold) => room.use(hold),
|
|
6836
7704
|
blastAt: (weapon, point) => {
|
|
6837
7705
|
const def = arsenal.weapons.get(weapon);
|
|
@@ -6887,7 +7755,7 @@ function createTestRoom(options) {
|
|
|
6887
7755
|
grab,
|
|
6888
7756
|
throwHeld,
|
|
6889
7757
|
look(position, target) {
|
|
6890
|
-
|
|
7758
|
+
lookAt(position, target);
|
|
6891
7759
|
},
|
|
6892
7760
|
inspect() {
|
|
6893
7761
|
const r = inspectTarget();
|
|
@@ -6954,11 +7822,23 @@ function createTestRoom(options) {
|
|
|
6954
7822
|
busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
|
|
6955
7823
|
loadModule,
|
|
6956
7824
|
warmUp: () => {
|
|
7825
|
+
audio?.warmUp();
|
|
6957
7826
|
warmUp();
|
|
6958
7827
|
warmUpEffects();
|
|
6959
7828
|
},
|
|
6960
7829
|
animations: animationsOf,
|
|
6961
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
|
+
},
|
|
6962
7842
|
bench(moduleId) {
|
|
6963
7843
|
const id = moduleId ?? activeBench;
|
|
6964
7844
|
return (id ? benches.get(id) : void 0) ?? null;
|
|
@@ -7001,6 +7881,11 @@ function createTestRoom(options) {
|
|
|
7001
7881
|
get playerPosition() {
|
|
7002
7882
|
return player.position;
|
|
7003
7883
|
},
|
|
7884
|
+
sit: (objectId, seatId) => seating.sit(objectId, seatId),
|
|
7885
|
+
stand: () => seating.stand(),
|
|
7886
|
+
get seat() {
|
|
7887
|
+
return seating.seat;
|
|
7888
|
+
},
|
|
7004
7889
|
push: { begin: () => push.begin(), end: () => push.end(), setWalk: (walk) => push.setWalk(walk), get state() {
|
|
7005
7890
|
return push.state;
|
|
7006
7891
|
} },
|
|
@@ -7103,8 +7988,8 @@ function createTestRoom(options) {
|
|
|
7103
7988
|
},
|
|
7104
7989
|
onTap: () => toggleTap(),
|
|
7105
7990
|
...combo ? { combo: { label: combo.name?.[locale] ?? combo.id, onToggle: () => setStrikeCombo(!comboOn) } } : {},
|
|
7106
|
-
...arsenal.weapons.has("kick") ? { kick: { label: `${t("ui.kick")} (${kickKeys.map((
|
|
7107
|
-
onBlast: (
|
|
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),
|
|
7108
7993
|
onPlay: (objectId, clipId) => {
|
|
7109
7994
|
playAnimation(clipId, objectId);
|
|
7110
7995
|
},
|
|
@@ -7130,19 +8015,19 @@ function createTestRoom(options) {
|
|
|
7130
8015
|
let nextLightsCheck = 0;
|
|
7131
8016
|
let warmedLights = "";
|
|
7132
8017
|
const lightsKey = () => {
|
|
7133
|
-
let
|
|
8018
|
+
let key2 = "";
|
|
7134
8019
|
scene.traverseVisible((object) => {
|
|
7135
|
-
if (object.isLight)
|
|
8020
|
+
if (object.isLight) key2 += `${object.type[0]}${object.castShadow ? "s" : ""}`;
|
|
7136
8021
|
});
|
|
7137
|
-
return
|
|
8022
|
+
return key2;
|
|
7138
8023
|
};
|
|
7139
8024
|
const loop = (now) => {
|
|
7140
8025
|
raf = requestAnimationFrame(loop);
|
|
7141
8026
|
if (now >= nextLightsCheck) {
|
|
7142
8027
|
nextLightsCheck = now + LIGHTS_CHECK_MS;
|
|
7143
|
-
const
|
|
7144
|
-
if (
|
|
7145
|
-
warmedLights =
|
|
8028
|
+
const key2 = lightsKey();
|
|
8029
|
+
if (key2 !== warmedLights) {
|
|
8030
|
+
warmedLights = key2;
|
|
7146
8031
|
warmUp();
|
|
7147
8032
|
}
|
|
7148
8033
|
}
|
|
@@ -7153,6 +8038,7 @@ function createTestRoom(options) {
|
|
|
7153
8038
|
if (!scenarioRunning) update(dt);
|
|
7154
8039
|
const restoreCamera = room.applyCamera();
|
|
7155
8040
|
gl.render(scene, debugView ?? camera);
|
|
8041
|
+
pinPrograms();
|
|
7156
8042
|
restoreCamera();
|
|
7157
8043
|
roomMetrics.rendered();
|
|
7158
8044
|
};
|