@adatechnology/game-test-room 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +83 -14
- package/dist/index.global.js +7391 -1050
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +962 -161
- package/dist/index.js.map +1 -1
- package/package.json +18 -14
package/dist/index.js
CHANGED
|
@@ -1,13 +1,92 @@
|
|
|
1
1
|
// src/room.ts
|
|
2
2
|
import { createDestruction, JOINT_DEFAULTS, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
|
|
3
|
-
import { createAudioEngine, createAudioSink } from "@adatechnology/game-audio";
|
|
4
|
-
import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
3
|
+
import { createAudioEngine, createAudioSink, loadKitSamples } from "@adatechnology/game-audio";
|
|
4
|
+
import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, impactSoundKeys, specFromObject3D } from "@adatechnology/game-destruction-three";
|
|
5
|
+
import { bodyMass, KNOCK_CHEST, knockOf, surfaceOfMaterial } from "@adatechnology/game-contracts";
|
|
6
|
+
|
|
7
|
+
// src/body-sounds.ts
|
|
8
|
+
import { BODY_FOLEY_BANK } from "@adatechnology/game-audio";
|
|
9
|
+
import { SWING_LEAD_SECONDS, hitSoundId, landSoundId, swingSoundId } from "@adatechnology/game-contracts";
|
|
10
|
+
var DEFAULT_TOOL_BLOWS = {
|
|
11
|
+
hand: "punch",
|
|
12
|
+
fists: "punch",
|
|
13
|
+
kick: "kick",
|
|
14
|
+
shove: "shove",
|
|
15
|
+
knife: "blade",
|
|
16
|
+
machete: "blade",
|
|
17
|
+
utility_knife: "blade",
|
|
18
|
+
axe: "chop",
|
|
19
|
+
hammer: "blunt",
|
|
20
|
+
claw_hammer: "blunt",
|
|
21
|
+
sledgehammer: "blunt",
|
|
22
|
+
baseball_bat: "blunt",
|
|
23
|
+
crowbar: "blunt",
|
|
24
|
+
wrench: "blunt"
|
|
25
|
+
};
|
|
26
|
+
function blowOfTool(id, category, table = DEFAULT_TOOL_BLOWS) {
|
|
27
|
+
const listed = table[id];
|
|
28
|
+
if (listed) return listed;
|
|
29
|
+
if (category === "melee_blade") return "blade";
|
|
30
|
+
if (category === "melee_blunt" || category === "hand") return id === "kick" ? "kick" : "blunt";
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
var levelOf = (energyJ) => Math.max(0.5, Math.min(1, 0.5 + energyJ / 800));
|
|
34
|
+
var STRIDE_METRES = { walk: 0.72, run: 1.05 };
|
|
35
|
+
var SLAM_HURT_SPEED = 3.5;
|
|
36
|
+
function createBodySounds(audio) {
|
|
37
|
+
audio?.register(BODY_FOLEY_BANK);
|
|
38
|
+
const played = [];
|
|
39
|
+
const play = (id, position, volume, delay = 0) => {
|
|
40
|
+
played.push(id);
|
|
41
|
+
if (played.length > 64) played.shift();
|
|
42
|
+
audio?.play(id, { position, volume, urgent: true, ...delay > 0 ? { delay } : {} });
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
swing(blow, position, delay = 0) {
|
|
46
|
+
const id = swingSoundId(blow);
|
|
47
|
+
if (id) play(id, position, 0.8, delay);
|
|
48
|
+
},
|
|
49
|
+
hit(blow, target, position, energyJ) {
|
|
50
|
+
play(hitSoundId(blow, target), position, levelOf(energyJ));
|
|
51
|
+
},
|
|
52
|
+
jump: (position) => play("jump", position, 0.6),
|
|
53
|
+
land: (surface, speed, position) => play(landSoundId(surface, speed), position, Math.min(1, 0.45 + speed / 8)),
|
|
54
|
+
slam(speed, position) {
|
|
55
|
+
play("body_fall", position, Math.min(1, 0.4 + speed / 8));
|
|
56
|
+
if (speed >= SLAM_HURT_SPEED) play("player_hurt", position, Math.min(1, 0.5 + speed / 12));
|
|
57
|
+
},
|
|
58
|
+
played
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
var swingLead = (blow) => SWING_LEAD_SECONDS[blow];
|
|
62
|
+
function createStrideCounter(stride = STRIDE_METRES) {
|
|
63
|
+
let left = stride.walk / 2;
|
|
64
|
+
let next = "right";
|
|
65
|
+
return {
|
|
66
|
+
advance(metres, gait) {
|
|
67
|
+
left -= metres;
|
|
68
|
+
if (left > 0) return null;
|
|
69
|
+
left += stride[gait];
|
|
70
|
+
const foot = next;
|
|
71
|
+
next = next === "right" ? "left" : "right";
|
|
72
|
+
return foot;
|
|
73
|
+
},
|
|
74
|
+
reset() {
|
|
75
|
+
left = stride.walk / 2;
|
|
76
|
+
next = "right";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/room.ts
|
|
5
82
|
import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
|
|
6
83
|
import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
|
|
7
84
|
import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
|
|
8
85
|
import { SPRITE_KINDS, createVfx, patchSpriteKinds } from "@adatechnology/game-vfx";
|
|
9
86
|
import { createVfxRenderer } from "@adatechnology/game-vfx-three";
|
|
10
|
-
import {
|
|
87
|
+
import { createFramePacer, createGeometryPool, createLightBudget, createMaterialPool, createProgramWarmup, createStaticBatcher, spriteProbes } from "@adatechnology/game-render-three";
|
|
88
|
+
import { DEFERRED_PRIORITY, DEFERRED_QUEUE_ID, checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus, sealBlock, sealOpener, shotExtras, twistStroke, TWIST_LIFT, useToHit } from "@adatechnology/game-contracts";
|
|
89
|
+
import { createScheduler, startLoop } from "@adatechnology/game-runtime";
|
|
11
90
|
|
|
12
91
|
// src/arena.ts
|
|
13
92
|
import { markKitOrigin } from "@adatechnology/game-contracts";
|
|
@@ -242,6 +321,38 @@ function createArena(THREE, size, scene, scenery, tapOption) {
|
|
|
242
321
|
};
|
|
243
322
|
}
|
|
244
323
|
|
|
324
|
+
// src/hand-sounds.ts
|
|
325
|
+
import { LEAF_FOLEY_SETS } from "@adatechnology/game-audio";
|
|
326
|
+
function pickupClassOf(def) {
|
|
327
|
+
if (!def) return "plastic";
|
|
328
|
+
const tags = def.tags ?? [];
|
|
329
|
+
if (def.id === "paper") return "paper";
|
|
330
|
+
if (tags.includes("metal")) return "metal";
|
|
331
|
+
if (tags.includes("glass") || tags.includes("ceramic")) return "glass";
|
|
332
|
+
if (tags.includes("plastic") || tags.includes("rubber")) return "plastic";
|
|
333
|
+
if (tags.includes("wood")) return "wood";
|
|
334
|
+
if (tags.includes("fabric")) return "fabric";
|
|
335
|
+
if (tags.includes("plant")) return "food";
|
|
336
|
+
return "plastic";
|
|
337
|
+
}
|
|
338
|
+
var pickupSoundOf = (sounds, def) => `item_pickup_${sounds?.handling ?? pickupClassOf(def)}`;
|
|
339
|
+
function leafFoleyOf(sounds, part, impactKey2, slides2) {
|
|
340
|
+
const named = sounds?.leaves?.[part] ?? sounds?.leaves?.["*"];
|
|
341
|
+
const set = named ? LEAF_FOLEY_SETS[named] : void 0;
|
|
342
|
+
if (set) return set;
|
|
343
|
+
if (slides2) return LEAF_FOLEY_SETS.drawer;
|
|
344
|
+
return /metal|glass/.test(impactKey2) ? LEAF_FOLEY_SETS.metal_door : LEAF_FOLEY_SETS.wood_door;
|
|
345
|
+
}
|
|
346
|
+
var openSoundOf = (sounds, def) => sounds?.open ?? (pickupClassOf(def) === "metal" ? "can_open" : "bottle_open");
|
|
347
|
+
var USE_SOUNDS = {
|
|
348
|
+
drink: "drink_gulp",
|
|
349
|
+
sip: "drink_gulp",
|
|
350
|
+
chug: "drink_gulp",
|
|
351
|
+
eat: "eat_chew"
|
|
352
|
+
};
|
|
353
|
+
var USE_SOUND_EVERY = 0.55;
|
|
354
|
+
var useSoundOf = (sounds, useId) => sounds?.uses?.[useId] ?? USE_SOUNDS[useId];
|
|
355
|
+
|
|
245
356
|
// src/bench.ts
|
|
246
357
|
var snap = (control, value) => {
|
|
247
358
|
const clamped = Math.min(control.max, Math.max(control.min, value));
|
|
@@ -691,7 +802,7 @@ function createExplosives(context) {
|
|
|
691
802
|
if (!detonation.cone) told(results);
|
|
692
803
|
if (detonation.energy > 0) {
|
|
693
804
|
const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
|
|
694
|
-
renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results });
|
|
805
|
+
renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed, report: results, effect: detonation.explosive });
|
|
695
806
|
}
|
|
696
807
|
if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
|
|
697
808
|
const onPlayer = blastEffect(detonation, toArray(camera.position));
|
|
@@ -1187,6 +1298,91 @@ function createLoading(options) {
|
|
|
1187
1298
|
};
|
|
1188
1299
|
}
|
|
1189
1300
|
|
|
1301
|
+
// src/graphics.ts
|
|
1302
|
+
import { createGraphicsController, frameLoad, targetFrameMs } from "@adatechnology/game-graphics";
|
|
1303
|
+
import { createFpsOverlay, createGraphicsScreen } from "@adatechnology/game-graphics/screen";
|
|
1304
|
+
import { createGpuFrameTimer, createGraphicsView, gpuName } from "@adatechnology/game-graphics-three";
|
|
1305
|
+
var QUIET_FRAMES = 10;
|
|
1306
|
+
function createRoomGraphics(o) {
|
|
1307
|
+
const view = createGraphicsView({ renderer: o.gl, camera: o.camera, sun: o.sun });
|
|
1308
|
+
const seconds = o.options.calibrationSeconds ?? 3;
|
|
1309
|
+
const controller = createGraphicsController({
|
|
1310
|
+
store: { ...o.options.storage !== void 0 ? { storage: o.options.storage } : {}, ...o.options.key ? { key: o.options.key } : {} },
|
|
1311
|
+
gpu: gpuName(o.gl),
|
|
1312
|
+
benchmark: seconds > 0 ? { durationS: seconds } : false,
|
|
1313
|
+
// the room measures its own frames once it is up, not the loading of its content
|
|
1314
|
+
calibrate: "manual",
|
|
1315
|
+
apply: (e) => view.apply(e)
|
|
1316
|
+
});
|
|
1317
|
+
const timer = createGpuFrameTimer(o.gl);
|
|
1318
|
+
const screen = o.ui ? createGraphicsScreen({ container: o.container, controller, locale: o.locale }) : null;
|
|
1319
|
+
const overlay = o.ui ? createFpsOverlay({ container: o.container, controller, locale: o.locale }) : null;
|
|
1320
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1321
|
+
let lastHorde = controller.settings.hordeDetail;
|
|
1322
|
+
const unsubscribe = controller.subscribe(() => {
|
|
1323
|
+
if (controller.settings.hordeDetail === lastHorde) return;
|
|
1324
|
+
lastHorde = controller.settings.hordeDetail;
|
|
1325
|
+
for (const l of listeners) l();
|
|
1326
|
+
});
|
|
1327
|
+
let task = null;
|
|
1328
|
+
const syncTask = () => {
|
|
1329
|
+
if (controller.phase === "calibrating") {
|
|
1330
|
+
task ?? (task = o.loading.begin(o.label, { total: 100 }));
|
|
1331
|
+
task.step(`${Math.round(controller.calibrationProgress * 100)}%`, Math.round(controller.calibrationProgress * 100));
|
|
1332
|
+
} else if (task) {
|
|
1333
|
+
task.end();
|
|
1334
|
+
task = null;
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
syncTask();
|
|
1338
|
+
let started = 0;
|
|
1339
|
+
let quiet = 0;
|
|
1340
|
+
let lastEnd = 0;
|
|
1341
|
+
return {
|
|
1342
|
+
controller,
|
|
1343
|
+
view,
|
|
1344
|
+
screen,
|
|
1345
|
+
host: {
|
|
1346
|
+
hordeDetail: () => controller.settings.hordeDetail,
|
|
1347
|
+
onChange(listener) {
|
|
1348
|
+
listeners.add(listener);
|
|
1349
|
+
return () => listeners.delete(listener);
|
|
1350
|
+
},
|
|
1351
|
+
report: (key2, value) => controller.report(key2, value)
|
|
1352
|
+
},
|
|
1353
|
+
frameStart() {
|
|
1354
|
+
started = performance.now();
|
|
1355
|
+
},
|
|
1356
|
+
beforeRender(camera) {
|
|
1357
|
+
view.hold();
|
|
1358
|
+
view.follow(camera.position.x, camera.position.z);
|
|
1359
|
+
timer.begin();
|
|
1360
|
+
},
|
|
1361
|
+
afterRender() {
|
|
1362
|
+
timer.end();
|
|
1363
|
+
},
|
|
1364
|
+
frameEnd() {
|
|
1365
|
+
const now = performance.now();
|
|
1366
|
+
const realSeconds = lastEnd > 0 ? (now - lastEnd) / 1e3 : 0;
|
|
1367
|
+
lastEnd = now;
|
|
1368
|
+
if (!(realSeconds > 0)) return;
|
|
1369
|
+
const cpuMs = started > 0 ? now - started : realSeconds * 1e3;
|
|
1370
|
+
controller.frame(realSeconds, frameLoad({ cpuMs, gpuMs: timer.ms, intervalMs: realSeconds * 1e3, targetMs: targetFrameMs(controller.settings.fpsCap) }));
|
|
1371
|
+
quiet = o.loading.active && !task ? 0 : quiet + 1;
|
|
1372
|
+
if (o.options.calibrate !== "manual" && quiet >= QUIET_FRAMES) controller.calibrateIfNeeded();
|
|
1373
|
+
syncTask();
|
|
1374
|
+
},
|
|
1375
|
+
dispose() {
|
|
1376
|
+
task?.end();
|
|
1377
|
+
unsubscribe();
|
|
1378
|
+
listeners.clear();
|
|
1379
|
+
timer.dispose();
|
|
1380
|
+
screen?.dispose();
|
|
1381
|
+
overlay?.dispose();
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1190
1386
|
// src/log.ts
|
|
1191
1387
|
var MAX_ENTRIES = 5e3;
|
|
1192
1388
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
@@ -1316,8 +1512,12 @@ function createLog(options = {}) {
|
|
|
1316
1512
|
}
|
|
1317
1513
|
|
|
1318
1514
|
// src/metrics.ts
|
|
1319
|
-
var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals", "blast_shelter", "blast_confinement", "blast_fragments"];
|
|
1515
|
+
var ROOM_METRICS = ["fps", "frame_ms", "draw_calls", "triangles", "objects", "fragments", "particles", "decals", "blast_shelter", "blast_confinement", "blast_fragments", "geometries", "textures", "heap_mb"];
|
|
1320
1516
|
var WINDOW_S = 0.5;
|
|
1517
|
+
function heapMB() {
|
|
1518
|
+
const memory = globalThis.performance?.memory;
|
|
1519
|
+
return typeof memory?.usedJSHeapSize === "number" ? memory.usedJSHeapSize / 1048576 : void 0;
|
|
1520
|
+
}
|
|
1321
1521
|
function createRoomMetrics(sources) {
|
|
1322
1522
|
const wanted = new Set(sources.ids);
|
|
1323
1523
|
const gl = sources.gl;
|
|
@@ -1353,6 +1553,9 @@ function createRoomMetrics(sources) {
|
|
|
1353
1553
|
const render = ownsRender ? { calls, triangles } : gl.info?.render;
|
|
1354
1554
|
put("draw_calls", render?.calls);
|
|
1355
1555
|
put("triangles", render?.triangles);
|
|
1556
|
+
put("geometries", gl.info?.memory?.geometries);
|
|
1557
|
+
put("textures", gl.info?.memory?.textures);
|
|
1558
|
+
put("heap_mb", heapMB());
|
|
1356
1559
|
const blast = sources.blast?.();
|
|
1357
1560
|
if (blast) {
|
|
1358
1561
|
put("blast_shelter", blast.shelter * 100);
|
|
@@ -1372,6 +1575,43 @@ function createRoomMetrics(sources) {
|
|
|
1372
1575
|
};
|
|
1373
1576
|
}
|
|
1374
1577
|
|
|
1578
|
+
// src/memory-watch.ts
|
|
1579
|
+
function createMemoryWatch(options) {
|
|
1580
|
+
const clears = Math.max(2, options.clears ?? 4);
|
|
1581
|
+
const budget = { geometries: options.budget?.geometries ?? 40, textures: options.budget?.textures ?? 4 };
|
|
1582
|
+
const histories = /* @__PURE__ */ new Map();
|
|
1583
|
+
let last;
|
|
1584
|
+
return {
|
|
1585
|
+
cleared(where) {
|
|
1586
|
+
const now = options.read();
|
|
1587
|
+
if (!now) return void 0;
|
|
1588
|
+
const key2 = where ?? "";
|
|
1589
|
+
const history = histories.get(key2) ?? [];
|
|
1590
|
+
histories.set(key2, history);
|
|
1591
|
+
history.push(now);
|
|
1592
|
+
if (history.length > clears + 1) history.shift();
|
|
1593
|
+
const first = history[0];
|
|
1594
|
+
const grew = { geometries: now.geometries - first.geometries, textures: now.textures - first.textures };
|
|
1595
|
+
let growing = history.length === clears + 1;
|
|
1596
|
+
for (let i = 1; growing && i < history.length; i++) {
|
|
1597
|
+
const before = history[i - 1];
|
|
1598
|
+
const after = history[i];
|
|
1599
|
+
growing = after.geometries > before.geometries || after.textures > before.textures;
|
|
1600
|
+
}
|
|
1601
|
+
const leaking = growing && (grew.geometries > budget.geometries || grew.textures > budget.textures);
|
|
1602
|
+
last = { after: now, grew, leaking };
|
|
1603
|
+
return last;
|
|
1604
|
+
},
|
|
1605
|
+
get last() {
|
|
1606
|
+
return last;
|
|
1607
|
+
},
|
|
1608
|
+
reset() {
|
|
1609
|
+
histories.clear();
|
|
1610
|
+
last = void 0;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1375
1615
|
// src/narrate.ts
|
|
1376
1616
|
var SKIP_DAMAGE = /* @__PURE__ */ new Set(["fire", "heat"]);
|
|
1377
1617
|
function createNarrator(c) {
|
|
@@ -1885,6 +2125,9 @@ function createNarrator(c) {
|
|
|
1885
2125
|
}
|
|
1886
2126
|
|
|
1887
2127
|
// src/player.ts
|
|
2128
|
+
import { stepKnock } from "@adatechnology/game-contracts";
|
|
2129
|
+
var SLAM_MIN_SPEED = 1.5;
|
|
2130
|
+
var LANDING_MIN_SPEED = 1;
|
|
1888
2131
|
var EYE = 1.7;
|
|
1889
2132
|
var PLAYER_RADIUS = 0.3;
|
|
1890
2133
|
var FOOT = 0.5;
|
|
@@ -1901,6 +2144,9 @@ var GRAVITY = 13;
|
|
|
1901
2144
|
var FLOOR_CLIMB_SPEED = 3;
|
|
1902
2145
|
var FLOOR_FALL_SPEED = 6;
|
|
1903
2146
|
var LOOK = 22e-4;
|
|
2147
|
+
var LYING_EYE = 0.3;
|
|
2148
|
+
var LYING_PITCH = 1.05;
|
|
2149
|
+
var LYING_ROLL = 0.35;
|
|
1904
2150
|
function createPlayer(THREE, o) {
|
|
1905
2151
|
const cam = o.camera;
|
|
1906
2152
|
let yaw = 0;
|
|
@@ -1911,6 +2157,7 @@ function createPlayer(THREE, o) {
|
|
|
1911
2157
|
let bend = 0;
|
|
1912
2158
|
let bendTarget = 0;
|
|
1913
2159
|
let vy = 0;
|
|
2160
|
+
let floorUnder = 0;
|
|
1914
2161
|
let walked = 0;
|
|
1915
2162
|
let running = false;
|
|
1916
2163
|
const px = { x: o.start[0], z: o.start[2] };
|
|
@@ -1937,10 +2184,15 @@ function createPlayer(THREE, o) {
|
|
|
1937
2184
|
const reach = (gap > 0 ? FLOOR_CLIMB_SPEED : FLOOR_FALL_SPEED) * dt;
|
|
1938
2185
|
floorLevel += Math.max(-reach, Math.min(reach, gap));
|
|
1939
2186
|
};
|
|
2187
|
+
let knocked = null;
|
|
2188
|
+
let rollSide = 1;
|
|
1940
2189
|
const apply = () => {
|
|
1941
|
-
|
|
2190
|
+
const lying = knocked?.lying ?? 0;
|
|
2191
|
+
const fallen = lying * lying * (3 - 2 * lying);
|
|
2192
|
+
euler.set(pitch + (LYING_PITCH - pitch) * fallen, yaw, LYING_ROLL * rollSide * fallen, "YXZ");
|
|
1942
2193
|
cam.quaternion.setFromEuler(euler);
|
|
1943
|
-
|
|
2194
|
+
const standing = eye - CROUCH_DROP * crouch - bend;
|
|
2195
|
+
cam.position.set(px.x, floorLevel + y + standing + (LYING_EYE - standing) * fallen, px.z);
|
|
1944
2196
|
cam.updateMatrixWorld(true);
|
|
1945
2197
|
};
|
|
1946
2198
|
apply();
|
|
@@ -2115,6 +2367,41 @@ function createPlayer(THREE, o) {
|
|
|
2115
2367
|
window.addEventListener("keyup", onKeyUp);
|
|
2116
2368
|
window.addEventListener("blur", onBlur);
|
|
2117
2369
|
window.addEventListener("beforeunload", onBeforeUnload);
|
|
2370
|
+
const stepKnocked = (dt) => {
|
|
2371
|
+
const k = knocked;
|
|
2372
|
+
if (!k) return;
|
|
2373
|
+
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2374
|
+
floorUnder = floor;
|
|
2375
|
+
const at = { x: px.x, y, z: px.z };
|
|
2376
|
+
const fromY = y;
|
|
2377
|
+
const fallSpeed = Math.max(0, -k.velocity[1]);
|
|
2378
|
+
const done = stepKnock(k, at, dt, () => floor, true);
|
|
2379
|
+
const wantX = at.x - px.x;
|
|
2380
|
+
const wantZ = at.z - px.z;
|
|
2381
|
+
const fromX = px.x;
|
|
2382
|
+
const fromZ = px.z;
|
|
2383
|
+
const vx = k.velocity[0];
|
|
2384
|
+
const vz = k.velocity[2];
|
|
2385
|
+
move(wantX, wantZ);
|
|
2386
|
+
const stoppedX = Math.abs(wantX) > 1e-6 && Math.abs(px.x - fromX) < Math.abs(wantX) * 0.5;
|
|
2387
|
+
const stoppedZ = Math.abs(wantZ) > 1e-6 && Math.abs(px.z - fromZ) < Math.abs(wantZ) * 0.5;
|
|
2388
|
+
if (stoppedX) k.velocity[0] = 0;
|
|
2389
|
+
if (stoppedZ) k.velocity[2] = 0;
|
|
2390
|
+
const into = Math.hypot(stoppedX ? vx : 0, stoppedZ ? vz : 0);
|
|
2391
|
+
if (into >= SLAM_MIN_SPEED) o.onSlam?.({ against: "obstacle", speed: into, direction: [stoppedX ? vx / into : 0, 0, stoppedZ ? vz / into : 0] });
|
|
2392
|
+
y = at.y;
|
|
2393
|
+
if (fromY > floor && y <= floor) {
|
|
2394
|
+
const speed = Math.max(fallSpeed, (fromY - y) / Math.max(dt, 1e-6));
|
|
2395
|
+
if (k.down && speed >= SLAM_MIN_SPEED * 0.5) o.onSlam?.({ against: "ground", speed: Math.hypot(speed, Math.hypot(vx, vz) * 0.5), direction: [0, -1, 0] });
|
|
2396
|
+
else if (speed >= LANDING_MIN_SPEED) o.onFeet?.({ type: "land", speed });
|
|
2397
|
+
}
|
|
2398
|
+
vy = 0;
|
|
2399
|
+
walked = dt > 0 ? Math.hypot(px.x - fromX, px.z - fromZ) / dt : 0;
|
|
2400
|
+
running = false;
|
|
2401
|
+
if (done) knocked = null;
|
|
2402
|
+
followFloor(dt);
|
|
2403
|
+
apply();
|
|
2404
|
+
};
|
|
2118
2405
|
return {
|
|
2119
2406
|
get walkIntent() {
|
|
2120
2407
|
return walkKeys();
|
|
@@ -2163,11 +2450,35 @@ function createPlayer(THREE, o) {
|
|
|
2163
2450
|
get running() {
|
|
2164
2451
|
return running;
|
|
2165
2452
|
},
|
|
2453
|
+
get grounded() {
|
|
2454
|
+
return y <= floorUnder + 1e-3 && vy <= 0;
|
|
2455
|
+
},
|
|
2166
2456
|
get crouching() {
|
|
2167
2457
|
return isCrouched();
|
|
2168
2458
|
},
|
|
2169
2459
|
keys,
|
|
2460
|
+
knock(next) {
|
|
2461
|
+
if (seated) return;
|
|
2462
|
+
if (knocked) {
|
|
2463
|
+
knocked.velocity = [knocked.velocity[0] + next.velocity[0], knocked.velocity[1] + next.velocity[1], knocked.velocity[2] + next.velocity[2]];
|
|
2464
|
+
knocked.airborne || (knocked.airborne = next.airborne);
|
|
2465
|
+
knocked.down || (knocked.down = next.down);
|
|
2466
|
+
} else knocked = { ...next, velocity: [...next.velocity] };
|
|
2467
|
+
if (next.down) {
|
|
2468
|
+
const right = Math.cos(yaw) * next.velocity[0] - Math.sin(yaw) * next.velocity[2];
|
|
2469
|
+
rollSide = right >= 0 ? -1 : 1;
|
|
2470
|
+
crouchLatched = false;
|
|
2471
|
+
}
|
|
2472
|
+
if (knocked.airborne) vy = Math.max(vy, knocked.velocity[1]);
|
|
2473
|
+
},
|
|
2474
|
+
get knocked() {
|
|
2475
|
+
return knocked;
|
|
2476
|
+
},
|
|
2170
2477
|
update(dt) {
|
|
2478
|
+
if (knocked) {
|
|
2479
|
+
stepKnocked(dt);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2171
2482
|
if (crouchPress) crouchPress.held += dt;
|
|
2172
2483
|
const crouched = isCrouched();
|
|
2173
2484
|
crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
|
|
@@ -2188,13 +2499,18 @@ function createPlayer(THREE, o) {
|
|
|
2188
2499
|
return;
|
|
2189
2500
|
}
|
|
2190
2501
|
const floor = Math.max(floorHere(), o.footing?.(px.x, px.z, PLAYER_RADIUS * FOOT, y).floor ?? -Infinity);
|
|
2502
|
+
floorUnder = floor;
|
|
2191
2503
|
if (y < floor && vy <= 0) y = floor;
|
|
2192
|
-
if (y <= floor && keys.has("Space") && !walkHeld)
|
|
2504
|
+
if (y <= floor && keys.has("Space") && !walkHeld) {
|
|
2505
|
+
vy = JUMP;
|
|
2506
|
+
o.onFeet?.({ type: "jump" });
|
|
2507
|
+
}
|
|
2193
2508
|
if (y > floor || vy > 0) {
|
|
2194
2509
|
vy -= GRAVITY * dt;
|
|
2195
2510
|
y += vy * dt;
|
|
2196
2511
|
if (y <= floor) {
|
|
2197
2512
|
y = floor;
|
|
2513
|
+
if (-vy >= LANDING_MIN_SPEED) o.onFeet?.({ type: "land", speed: -vy });
|
|
2198
2514
|
vy = 0;
|
|
2199
2515
|
}
|
|
2200
2516
|
}
|
|
@@ -2202,6 +2518,7 @@ function createPlayer(THREE, o) {
|
|
|
2202
2518
|
apply();
|
|
2203
2519
|
},
|
|
2204
2520
|
setPose(position, yw, pt) {
|
|
2521
|
+
knocked = null;
|
|
2205
2522
|
px.x = position[0];
|
|
2206
2523
|
px.z = position[2];
|
|
2207
2524
|
floorLevel = floorHere();
|
|
@@ -2213,6 +2530,7 @@ function createPlayer(THREE, o) {
|
|
|
2213
2530
|
apply();
|
|
2214
2531
|
},
|
|
2215
2532
|
look(position, target) {
|
|
2533
|
+
knocked = null;
|
|
2216
2534
|
px.x = position[0];
|
|
2217
2535
|
px.z = position[2];
|
|
2218
2536
|
floorLevel = floorHere();
|
|
@@ -2569,6 +2887,8 @@ var TURN_RATE = 8;
|
|
|
2569
2887
|
var MAX_WALK_SECONDS = 1.5;
|
|
2570
2888
|
var APPROACH_SPEED2 = 3;
|
|
2571
2889
|
var FLOOR_GAP = 0.03;
|
|
2890
|
+
var LEAF_SLAM_VOLUME = 0.85;
|
|
2891
|
+
var shutVolume = (massKg) => Math.min(1, 0.3 + massKg * 0.03);
|
|
2572
2892
|
var key = (objectId, part) => `${objectId}\0${part}`;
|
|
2573
2893
|
var slides = (clip, part) => {
|
|
2574
2894
|
const last = clip.keys[clip.keys.length - 1]?.parts?.[part];
|
|
@@ -2814,7 +3134,7 @@ function createRoomDoors(options) {
|
|
|
2814
3134
|
using = { objectId, leaf, obj, grip, pull, phase: control.phase, progress: 0, walked: 0, there: goal === null };
|
|
2815
3135
|
const at = obj.localToWorld(grip.clone());
|
|
2816
3136
|
if (goal === null) {
|
|
2817
|
-
options.sound("leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
3137
|
+
options.sound(options.leafFoley?.(objectId, leaf.part, slides(leaf.clip, leaf.part))?.locked ?? "leaf_rattle", [at.x, at.y, at.z], 0.6);
|
|
2818
3138
|
event("locked", using);
|
|
2819
3139
|
} else event("reach", using);
|
|
2820
3140
|
return true;
|
|
@@ -2899,17 +3219,27 @@ function createRoomDoors(options) {
|
|
|
2899
3219
|
const at = u.obj.localToWorld(tmp.copy(u.grip));
|
|
2900
3220
|
const point = [at.x, at.y, at.z];
|
|
2901
3221
|
const slide = slides(u.leaf.clip, u.leaf.part);
|
|
2902
|
-
const
|
|
3222
|
+
const foley = options.leafFoley?.(u.objectId, u.leaf.part, slide) ?? null;
|
|
3223
|
+
const own = (moment) => options.leafSound?.(u.objectId, u.leaf.part, moment);
|
|
3224
|
+
const shut = own("shut") ?? options.impactKey(u.objectId, u.leaf.part).replace("impact_", "leaf_shut_");
|
|
2903
3225
|
if (wasPhase === "reach" && s.phase !== "reach") {
|
|
2904
|
-
|
|
2905
|
-
|
|
3226
|
+
const opening = foley ? void 0 : own("open");
|
|
3227
|
+
if (foley) {
|
|
3228
|
+
if (was < 0.02) options.sound(foley.open, point, 0.5);
|
|
3229
|
+
if (foley.turn) options.sound(foley.turn, point, 0.35);
|
|
3230
|
+
} else if (slide) options.sound("leaf_slide", point, 0.5);
|
|
3231
|
+
else if (opening) {
|
|
3232
|
+
if (was < 0.02) options.sound(opening, point, 0.5);
|
|
3233
|
+
} else {
|
|
2906
3234
|
if (was < 0.02) options.sound("leaf_latch", point, 0.5);
|
|
2907
3235
|
options.sound(/metal|glass/.test(shut) ? "leaf_creak_metal" : "leaf_creak_wood", point, 0.35);
|
|
2908
3236
|
}
|
|
2909
3237
|
}
|
|
2910
3238
|
if (s.arrived) {
|
|
2911
3239
|
if (s.opening <= 0) {
|
|
2912
|
-
|
|
3240
|
+
const volume = shutVolume(massOf(u.objectId, u.leaf.part));
|
|
3241
|
+
if (foley) options.sound(foley.slam && volume >= LEAF_SLAM_VOLUME ? foley.slam : foley.close, point, volume);
|
|
3242
|
+
else options.sound(slide ? "leaf_slide" : shut, point, volume);
|
|
2913
3243
|
event("closed", u);
|
|
2914
3244
|
} else event("opened", u);
|
|
2915
3245
|
}
|
|
@@ -3121,6 +3451,11 @@ var en = {
|
|
|
3121
3451
|
"metric.fragments": "Fragments",
|
|
3122
3452
|
"metric.particles": "Particles",
|
|
3123
3453
|
"metric.decals": "Decals",
|
|
3454
|
+
"metric.geometries": "Geometries",
|
|
3455
|
+
"metric.textures": "Textures",
|
|
3456
|
+
"metric.heap_mb": "Heap MB",
|
|
3457
|
+
"memory.leak_short": "memory leak: +{geometries} geometries, +{textures} textures per {clears} clears",
|
|
3458
|
+
"memory.leak": "Memory keeps growing after each clear: +{geometries} geometries and +{textures} textures over the last {clears} clears. Something removed from the scene was not released (discardObject3D).",
|
|
3124
3459
|
"act.spawn": "Spawned {name} at x {x} \xB7 z {z}",
|
|
3125
3460
|
"act.clear": "Cleared the room",
|
|
3126
3461
|
"act.reset": "Repaired every object",
|
|
@@ -3167,11 +3502,18 @@ var en = {
|
|
|
3167
3502
|
"act.throw": "Threw {name} at {speed} m/s",
|
|
3168
3503
|
"ground.name": "Ground",
|
|
3169
3504
|
"act.landing": "{name} landed at {speed} m/s: {energy} J",
|
|
3505
|
+
"act.player_land": "You landed at {speed} m/s",
|
|
3170
3506
|
"act.bench_failed": "{name} failed: {error}",
|
|
3171
3507
|
"loading.working": "Working\u2026",
|
|
3508
|
+
"loading.graphics": "Calibrating graphics\u2026",
|
|
3509
|
+
"ui.graphics": "Graphics",
|
|
3172
3510
|
"loading.open_area": "Opening {area}\u2026",
|
|
3173
3511
|
"loading.spawn": "Building {name}\u2026",
|
|
3174
3512
|
"act.loosened": "{name} was heavy but free: the blast moved it",
|
|
3513
|
+
"act.knocked_down": "The blast threw the player off their feet at {speed} m/s ({reach}% got through the cover)",
|
|
3514
|
+
"act.player_slammed": "The player was thrown into {what} at {speed} m/s",
|
|
3515
|
+
"act.player_fell": "The player hit the floor at {speed} m/s",
|
|
3516
|
+
"act.player_shoved": "The blast pushed the player at {speed} m/s ({reach}% got through the cover)",
|
|
3175
3517
|
"act.came_apart": "{name} came apart: what stayed is loose and falls",
|
|
3176
3518
|
"act.shoved": "{name} stands free: the shots moved it",
|
|
3177
3519
|
"act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
|
|
@@ -3471,6 +3813,11 @@ var ptBR = {
|
|
|
3471
3813
|
"metric.fragments": "Fragmentos",
|
|
3472
3814
|
"metric.particles": "Part\xEDculas",
|
|
3473
3815
|
"metric.decals": "Decalques",
|
|
3816
|
+
"metric.geometries": "Geometrias",
|
|
3817
|
+
"metric.textures": "Texturas",
|
|
3818
|
+
"metric.heap_mb": "Heap MB",
|
|
3819
|
+
"memory.leak_short": "vazamento de mem\xF3ria: +{geometries} geometrias, +{textures} texturas em {clears} limpezas",
|
|
3820
|
+
"memory.leak": "A mem\xF3ria continua subindo a cada limpeza: +{geometries} geometrias e +{textures} texturas nas \xFAltimas {clears} limpezas. Algo tirado da cena n\xE3o foi liberado (discardObject3D).",
|
|
3474
3821
|
"act.spawn": "Criou {name} em x {x} \xB7 z {z}",
|
|
3475
3822
|
"act.clear": "Limpou a sala",
|
|
3476
3823
|
"act.reset": "Consertou todos os objetos",
|
|
@@ -3517,11 +3864,18 @@ var ptBR = {
|
|
|
3517
3864
|
"act.throw": "Arremessou {name} a {speed} m/s",
|
|
3518
3865
|
"ground.name": "Ch\xE3o",
|
|
3519
3866
|
"act.landing": "{name} caiu a {speed} m/s: {energy} J",
|
|
3867
|
+
"act.player_land": "Voc\xEA aterrissou a {speed} m/s",
|
|
3520
3868
|
"act.bench_failed": "{name} falhou: {error}",
|
|
3521
3869
|
"loading.working": "Trabalhando\u2026",
|
|
3870
|
+
"loading.graphics": "Calibrando os gr\xE1ficos\u2026",
|
|
3871
|
+
"ui.graphics": "Gr\xE1ficos",
|
|
3522
3872
|
"loading.open_area": "Abrindo {area}\u2026",
|
|
3523
3873
|
"loading.spawn": "Montando {name}\u2026",
|
|
3524
3874
|
"act.loosened": "{name} \xE9 pesado mas estava solto: a explos\xE3o o moveu",
|
|
3875
|
+
"act.knocked_down": "A explos\xE3o derrubou o jogador a {speed} m/s ({reach}% passou pela cobertura)",
|
|
3876
|
+
"act.player_slammed": "O jogador foi arremessado contra {what} a {speed} m/s",
|
|
3877
|
+
"act.player_fell": "O jogador bateu no ch\xE3o a {speed} m/s",
|
|
3878
|
+
"act.player_shoved": "A explos\xE3o empurrou o jogador a {speed} m/s ({reach}% passou pela cobertura)",
|
|
3525
3879
|
"act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
|
|
3526
3880
|
"act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
|
|
3527
3881
|
"act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
|
|
@@ -4110,6 +4464,7 @@ var CSS = `
|
|
|
4110
4464
|
.tr-top{position:absolute;top:8px;left:50%;transform:translateX(-50%);text-align:center;max-width:46vw}
|
|
4111
4465
|
.tr-tool{display:inline-block;background:rgba(10,12,16,.72);padding:4px 12px;border-radius:6px;font-size:14px;font-weight:600}
|
|
4112
4466
|
.tr-tool small{font-weight:400;opacity:.8;margin-left:8px}
|
|
4467
|
+
.tr-alert{display:none;position:relative;z-index:20;margin-top:6px;background:rgba(183,28,28,.92);color:#fff;padding:6px 12px;border-radius:6px;font-size:13px;font-weight:600;box-shadow:0 2px 8px rgba(0,0,0,.4)}
|
|
4113
4468
|
.tr-status{margin-top:4px;min-height:16px;color:#ffd23f;text-shadow:0 1px 2px #000}
|
|
4114
4469
|
.tr-prog{margin:4px auto 0;width:260px;background:rgba(10,12,16,.72);border-radius:6px;padding:4px 8px;display:none}
|
|
4115
4470
|
.tr-prog b{display:block;font-weight:500;margin-bottom:3px}
|
|
@@ -4228,13 +4583,14 @@ function createUi(o) {
|
|
|
4228
4583
|
const top = mk3("div", "tr-top");
|
|
4229
4584
|
const toolBox = mk3("div", "tr-tool");
|
|
4230
4585
|
const status = mk3("div", "tr-status");
|
|
4586
|
+
const alertBox = mk3("div", "tr-alert");
|
|
4231
4587
|
const prog = mk3("div", "tr-prog");
|
|
4232
4588
|
const progLabel = mk3("b");
|
|
4233
4589
|
const progBar = mk3("div", "tr-bar");
|
|
4234
4590
|
const progFill = mk3("i");
|
|
4235
4591
|
progBar.appendChild(progFill);
|
|
4236
4592
|
prog.append(progLabel, progBar);
|
|
4237
|
-
top.append(toolBox, status, prog);
|
|
4593
|
+
top.append(toolBox, alertBox, status, prog);
|
|
4238
4594
|
const bottom = mk3("div", "tr-bottom");
|
|
4239
4595
|
const held = mk3("div", "tr-held");
|
|
4240
4596
|
const hotbar = mk3("div", "tr-hotbar");
|
|
@@ -4345,9 +4701,16 @@ function createUi(o) {
|
|
|
4345
4701
|
nodes.delete(oldest.value);
|
|
4346
4702
|
logBody.firstElementChild?.remove();
|
|
4347
4703
|
}
|
|
4348
|
-
if (autoscroll)
|
|
4704
|
+
if (autoscroll) isScrollDue = true;
|
|
4349
4705
|
}
|
|
4706
|
+
let isScrollDue = false;
|
|
4707
|
+
const scrollToEnd = () => {
|
|
4708
|
+
if (!isScrollDue) return;
|
|
4709
|
+
isScrollDue = false;
|
|
4710
|
+
logBody.scrollTop = logBody.scrollHeight;
|
|
4711
|
+
};
|
|
4350
4712
|
const flushLog = () => {
|
|
4713
|
+
scrollToEnd();
|
|
4351
4714
|
if (pendingAdds.length === 0 && pendingUpdates.size === 0) return;
|
|
4352
4715
|
if (!isLogVisible()) {
|
|
4353
4716
|
isLogStale = true;
|
|
@@ -4461,17 +4824,22 @@ function createUi(o) {
|
|
|
4461
4824
|
fullscreenBtn.title = t("ui.fullscreen.hint");
|
|
4462
4825
|
const blastBtns = ["shelter", "fragmentHits", "confinement"].map((key2) => ({ key: key2, b: ctl(t(`ui.blast.${key2}`), () => o.onBlast(key2)) }));
|
|
4463
4826
|
for (const { key: key2, b } of blastBtns) b.title = t(`ui.blast.${key2}.hint`);
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
qSel
|
|
4827
|
+
if (o.onGraphics) {
|
|
4828
|
+
const g = ctl(t("ui.graphics"), () => o.onGraphics());
|
|
4829
|
+
g.dataset.testid = "graphics-button";
|
|
4830
|
+
} else {
|
|
4831
|
+
const qSel = mk3("select");
|
|
4832
|
+
for (const q of ["low", "medium", "high"]) {
|
|
4833
|
+
const op = mk3("option", void 0, t(`ui.quality.${q}`));
|
|
4834
|
+
op.value = q;
|
|
4835
|
+
qSel.appendChild(op);
|
|
4836
|
+
}
|
|
4837
|
+
qSel.value = o.quality;
|
|
4838
|
+
qSel.onchange = () => o.onQuality(qSel.value);
|
|
4839
|
+
const qLabel = mk3("label", void 0, t("ui.quality") + " ");
|
|
4840
|
+
qLabel.appendChild(qSel);
|
|
4841
|
+
controls.appendChild(qLabel);
|
|
4469
4842
|
}
|
|
4470
|
-
qSel.value = o.quality;
|
|
4471
|
-
qSel.onchange = () => o.onQuality(qSel.value);
|
|
4472
|
-
const qLabel = mk3("label", void 0, t("ui.quality") + " ");
|
|
4473
|
-
qLabel.appendChild(qSel);
|
|
4474
|
-
controls.appendChild(qLabel);
|
|
4475
4843
|
const count = mk3("span");
|
|
4476
4844
|
controls.appendChild(count);
|
|
4477
4845
|
cat.appendChild(controls);
|
|
@@ -4697,6 +5065,10 @@ function createUi(o) {
|
|
|
4697
5065
|
renderInspector(s.inspect);
|
|
4698
5066
|
}
|
|
4699
5067
|
},
|
|
5068
|
+
alert(text) {
|
|
5069
|
+
alertBox.textContent = text ?? "";
|
|
5070
|
+
alertBox.style.display = text ? "block" : "none";
|
|
5071
|
+
},
|
|
4700
5072
|
toast(text) {
|
|
4701
5073
|
status.textContent = text;
|
|
4702
5074
|
if (toastTimer) clearTimeout(toastTimer);
|
|
@@ -4731,9 +5103,144 @@ function createUi(o) {
|
|
|
4731
5103
|
return api;
|
|
4732
5104
|
}
|
|
4733
5105
|
|
|
5106
|
+
// src/module-views.ts
|
|
5107
|
+
var FRAME_PX = 3;
|
|
5108
|
+
function createModuleViews(THREE, gl) {
|
|
5109
|
+
const entries = [];
|
|
5110
|
+
let mainCamera = null;
|
|
5111
|
+
const overlay = new THREE.Scene();
|
|
5112
|
+
const ortho = new THREE.OrthographicCamera(-0.5, 0.5, 0.5, -0.5, 0, 2);
|
|
5113
|
+
ortho.position.z = 1;
|
|
5114
|
+
const plane = new THREE.PlaneGeometry(1, 1);
|
|
5115
|
+
const roundedMask = (rounded, aspect) => {
|
|
5116
|
+
if (!rounded || typeof document === "undefined") return null;
|
|
5117
|
+
const canvas = document.createElement("canvas");
|
|
5118
|
+
canvas.width = 256;
|
|
5119
|
+
canvas.height = Math.max(16, Math.round(256 / aspect));
|
|
5120
|
+
const g = canvas.getContext("2d");
|
|
5121
|
+
if (!g) return null;
|
|
5122
|
+
const r = Math.min(canvas.width, canvas.height) * Math.min(0.5, rounded);
|
|
5123
|
+
g.fillStyle = "#000";
|
|
5124
|
+
g.fillRect(0, 0, canvas.width, canvas.height);
|
|
5125
|
+
g.fillStyle = "#fff";
|
|
5126
|
+
g.beginPath();
|
|
5127
|
+
g.roundRect(0, 0, canvas.width, canvas.height, r);
|
|
5128
|
+
g.fill();
|
|
5129
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
5130
|
+
texture.colorSpace = THREE.NoColorSpace;
|
|
5131
|
+
return texture;
|
|
5132
|
+
};
|
|
5133
|
+
const size = () => {
|
|
5134
|
+
if (typeof gl.getSize !== "function") return { w: 960, h: 600 };
|
|
5135
|
+
const v = new THREE.Vector2();
|
|
5136
|
+
gl.getSize(v);
|
|
5137
|
+
return { w: v.x, h: v.y };
|
|
5138
|
+
};
|
|
5139
|
+
const take = (entry) => {
|
|
5140
|
+
const i = entries.indexOf(entry);
|
|
5141
|
+
if (i < 0) return;
|
|
5142
|
+
entries.splice(i, 1);
|
|
5143
|
+
overlay.remove(entry.quad, entry.frame);
|
|
5144
|
+
entry.target.dispose();
|
|
5145
|
+
entry.quad.material.dispose();
|
|
5146
|
+
entry.frame.material.dispose();
|
|
5147
|
+
entry.alpha?.dispose();
|
|
5148
|
+
};
|
|
5149
|
+
return {
|
|
5150
|
+
add(view) {
|
|
5151
|
+
const { w, h } = size();
|
|
5152
|
+
const scale = view.resolution ?? 0.5;
|
|
5153
|
+
const pr = typeof gl.getPixelRatio === "function" ? gl.getPixelRatio() : 1;
|
|
5154
|
+
const target = new THREE.WebGLRenderTarget(Math.max(16, Math.round(w * view.rect.width * pr * scale)), Math.max(16, Math.round(h * view.rect.height * pr * scale)), { samples: 2 });
|
|
5155
|
+
target.texture.colorSpace = THREE.LinearSRGBColorSpace;
|
|
5156
|
+
const alpha = roundedMask(view.rounded ?? 0, w * view.rect.width / Math.max(1, h * view.rect.height));
|
|
5157
|
+
const quad = new THREE.Mesh(plane, new THREE.MeshBasicMaterial({ map: target.texture, side: THREE.DoubleSide, depthTest: false, depthWrite: false, ...alpha ? { alphaMap: alpha, transparent: true } : {} }));
|
|
5158
|
+
const frame = new THREE.Mesh(plane, new THREE.MeshBasicMaterial({ color: 723982, side: THREE.DoubleSide, depthTest: false, depthWrite: false, ...alpha ? { alphaMap: alpha, transparent: true } : {} }));
|
|
5159
|
+
quad.renderOrder = 1;
|
|
5160
|
+
frame.visible = quad.visible = false;
|
|
5161
|
+
overlay.add(frame, quad);
|
|
5162
|
+
const entry = { view, target, quad, frame, alpha };
|
|
5163
|
+
entries.push(entry);
|
|
5164
|
+
return () => take(entry);
|
|
5165
|
+
},
|
|
5166
|
+
setMain(camera) {
|
|
5167
|
+
mainCamera = camera;
|
|
5168
|
+
},
|
|
5169
|
+
main(fallback) {
|
|
5170
|
+
if (!mainCamera) return fallback;
|
|
5171
|
+
const persp = mainCamera;
|
|
5172
|
+
const back = fallback;
|
|
5173
|
+
if (persp.isPerspectiveCamera && back.isPerspectiveCamera && persp.aspect !== back.aspect) {
|
|
5174
|
+
persp.aspect = back.aspect;
|
|
5175
|
+
persp.updateProjectionMatrix();
|
|
5176
|
+
}
|
|
5177
|
+
return mainCamera;
|
|
5178
|
+
},
|
|
5179
|
+
render(scene) {
|
|
5180
|
+
if (!entries.length) return;
|
|
5181
|
+
const { w, h } = size();
|
|
5182
|
+
const autoClear = gl.autoClear;
|
|
5183
|
+
const shadows = gl.shadowMap.autoUpdate;
|
|
5184
|
+
gl.shadowMap.autoUpdate = false;
|
|
5185
|
+
for (const entry of entries) {
|
|
5186
|
+
const cam = entry.view.camera;
|
|
5187
|
+
const aspect = w * entry.view.rect.width / Math.max(1, h * entry.view.rect.height);
|
|
5188
|
+
if (cam.isPerspectiveCamera && Math.abs(cam.aspect - aspect) > 1e-3) {
|
|
5189
|
+
cam.aspect = aspect;
|
|
5190
|
+
cam.updateProjectionMatrix();
|
|
5191
|
+
}
|
|
5192
|
+
gl.setRenderTarget(entry.target);
|
|
5193
|
+
gl.clear();
|
|
5194
|
+
gl.render(scene, cam);
|
|
5195
|
+
}
|
|
5196
|
+
gl.setRenderTarget(null);
|
|
5197
|
+
gl.autoClear = false;
|
|
5198
|
+
gl.setScissorTest(true);
|
|
5199
|
+
for (const entry of entries) {
|
|
5200
|
+
const r = entry.view.rect;
|
|
5201
|
+
const x = r.x * w;
|
|
5202
|
+
const y = (1 - r.y - r.height) * h;
|
|
5203
|
+
const vw = r.width * w;
|
|
5204
|
+
const vh = r.height * h;
|
|
5205
|
+
for (const mesh of [entry.frame, entry.quad]) mesh.visible = false;
|
|
5206
|
+
entry.frame.visible = true;
|
|
5207
|
+
gl.setViewport(x - FRAME_PX, y - FRAME_PX, vw + 2 * FRAME_PX, vh + 2 * FRAME_PX);
|
|
5208
|
+
gl.setScissor(x - FRAME_PX, y - FRAME_PX, vw + 2 * FRAME_PX, vh + 2 * FRAME_PX);
|
|
5209
|
+
gl.render(overlay, ortho);
|
|
5210
|
+
entry.frame.visible = false;
|
|
5211
|
+
entry.quad.visible = true;
|
|
5212
|
+
entry.quad.scale.x = entry.view.mirror ? -1 : 1;
|
|
5213
|
+
gl.setViewport(x, y, vw, vh);
|
|
5214
|
+
gl.setScissor(x, y, vw, vh);
|
|
5215
|
+
gl.render(overlay, ortho);
|
|
5216
|
+
entry.quad.visible = false;
|
|
5217
|
+
}
|
|
5218
|
+
gl.setScissorTest(false);
|
|
5219
|
+
gl.setViewport(0, 0, w, h);
|
|
5220
|
+
gl.autoClear = autoClear;
|
|
5221
|
+
gl.shadowMap.autoUpdate = shadows;
|
|
5222
|
+
},
|
|
5223
|
+
get count() {
|
|
5224
|
+
return entries.length;
|
|
5225
|
+
},
|
|
5226
|
+
dispose() {
|
|
5227
|
+
for (const entry of [...entries]) take(entry);
|
|
5228
|
+
plane.dispose();
|
|
5229
|
+
mainCamera = null;
|
|
5230
|
+
}
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
|
|
4734
5234
|
// src/room.ts
|
|
4735
5235
|
var FIXED_DT = 1 / 60;
|
|
5236
|
+
var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
|
|
5237
|
+
var DEFAULT_PARTICLE_LIMIT = 900;
|
|
5238
|
+
var WARMED_FOG_STATES = ["none", "linear", "exp2"];
|
|
5239
|
+
var ROOM_LIGHT_SIZES = { point: 16, spot: 4, pointShadows: 0, hemisphere: 1 };
|
|
4736
5240
|
var SOLID_SPAN = 3;
|
|
5241
|
+
var BATCH_MIN_MESHES = 8;
|
|
5242
|
+
var BATCH_BUDGET_MS = 1;
|
|
5243
|
+
var UNBATCHED_OWNERS = /vehicles|characters|animals|crowd/;
|
|
4737
5244
|
var STEP_UP = 0.45;
|
|
4738
5245
|
var BODY_HEIGHT2 = 1.75;
|
|
4739
5246
|
var KICK_MASS = 6;
|
|
@@ -4752,6 +5259,12 @@ var CAP_RISE = 0.03;
|
|
|
4752
5259
|
var MAX_STRIDE_SPEED = 10;
|
|
4753
5260
|
var v3 = (v) => [v.x, v.y, v.z];
|
|
4754
5261
|
var dist3 = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]);
|
|
5262
|
+
var vehicleLeafKey = (spec, part, moment) => {
|
|
5263
|
+
const tags = spec.parts.find((p) => p.id === part)?.tags ?? [];
|
|
5264
|
+
const leaf = tags.includes("hood") ? "hood" : tags.includes("lid") || tags.includes("trunk") ? "trunk" : "door";
|
|
5265
|
+
return `vehicle_${leaf}_${moment}`;
|
|
5266
|
+
};
|
|
5267
|
+
var MARK_EVENTS = /* @__PURE__ */ new Set(["dent", "hole", "crack", "chip", "stage", "fracture"]);
|
|
4755
5268
|
function createTestRoom(options) {
|
|
4756
5269
|
const THREE = options.THREE;
|
|
4757
5270
|
const locale = options.locale ?? "en";
|
|
@@ -4798,9 +5311,21 @@ function createTestRoom(options) {
|
|
|
4798
5311
|
};
|
|
4799
5312
|
const onResize = () => resize();
|
|
4800
5313
|
window.addEventListener("resize", onResize);
|
|
5314
|
+
const moduleViews = createModuleViews(THREE, gl);
|
|
4801
5315
|
const arenaSize = options.arenaSize ?? 14;
|
|
4802
5316
|
const arena = createArena(THREE, arenaSize, scene, options.scenery, options.tap);
|
|
4803
5317
|
scene.add(arena.group);
|
|
5318
|
+
const graphics = ownRenderer && options.graphics ? createRoomGraphics({
|
|
5319
|
+
gl,
|
|
5320
|
+
camera,
|
|
5321
|
+
sun: arena.sun,
|
|
5322
|
+
container,
|
|
5323
|
+
locale,
|
|
5324
|
+
loading,
|
|
5325
|
+
label: t("loading.graphics"),
|
|
5326
|
+
ui: options.ui !== false,
|
|
5327
|
+
options: options.graphics === true ? {} : options.graphics
|
|
5328
|
+
}) : null;
|
|
4804
5329
|
const ownWorld = !options.world;
|
|
4805
5330
|
const world = options.world ?? createDestruction({ seed: 1, ...options.worldOptions, materials: [...options.worldOptions?.materials ?? []] });
|
|
4806
5331
|
const pristineWorld = ownWorld ? world.serialize() : null;
|
|
@@ -4846,12 +5371,30 @@ function createTestRoom(options) {
|
|
|
4846
5371
|
const ownAudio = options.audio === void 0;
|
|
4847
5372
|
const audio = options.audio === false ? null : options.audio ?? createAudioEngine({ deferredStart: {} });
|
|
4848
5373
|
const destructionSound = audio ? createDestructionSound(audio) : null;
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
5374
|
+
if (audio && options.samplesBaseUrl !== null) {
|
|
5375
|
+
void loadKitSamples(audio, { baseUrl: options.samplesBaseUrl ?? "./sfx/" }).then((bank) => {
|
|
5376
|
+
if (!bank || typeof window === "undefined") return;
|
|
5377
|
+
const preload = () => {
|
|
5378
|
+
window.removeEventListener("keydown", preload, true);
|
|
5379
|
+
window.removeEventListener("pointerdown", preload, true);
|
|
5380
|
+
void audio.loadSamples(["latch", "creak_wood", "shut_wood", "rattle", "grab_hard", "grab_soft"]).catch(() => void 0);
|
|
5381
|
+
};
|
|
5382
|
+
window.addEventListener("keydown", preload, true);
|
|
5383
|
+
window.addEventListener("pointerdown", preload, true);
|
|
5384
|
+
});
|
|
5385
|
+
}
|
|
5386
|
+
const bodySounds = createBodySounds(audio);
|
|
5387
|
+
const playSound = (key2, point, volume, detail) => {
|
|
5388
|
+
destructionSound?.onSound(key2, point, volume, detail);
|
|
5389
|
+
if (detail) options.onSound?.(key2, point, volume, detail);
|
|
5390
|
+
else options.onSound?.(key2, point, volume);
|
|
5391
|
+
};
|
|
5392
|
+
const lights = createLightBudget(THREE, scene, { ...ROOM_LIGHT_SIZES, ...options.lightSizes });
|
|
5393
|
+
const warmup = createProgramWarmup(THREE);
|
|
5394
|
+
const pools = { geometries: createGeometryPool(), materials: createMaterialPool({ anchors: warmup.anchors, renderer: gl }) };
|
|
4853
5395
|
const vfx = createVfxRenderer(THREE, {
|
|
4854
5396
|
scene,
|
|
5397
|
+
lights,
|
|
4855
5398
|
camera,
|
|
4856
5399
|
overlay: container,
|
|
4857
5400
|
quality,
|
|
@@ -4914,7 +5457,7 @@ function createTestRoom(options) {
|
|
|
4914
5457
|
const size = Math.min(GROUND_BURST_MAX, Math.ceil(result.radius * GROUND_BURST_PER_METRE));
|
|
4915
5458
|
renderer.emit(material.debris === "clods" ? "dust" : material.debris, result.point, [0, 1, 0], Math.max(2, size), Math.min(4, 1 + result.radius * 4));
|
|
4916
5459
|
if (result.radius > GROUND_DUST_RADIUS) renderer.emit("dust", result.point, [0, 1, 0], size, 1.4);
|
|
4917
|
-
playSound(impactKey(def), result.point, Math.min(1, 0.25 + result.radius));
|
|
5460
|
+
playSound(impactKey(def), result.point, Math.min(1, 0.25 + result.radius), impactSoundKeys(material.id, def));
|
|
4918
5461
|
};
|
|
4919
5462
|
terrain.onDeform((region) => renderer.wakeRegion(region.x, region.z, region.radius));
|
|
4920
5463
|
const groundImpl = createTerrainImpl(terrain, { id: GROUND_ID, name: t("ground.name"), onBlow: showGroundBlow });
|
|
@@ -5005,7 +5548,7 @@ function createTestRoom(options) {
|
|
|
5005
5548
|
});
|
|
5006
5549
|
let pending = [];
|
|
5007
5550
|
const eventListeners = /* @__PURE__ */ new Set();
|
|
5008
|
-
const listeners = { spawn: /* @__PURE__ */ new Set(), tool: /* @__PURE__ */ new Set(), grab: /* @__PURE__ */ new Set(), throw: /* @__PURE__ */ new Set(), event: eventListeners, camera: /* @__PURE__ */ new Set() };
|
|
5551
|
+
const listeners = { spawn: /* @__PURE__ */ new Set(), tool: /* @__PURE__ */ new Set(), grab: /* @__PURE__ */ new Set(), throw: /* @__PURE__ */ new Set(), event: eventListeners, camera: /* @__PURE__ */ new Set(), footfall: /* @__PURE__ */ new Set() };
|
|
5009
5552
|
const fire = (type, data) => {
|
|
5010
5553
|
for (const fn of listeners[type] ?? []) fn(data);
|
|
5011
5554
|
};
|
|
@@ -5142,6 +5685,15 @@ function createTestRoom(options) {
|
|
|
5142
5685
|
floorAt: terrain.heightAt,
|
|
5143
5686
|
start: arena.playerStart,
|
|
5144
5687
|
crouchKeys: options.crouch?.keys,
|
|
5688
|
+
onSlam: (event) => slamPlayer(event),
|
|
5689
|
+
onFeet: (event) => {
|
|
5690
|
+
const at = player.position;
|
|
5691
|
+
if (event.type === "jump") bodySounds.jump(at);
|
|
5692
|
+
else {
|
|
5693
|
+
bodySounds.land(surfaceUnderFeet(), event.speed, at);
|
|
5694
|
+
action("landing_player", t("act.player_land", { speed: num(event.speed, 1) }), { data: { speedMs: Math.round(event.speed * 100) / 100, surface: surfaceUnderFeet() } });
|
|
5695
|
+
}
|
|
5696
|
+
},
|
|
5145
5697
|
hooks: {
|
|
5146
5698
|
primaryDown: () => {
|
|
5147
5699
|
useHeld = true;
|
|
@@ -5213,7 +5765,11 @@ function createTestRoom(options) {
|
|
|
5213
5765
|
return piece;
|
|
5214
5766
|
},
|
|
5215
5767
|
dragSound: (objectId, point, volume) => playSound(dragSoundKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, volume),
|
|
5216
|
-
hitSound: (objectId, point) =>
|
|
5768
|
+
hitSound: (objectId, point) => {
|
|
5769
|
+
const material = heaviestMaterial(objectId) ?? "";
|
|
5770
|
+
const def = world.materials.get(material);
|
|
5771
|
+
playSound(impactKey(def), point, 0.5, impactSoundKeys(material, def, "blunt"));
|
|
5772
|
+
},
|
|
5217
5773
|
onEvent: (ev) => {
|
|
5218
5774
|
const vars = { name: ev.name, kg: num(ev.massKg, 0) };
|
|
5219
5775
|
action(`push_${ev.type}`, t(`act.push_${ev.type}`, vars), { objectId: ev.objectId, itemId: entryOf(ev.objectId)?.so.itemId, data: { massKg: Math.round(ev.massKg * 10) / 10 } });
|
|
@@ -5303,7 +5859,13 @@ function createTestRoom(options) {
|
|
|
5303
5859
|
isLocked: (objectId, part) => lockedLeaves.has(`${objectId}\0*`) || lockedLeaves.has(`${objectId}\0${part}`),
|
|
5304
5860
|
nameOf,
|
|
5305
5861
|
impactKey: (objectId, part) => impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")),
|
|
5862
|
+
leafSound: (objectId, part, moment) => {
|
|
5863
|
+
const spec = specs.get(objectId);
|
|
5864
|
+
return spec?.tags?.includes("vehicle") ? vehicleLeafKey(spec, part, moment) : void 0;
|
|
5865
|
+
},
|
|
5306
5866
|
sound: playSound,
|
|
5867
|
+
// a vehicle's doors, hood and lid have their own sounds in the vehicle bank (game-vehicles-three), not those of the house
|
|
5868
|
+
leafFoley: (objectId, part, slides2) => specs.get(objectId)?.tags?.includes("vehicle") ? null : leafFoleyOf(entryOf(objectId)?.item?.sounds, part, impactKey(world.materials.get(world.part(objectId, part)?.material ?? heaviestMaterial(objectId) ?? "")), slides2),
|
|
5307
5869
|
onEvent: (ev) => {
|
|
5308
5870
|
const vars = { name: ev.name, percent: Math.round(ev.opening * 100) };
|
|
5309
5871
|
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 } : {} } });
|
|
@@ -5318,12 +5880,23 @@ function createTestRoom(options) {
|
|
|
5318
5880
|
for (const p of st?.parts ?? []) if (p.status !== "broken" && p.status !== "melted" && p.status !== "ashes") m += p.mass;
|
|
5319
5881
|
return m;
|
|
5320
5882
|
};
|
|
5883
|
+
const batchers = /* @__PURE__ */ new Map();
|
|
5884
|
+
const batchObject = (objectId, root) => {
|
|
5885
|
+
let meshes = 0;
|
|
5886
|
+
let unbatched = false;
|
|
5887
|
+
root.traverse((object) => {
|
|
5888
|
+
if (object.isMesh) meshes++;
|
|
5889
|
+
if (UNBATCHED_OWNERS.test(object.userData.kitOrigin?.owner ?? "")) unbatched = true;
|
|
5890
|
+
});
|
|
5891
|
+
if (!unbatched && meshes >= BATCH_MIN_MESHES) batchers.set(objectId, createStaticBatcher(THREE, root, { budgetMs: BATCH_BUDGET_MS }));
|
|
5892
|
+
};
|
|
5321
5893
|
const registerEntry = (objectId, itemId, root, spec, name, parts, item, arenaObject, movable) => {
|
|
5322
5894
|
specs.set(objectId, spec);
|
|
5323
5895
|
renderer.bind(objectId, root, parts ? { parts } : void 0);
|
|
5324
5896
|
const so = { objectId, itemId, root, movable };
|
|
5325
5897
|
const e = { so, item, name, arena: arenaObject, fluidOffset: [0, 0, 0], uses: [], piece: null, last: [root.position.x, root.position.y, root.position.z], velocity: [0, 0, 0] };
|
|
5326
5898
|
entries.set(objectId, e);
|
|
5899
|
+
batchObject(objectId, root);
|
|
5327
5900
|
if (movable) {
|
|
5328
5901
|
e.piece = renderer.makePiece(objectId, { mass: Math.max(0.2, massOfObject(objectId)) });
|
|
5329
5902
|
rendererPieces.set(objectId, e.piece);
|
|
@@ -5591,6 +6164,8 @@ function createTestRoom(options) {
|
|
|
5591
6164
|
dropTree(e.so.root);
|
|
5592
6165
|
dropTree(inner);
|
|
5593
6166
|
entries.delete(id);
|
|
6167
|
+
batchers.get(id)?.dispose();
|
|
6168
|
+
batchers.delete(id);
|
|
5594
6169
|
doors.forget(id);
|
|
5595
6170
|
rendererPieces.delete(id);
|
|
5596
6171
|
specs.delete(id);
|
|
@@ -5689,7 +6264,7 @@ function createTestRoom(options) {
|
|
|
5689
6264
|
lastImpact.clear();
|
|
5690
6265
|
addArena();
|
|
5691
6266
|
standFootprints();
|
|
5692
|
-
timeScale = 1;
|
|
6267
|
+
scheduler.timeScale = 1;
|
|
5693
6268
|
fastForward = false;
|
|
5694
6269
|
walkPlan = null;
|
|
5695
6270
|
};
|
|
@@ -5699,6 +6274,37 @@ function createTestRoom(options) {
|
|
|
5699
6274
|
let avatar = null;
|
|
5700
6275
|
let cameraMode = options.cameraMode ?? "first";
|
|
5701
6276
|
let crouchForced = false;
|
|
6277
|
+
let downForced = false;
|
|
6278
|
+
const playerMass = options.playerMass ?? bodyMass(1.75);
|
|
6279
|
+
const SLAM_REACH = PLAYER_RADIUS + 0.35;
|
|
6280
|
+
const SLAM_SHOULDER = 1.3;
|
|
6281
|
+
const slamPlayer = (event) => {
|
|
6282
|
+
const [x, feet, z] = player.position;
|
|
6283
|
+
const energy = 0.5 * playerMass * event.speed * event.speed;
|
|
6284
|
+
let hit = null;
|
|
6285
|
+
if (event.against === "obstacle") {
|
|
6286
|
+
const from = [x, feet + ((player.knocked?.lying ?? 0) > 0.5 ? 0.3 : SLAM_SHOULDER), z];
|
|
6287
|
+
hit = rayWorld(from, event.direction, SLAM_REACH);
|
|
6288
|
+
if (hit) world.hitPart(hit.object, hit.part, { type: "blunt", point: hit.point, direction: event.direction, energy, radius: 0.2, ...hit.normal ? { normal: hit.normal } : {} });
|
|
6289
|
+
}
|
|
6290
|
+
bodySounds.slam(event.speed, [x, feet + 0.5, z]);
|
|
6291
|
+
action("player_slammed", t(event.against === "obstacle" ? "act.player_slammed" : "act.player_fell", { speed: num(event.speed, 1), what: hit ? nameOf(hit.object) : "" }), {
|
|
6292
|
+
...hit ? { objectId: hit.object, part: hit.part } : {},
|
|
6293
|
+
data: { against: event.against, speed: event.speed, energy, ...hit ? { object: hit.object, part: hit.part } : {} }
|
|
6294
|
+
});
|
|
6295
|
+
};
|
|
6296
|
+
const knockPlayer = (event) => {
|
|
6297
|
+
if (seating.seat) return;
|
|
6298
|
+
const feet = [player.x, player.feet, player.z];
|
|
6299
|
+
const reach = world.blastReach(event.point, [feet[0], feet[1] + KNOCK_CHEST, feet[2]], event.source ? { ignore: [event.source] } : {});
|
|
6300
|
+
const knock = knockOf({ point: event.point, energy: event.energy, radius: event.radius, reach }, feet, playerMass, options.blastShove);
|
|
6301
|
+
if (!knock) return;
|
|
6302
|
+
player.knock(knock);
|
|
6303
|
+
const speed = Math.hypot(knock.velocity[0], knock.velocity[1], knock.velocity[2]);
|
|
6304
|
+
action(knock.down ? "player_knocked_down" : "player_shoved", t(knock.down ? "act.knocked_down" : "act.player_shoved", { speed: num(speed, 1), reach: num(reach * 100, 0) }), {
|
|
6305
|
+
data: { speed, reach, down: knock.down, source: event.source }
|
|
6306
|
+
});
|
|
6307
|
+
};
|
|
5702
6308
|
const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
|
|
5703
6309
|
let feetPrev = null;
|
|
5704
6310
|
let armedForced = null;
|
|
@@ -5754,6 +6360,7 @@ function createTestRoom(options) {
|
|
|
5754
6360
|
feetPrev = [x, z];
|
|
5755
6361
|
avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
|
|
5756
6362
|
avatarState.crouch = crouchForced || player.crouching || push.isBent;
|
|
6363
|
+
avatarState.down = downForced || Boolean(player.knocked?.down || (player.knocked?.lying ?? 0) > 0.5);
|
|
5757
6364
|
avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
|
|
5758
6365
|
avatarState.pitch = player.pitch;
|
|
5759
6366
|
avatarState.holding = heldPiece !== null;
|
|
@@ -5826,14 +6433,18 @@ function createTestRoom(options) {
|
|
|
5826
6433
|
if (!avatar?.reach) return;
|
|
5827
6434
|
avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
|
|
5828
6435
|
};
|
|
6436
|
+
const thirdEuler = new THREE.Euler(0, 0, 0, "YXZ");
|
|
5829
6437
|
const applyThirdView = () => {
|
|
5830
6438
|
if (!thirdActive()) return () => void 0;
|
|
5831
6439
|
const home = camera.position.clone();
|
|
6440
|
+
const turn = camera.quaternion.clone();
|
|
5832
6441
|
const pose = thirdPose(0);
|
|
5833
6442
|
camera.position.set(pose.position[0], pose.position[1], pose.position[2]);
|
|
6443
|
+
if (player.knocked) camera.quaternion.setFromEuler(thirdEuler.set(player.pitch, player.yaw, 0, "YXZ"));
|
|
5834
6444
|
camera.updateMatrixWorld(true);
|
|
5835
6445
|
return () => {
|
|
5836
6446
|
camera.position.copy(home);
|
|
6447
|
+
camera.quaternion.copy(turn);
|
|
5837
6448
|
camera.updateMatrixWorld(true);
|
|
5838
6449
|
};
|
|
5839
6450
|
};
|
|
@@ -5890,6 +6501,28 @@ function createTestRoom(options) {
|
|
|
5890
6501
|
}
|
|
5891
6502
|
};
|
|
5892
6503
|
const rayWorld = (o, d, max) => withoutHeld(() => world.raycast(o, d, max));
|
|
6504
|
+
const surfaceUnderFeet = () => {
|
|
6505
|
+
const [x, feet, z] = player.position;
|
|
6506
|
+
const under = rayWorld([x, feet + 0.15, z], [0, -1, 0], 0.5);
|
|
6507
|
+
if (under && under.object !== GROUND_ID) {
|
|
6508
|
+
const material = world.state(under.object)?.parts.find((p) => p.id === under.part)?.material;
|
|
6509
|
+
if (material) return surfaceOfMaterial(material);
|
|
6510
|
+
}
|
|
6511
|
+
return surfaceOfMaterial(terrain.columnAt(x, z).material);
|
|
6512
|
+
};
|
|
6513
|
+
const strides = createStrideCounter();
|
|
6514
|
+
let feetAt = null;
|
|
6515
|
+
const stepFeet = (dt) => {
|
|
6516
|
+
const [x, , z] = player.position;
|
|
6517
|
+
const moved = feetAt && dt > 0 ? Math.hypot(x - feetAt[0], z - feetAt[1]) : 0;
|
|
6518
|
+
feetAt = [x, z];
|
|
6519
|
+
if (dt <= 0 || seating.seat) return;
|
|
6520
|
+
const gait = player.running ? "run" : "walk";
|
|
6521
|
+
const foot = avatar?.footfall ? avatar.footfall() : moved / dt > MAX_STRIDE_SPEED ? null : strides.advance(moved, gait);
|
|
6522
|
+
if (!foot || !player.grounded) return;
|
|
6523
|
+
emitHost({ type: "footfall", foot, surface: surfaceUnderFeet(), gait, position: player.position });
|
|
6524
|
+
fire("footfall", { foot, gait, time: simTime });
|
|
6525
|
+
};
|
|
5893
6526
|
let rng = 12345;
|
|
5894
6527
|
const rnd = () => {
|
|
5895
6528
|
rng = rng * 1664525 + 1013904223 >>> 0;
|
|
@@ -6008,6 +6641,12 @@ function createTestRoom(options) {
|
|
|
6008
6641
|
ui?.toast(msg);
|
|
6009
6642
|
};
|
|
6010
6643
|
const toolName = (id = toolId) => arsenal.name(id);
|
|
6644
|
+
const toolDef = (id) => arsenal.weapons.get(id);
|
|
6645
|
+
const blowTarget = (r) => {
|
|
6646
|
+
const part = world.state(r.object)?.parts.find((p) => p.id === r.part);
|
|
6647
|
+
if (!part || !(world.materials.get(part.material)?.tags ?? []).includes("body")) return "object";
|
|
6648
|
+
return /head|face|jaw|skull|neck/.test(r.part) ? "head" : "body";
|
|
6649
|
+
};
|
|
6011
6650
|
const hitWhere = (r) => {
|
|
6012
6651
|
const p = world.part(r.object, r.part);
|
|
6013
6652
|
return `${nameOf(r.object)} \u203A ${r.part}${p ? ` (${dStrings.materialNames[p.material] ?? p.material})` : ""}`;
|
|
@@ -6017,6 +6656,7 @@ function createTestRoom(options) {
|
|
|
6017
6656
|
renderer.impulse(objectId, [dir[0] * momentum, dir[1] * momentum, dir[2] * momentum], point);
|
|
6018
6657
|
};
|
|
6019
6658
|
const shotRay = new THREE.Raycaster();
|
|
6659
|
+
shotRay.layers.enableAll();
|
|
6020
6660
|
const LIQUID_SLOWING = 0.25;
|
|
6021
6661
|
const SHOT_GRIP = 0.3;
|
|
6022
6662
|
const shotPush = (s, objectId, results, fallback) => {
|
|
@@ -6186,7 +6826,15 @@ function createTestRoom(options) {
|
|
|
6186
6826
|
action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
|
|
6187
6827
|
return;
|
|
6188
6828
|
}
|
|
6189
|
-
|
|
6829
|
+
const hit = useToHit(u, r.point, r.normal);
|
|
6830
|
+
const result = world.hitPart(r.object, r.part, { ...hit, source: "player" });
|
|
6831
|
+
if (!result.events.some((e) => MARK_EVENTS.has(e.type))) {
|
|
6832
|
+
const material = world.part(r.object, r.part)?.material ?? heaviestMaterial(r.object) ?? "";
|
|
6833
|
+
const def = world.materials.get(material);
|
|
6834
|
+
playSound(impactKey(def), r.point, Math.min(0.8, 0.25 + u.energy / 400), impactSoundKeys(material, def, hit.type));
|
|
6835
|
+
}
|
|
6836
|
+
const blow = blowOfTool(id, toolDef(id)?.category ?? "");
|
|
6837
|
+
if (blow) bodySounds.hit(blow, blowTarget(r), r.point, u.energy);
|
|
6190
6838
|
pushPiece(r.object, u.direction, 2 * u.energy / 6, r.point);
|
|
6191
6839
|
action("melee", t("act.melee.hit", { weapon: w, energy: num(u.energy, 0), where: hitWhere(r), dist: num(r.distance, 1) }), {
|
|
6192
6840
|
objectId: r.object,
|
|
@@ -6227,6 +6875,10 @@ function createTestRoom(options) {
|
|
|
6227
6875
|
comboNext = (stepIndex + 1) % combo.steps.length;
|
|
6228
6876
|
comboAt = simTime;
|
|
6229
6877
|
}
|
|
6878
|
+
const blow = blowOfTool(id, toolDef(id)?.category ?? "");
|
|
6879
|
+
const lead = blow ? swingLead(blow) : 0;
|
|
6880
|
+
const swing = blow ? () => bodySounds.swing(blow, player.position) : void 0;
|
|
6881
|
+
if (swing && (seconds === null || seconds <= lead)) swing();
|
|
6230
6882
|
if (clip === void 0 || seconds === null) {
|
|
6231
6883
|
if (leg) {
|
|
6232
6884
|
legAim();
|
|
@@ -6241,6 +6893,7 @@ function createTestRoom(options) {
|
|
|
6241
6893
|
strikes.push({
|
|
6242
6894
|
key: id,
|
|
6243
6895
|
remaining: seconds,
|
|
6896
|
+
...swing && seconds > lead ? { swing, lead } : {},
|
|
6244
6897
|
land: () => {
|
|
6245
6898
|
if (leg) legAim();
|
|
6246
6899
|
else aim();
|
|
@@ -6250,7 +6903,13 @@ function createTestRoom(options) {
|
|
|
6250
6903
|
};
|
|
6251
6904
|
const stepStrikes = (dt) => {
|
|
6252
6905
|
if (strikes.length === 0) return;
|
|
6253
|
-
for (const strike of strikes)
|
|
6906
|
+
for (const strike of strikes) {
|
|
6907
|
+
strike.remaining -= dt;
|
|
6908
|
+
if (strike.swing && strike.remaining <= (strike.lead ?? 0)) {
|
|
6909
|
+
strike.swing();
|
|
6910
|
+
delete strike.swing;
|
|
6911
|
+
}
|
|
6912
|
+
}
|
|
6254
6913
|
const landed = strikes.filter((strike) => strike.remaining <= 0);
|
|
6255
6914
|
strikes = strikes.filter((strike) => strike.remaining > 0);
|
|
6256
6915
|
for (const strike of landed) strike.land();
|
|
@@ -6364,26 +7023,27 @@ function createTestRoom(options) {
|
|
|
6364
7023
|
aim();
|
|
6365
7024
|
const cat = tool.def.category;
|
|
6366
7025
|
if (cat === "hand") {
|
|
6367
|
-
if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return;
|
|
7026
|
+
if (strikes.some((strike) => strike.key === "hand" || strike.key === "fists")) return "cooldown";
|
|
6368
7027
|
strikeUse({ kind: "melee", damage: "blunt", energy: 25, radius: 0.1, reach: 0.8, origin: [aimO[0], aimO[1], aimO[2]], direction: [aimD[0], aimD[1], aimD[2]] }, "hand");
|
|
6369
|
-
return;
|
|
7028
|
+
return "ok";
|
|
6370
7029
|
}
|
|
6371
|
-
if (strikes.some((strike) => strike.key === toolId)) return;
|
|
7030
|
+
if (strikes.some((strike) => strike.key === toolId)) return "cooldown";
|
|
6372
7031
|
const res = tool.trigger([aimO[0], aimO[1], aimO[2]], [aimD[0], aimD[1], aimD[2]], dt, held, "player");
|
|
6373
7032
|
flushEvents(toolId, tool, res.recoil);
|
|
6374
7033
|
if (res.status === "empty") {
|
|
6375
7034
|
if (!tool.reloading && tool.reserve > 0) startReload();
|
|
6376
7035
|
say(t(tool.reloading ? "hud.reloading" : "hud.empty"));
|
|
6377
|
-
return;
|
|
7036
|
+
return res.status;
|
|
6378
7037
|
}
|
|
6379
7038
|
if (res.status !== "ok") {
|
|
6380
7039
|
if (res.status === "reloading") say(t("hud.reloading"));
|
|
6381
7040
|
else if (res.status === "no_fuel") say(t("hud.no_fuel"));
|
|
6382
7041
|
else if (res.status === "fault" && res.fault) say(t("hud.fault", { fault: t(`fault.${res.fault}`) }));
|
|
6383
7042
|
else if (res.status === "safe") say(t("hud.safe"));
|
|
6384
|
-
return;
|
|
7043
|
+
return res.status;
|
|
6385
7044
|
}
|
|
6386
7045
|
applyUses(res, dt);
|
|
7046
|
+
return res.status;
|
|
6387
7047
|
};
|
|
6388
7048
|
const applyUses = (res, dt) => {
|
|
6389
7049
|
if (res.shots.length) fireShots(res.shots, res.recoil);
|
|
@@ -6485,8 +7145,9 @@ function createTestRoom(options) {
|
|
|
6485
7145
|
heldTilt = 0;
|
|
6486
7146
|
};
|
|
6487
7147
|
const handUse = () => {
|
|
6488
|
-
if (heldPiece)
|
|
6489
|
-
|
|
7148
|
+
if (!heldPiece) return useTool(FIXED_DT, false);
|
|
7149
|
+
grab();
|
|
7150
|
+
return "ok";
|
|
6490
7151
|
};
|
|
6491
7152
|
const grab = () => {
|
|
6492
7153
|
if (push.active) return false;
|
|
@@ -6529,7 +7190,9 @@ function createTestRoom(options) {
|
|
|
6529
7190
|
heldYaw = 0;
|
|
6530
7191
|
heldTilt = 0;
|
|
6531
7192
|
heldVel[0] = heldVel[1] = heldVel[2] = 0;
|
|
7193
|
+
playSound(pickupSoundOf(heldEntry?.item?.sounds, world.materials.get(heaviestMaterial(r.object) ?? "")), r.point, Math.min(1, 0.45 + kg * 0.1));
|
|
6532
7194
|
action("grab", t("act.grab", { name: heldName, kg: num(kg, 1) }), { objectId: r.object, itemId: entryOf(r.object)?.so.itemId, data: { massKg: Math.round(kg * 10) / 10 } });
|
|
7195
|
+
playSound(impactKey(world.materials.get(heaviestMaterial(r.object) ?? "")).replace("impact_", "grab_"), v3(piece.root.getWorldPosition(tmpV)), 0.8);
|
|
6533
7196
|
fire("grab", { held: true, objectId: r.object });
|
|
6534
7197
|
return true;
|
|
6535
7198
|
};
|
|
@@ -6542,6 +7205,7 @@ function createTestRoom(options) {
|
|
|
6542
7205
|
releaseHeld();
|
|
6543
7206
|
piece.velocity = [0, 0, 0];
|
|
6544
7207
|
renderer.impulse(id, [aimD[0] * speed * piece.mass, (aimD[1] * speed + 1.2) * piece.mass, aimD[2] * speed * piece.mass]);
|
|
7208
|
+
playSound("item_throw", [piece.root.position.x, piece.root.position.y, piece.root.position.z], Math.min(1, 0.3 + speed * 0.04));
|
|
6545
7209
|
action("throw", t("act.throw", { name, speed: num(speed, 1) }), { objectId: id, itemId: entryOf(id)?.so.itemId, data: { speedMs: speed, massKg: piece.mass } });
|
|
6546
7210
|
fire("throw", { objectId: id, speed });
|
|
6547
7211
|
return true;
|
|
@@ -6653,6 +7317,7 @@ function createTestRoom(options) {
|
|
|
6653
7317
|
e.opened = true;
|
|
6654
7318
|
if (e.containerId) fluids.open(e.containerId);
|
|
6655
7319
|
seal.open?.(wrapped.get(e.so.root) ?? e.so.root);
|
|
7320
|
+
playSound(openSoundOf(e.item?.sounds, world.materials.get(heaviestMaterial(e.so.objectId) ?? "")), v3(e.so.root.getWorldPosition(tmpV)), 0.7);
|
|
6656
7321
|
const tool2 = opener.tool ? catalogMap.get(opener.tool)?.name ?? (arsenal.weapons.has(opener.tool) ? toolName(opener.tool) : opener.tool) : void 0;
|
|
6657
7322
|
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" } });
|
|
6658
7323
|
return true;
|
|
@@ -6701,17 +7366,28 @@ function createTestRoom(options) {
|
|
|
6701
7366
|
if (consuming && c.closed) fluids.open(e.containerId);
|
|
6702
7367
|
useItemPlayer = createItemUse(fluids, e.containerId, def);
|
|
6703
7368
|
useItemName = def.id;
|
|
7369
|
+
useSoundIn = 0;
|
|
6704
7370
|
if (eating) useLiters = c.liters;
|
|
6705
7371
|
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 } });
|
|
6706
7372
|
return true;
|
|
6707
7373
|
};
|
|
6708
7374
|
let useLiters = null;
|
|
7375
|
+
let useSoundIn = 0;
|
|
6709
7376
|
const updateItemUse = (dt) => {
|
|
6710
7377
|
if (!useItemPlayer) return;
|
|
6711
7378
|
const e = heldEntry;
|
|
6712
7379
|
const c = e?.containerId ? fluids.container(e.containerId) : void 0;
|
|
6713
7380
|
const before = c?.liters;
|
|
6714
7381
|
useItemPlayer.update(dt);
|
|
7382
|
+
const useSound = e && useLiters !== null ? useSoundOf(e.item?.sounds, useItemName) : void 0;
|
|
7383
|
+
if (useSound && e) {
|
|
7384
|
+
useSoundIn -= dt;
|
|
7385
|
+
const now = e.containerId ? fluids.container(e.containerId)?.liters : void 0;
|
|
7386
|
+
if (useSoundIn <= 0 && before !== void 0 && now !== void 0 && now < before) {
|
|
7387
|
+
playSound(useSound, v3(e.so.root.getWorldPosition(tmpV)), 0.6);
|
|
7388
|
+
useSoundIn = USE_SOUND_EVERY;
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
6715
7391
|
const seal = e?.item?.seal;
|
|
6716
7392
|
twisting = null;
|
|
6717
7393
|
if (e && seal?.openUse === useItemName && !e.opened) {
|
|
@@ -6921,7 +7597,9 @@ function createTestRoom(options) {
|
|
|
6921
7597
|
if (!loadout.includes(weaponId)) loadout.push(weaponId);
|
|
6922
7598
|
setTool(weaponId);
|
|
6923
7599
|
};
|
|
7600
|
+
let hudAimHit;
|
|
6924
7601
|
const inspectTarget = () => {
|
|
7602
|
+
if (hudAimHit !== void 0) return hudAimHit;
|
|
6925
7603
|
aim();
|
|
6926
7604
|
return rayWorld(aimO, aimD, 14);
|
|
6927
7605
|
};
|
|
@@ -6999,13 +7677,64 @@ function createTestRoom(options) {
|
|
|
6999
7677
|
distance: r.distance
|
|
7000
7678
|
};
|
|
7001
7679
|
};
|
|
7002
|
-
let timeScale = 1;
|
|
7003
7680
|
let fastForward = false;
|
|
7004
7681
|
let hudAcc = 1;
|
|
7005
7682
|
let metricsAcc = 0;
|
|
7006
7683
|
let ownsLoop = false;
|
|
7684
|
+
let moduleFrameCount = 0;
|
|
7685
|
+
const headlessErrors = /* @__PURE__ */ new Map();
|
|
7686
|
+
const loggedErrors = /* @__PURE__ */ new Set();
|
|
7687
|
+
let headlessDepth = 0;
|
|
7688
|
+
const reportFrameError = (error, system) => {
|
|
7689
|
+
if (headlessDepth > 0) {
|
|
7690
|
+
if (!headlessErrors.has(system.id)) headlessErrors.set(system.id, error);
|
|
7691
|
+
return;
|
|
7692
|
+
}
|
|
7693
|
+
if (loggedErrors.has(system.id)) return;
|
|
7694
|
+
loggedErrors.add(system.id);
|
|
7695
|
+
console.error(`test-room: ${system.id} (${system.phase}) threw`, error);
|
|
7696
|
+
};
|
|
7697
|
+
const runHeadless = (run) => {
|
|
7698
|
+
headlessDepth++;
|
|
7699
|
+
try {
|
|
7700
|
+
run();
|
|
7701
|
+
} catch (error) {
|
|
7702
|
+
headlessErrors.clear();
|
|
7703
|
+
throw error;
|
|
7704
|
+
} finally {
|
|
7705
|
+
headlessDepth--;
|
|
7706
|
+
}
|
|
7707
|
+
if (headlessDepth > 0 || headlessErrors.size === 0) return;
|
|
7708
|
+
const errors = [...headlessErrors.values()];
|
|
7709
|
+
headlessErrors.clear();
|
|
7710
|
+
throw errors.length === 1 ? errors[0] : new AggregateError(errors, `${errors.length} systems threw in one frame`);
|
|
7711
|
+
};
|
|
7712
|
+
const scheduler = createScheduler({ maxFrameSeconds: 0.05, budgetMs: options.frameBudgetMs ?? 8, onError: reportFrameError });
|
|
7713
|
+
const audioQueue = audio?.deferredQueue?.();
|
|
7714
|
+
if (audioQueue) scheduler.budget.register(audioQueue, DEFERRED_PRIORITY[DEFERRED_QUEUE_ID.AUDIO_VOICES]);
|
|
7715
|
+
scheduler.budget.register(renderer.deferredQueue(), DEFERRED_PRIORITY[DEFERRED_QUEUE_ID.DESTRUCTION_EVENTS]);
|
|
7007
7716
|
const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
|
|
7008
7717
|
const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds, blast: () => lastBlast });
|
|
7718
|
+
const MEMORY_CLEARS = 4;
|
|
7719
|
+
const memoryWatch = createMemoryWatch({
|
|
7720
|
+
clears: MEMORY_CLEARS,
|
|
7721
|
+
read: () => {
|
|
7722
|
+
const memory = gl.info?.memory;
|
|
7723
|
+
const heap = heapMB();
|
|
7724
|
+
return memory ? { geometries: memory.geometries, textures: memory.textures, ...heap === void 0 ? {} : { heapMB: heap } } : void 0;
|
|
7725
|
+
}
|
|
7726
|
+
});
|
|
7727
|
+
clearListeners.add(() => {
|
|
7728
|
+
const verdict = memoryWatch.cleared(activeBench ?? void 0);
|
|
7729
|
+
if (!verdict) return;
|
|
7730
|
+
if (!verdict.leaking) {
|
|
7731
|
+
ui?.alert(null);
|
|
7732
|
+
return;
|
|
7733
|
+
}
|
|
7734
|
+
const text = t("memory.leak", { geometries: verdict.grew.geometries, textures: verdict.grew.textures, clears: MEMORY_CLEARS });
|
|
7735
|
+
log.add({ channel: "event", type: "memory_leak", text, data: { ...verdict.after, grew: verdict.grew } });
|
|
7736
|
+
ui?.alert(`\u26A0 ${text}`);
|
|
7737
|
+
});
|
|
7009
7738
|
let qualityName = quality;
|
|
7010
7739
|
const syncContainers = (sdt) => {
|
|
7011
7740
|
const drinkingHeld = useItemPlayer !== null && (useItemName === "drink" || useItemName === "sip" || useItemName === "chug");
|
|
@@ -7031,7 +7760,6 @@ function createTestRoom(options) {
|
|
|
7031
7760
|
if (part && part.liters !== void 0 && Math.abs(part.liters - c.liters) > 1e-4) world.setLiters(host2.objectId, e.fluidPart, c.liters);
|
|
7032
7761
|
}
|
|
7033
7762
|
};
|
|
7034
|
-
let paused = false;
|
|
7035
7763
|
let walkPlan = null;
|
|
7036
7764
|
const stepWalk = (dt) => {
|
|
7037
7765
|
if (!walkPlan) return;
|
|
@@ -7040,8 +7768,7 @@ function createTestRoom(options) {
|
|
|
7040
7768
|
walkPlan.remaining -= dt;
|
|
7041
7769
|
if (walkPlan.remaining <= 1e-9) walkPlan = null;
|
|
7042
7770
|
};
|
|
7043
|
-
const
|
|
7044
|
-
if (paused) return;
|
|
7771
|
+
const simulateBeforeModules = (dt) => {
|
|
7045
7772
|
simTime += dt;
|
|
7046
7773
|
stepWalk(dt);
|
|
7047
7774
|
stepStrikes(dt);
|
|
@@ -7057,6 +7784,7 @@ function createTestRoom(options) {
|
|
|
7057
7784
|
}
|
|
7058
7785
|
syncLiters();
|
|
7059
7786
|
renderer.update(dt);
|
|
7787
|
+
destructionSound?.update(dt);
|
|
7060
7788
|
for (const e of entries.values()) e.follower?.update?.();
|
|
7061
7789
|
terrainView.sync(camera.position);
|
|
7062
7790
|
showGroundFire(dt);
|
|
@@ -7073,7 +7801,8 @@ function createTestRoom(options) {
|
|
|
7073
7801
|
const look = camera.getWorldDirection(listenerDirection);
|
|
7074
7802
|
audio.setListener({ position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-look.x, -look.z) });
|
|
7075
7803
|
}
|
|
7076
|
-
|
|
7804
|
+
};
|
|
7805
|
+
const simulateAfterModules = (dt) => {
|
|
7077
7806
|
for (const tr of tracers) {
|
|
7078
7807
|
if (tr.life > 0) {
|
|
7079
7808
|
tr.life -= dt;
|
|
@@ -7082,7 +7811,7 @@ function createTestRoom(options) {
|
|
|
7082
7811
|
}
|
|
7083
7812
|
flush();
|
|
7084
7813
|
};
|
|
7085
|
-
const
|
|
7814
|
+
const updateInput = (dt, isPaused) => {
|
|
7086
7815
|
if (!ownsLoop) roomMetrics.frame(dt);
|
|
7087
7816
|
frameStamp++;
|
|
7088
7817
|
seating.update();
|
|
@@ -7091,31 +7820,33 @@ function createTestRoom(options) {
|
|
|
7091
7820
|
pushByKey = false;
|
|
7092
7821
|
push.end();
|
|
7093
7822
|
}
|
|
7094
|
-
push.step(
|
|
7095
|
-
doors.step(
|
|
7096
|
-
if (!
|
|
7097
|
-
if (!
|
|
7823
|
+
push.step(isPaused ? 0 : dt);
|
|
7824
|
+
doors.step(isPaused ? 0 : dt);
|
|
7825
|
+
if (!isPaused) kickLight(dt);
|
|
7826
|
+
if (!isPaused) pushByPlayer(dt);
|
|
7098
7827
|
else player.takeWish();
|
|
7099
|
-
stepAvatar(
|
|
7828
|
+
stepAvatar(isPaused ? 0 : dt);
|
|
7829
|
+
stepFeet(isPaused ? 0 : dt);
|
|
7100
7830
|
if (tiltKey || heldTilt > 0) {
|
|
7101
7831
|
heldTilt = Math.max(0, Math.min(2.4, heldTilt + (tiltKey ? 2.2 : -3) * dt));
|
|
7102
7832
|
}
|
|
7103
|
-
if ((useHeld || useFirst) && !
|
|
7833
|
+
if ((useHeld || useFirst) && !isPaused && !push.active) {
|
|
7104
7834
|
if (tool.def.category === "hand") {
|
|
7105
7835
|
if (useFirst) handUse();
|
|
7106
7836
|
} else if (useHeld) useTool(dt, !useFirst);
|
|
7107
7837
|
useFirst = false;
|
|
7108
7838
|
}
|
|
7109
|
-
if (!
|
|
7110
|
-
|
|
7839
|
+
if (!isPaused) followHeld(dt);
|
|
7840
|
+
};
|
|
7841
|
+
const updatePresent = (dt) => {
|
|
7111
7842
|
reachAvatar();
|
|
7112
7843
|
if (progress && simTime - progress.at > 0.25) progress = null;
|
|
7844
|
+
ui?.flushLog();
|
|
7113
7845
|
hudAcc += dt;
|
|
7114
7846
|
if (hudAcc >= 0.1 && ui) {
|
|
7115
7847
|
hudAcc = 0;
|
|
7116
7848
|
ui.hud(hudState());
|
|
7117
7849
|
}
|
|
7118
|
-
ui?.flushLog();
|
|
7119
7850
|
metricsAcc += dt;
|
|
7120
7851
|
if (metricsAcc >= METRICS_INTERVAL_S && ui) {
|
|
7121
7852
|
metricsAcc = 0;
|
|
@@ -7123,6 +7854,14 @@ function createTestRoom(options) {
|
|
|
7123
7854
|
}
|
|
7124
7855
|
};
|
|
7125
7856
|
const hudState = () => {
|
|
7857
|
+
hudAimHit = inspectTarget();
|
|
7858
|
+
try {
|
|
7859
|
+
return readHudState();
|
|
7860
|
+
} finally {
|
|
7861
|
+
hudAimHit = void 0;
|
|
7862
|
+
}
|
|
7863
|
+
};
|
|
7864
|
+
const readHudState = () => {
|
|
7126
7865
|
const page = Math.floor(toolIndex / 10);
|
|
7127
7866
|
const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
|
|
7128
7867
|
const ammo = tool.def.action.kind === "firearm" ? `${tool.ammo}/${tool.capacity}${tool.reserve ? ` \xB7 ${tool.reserve}` : ""}` : tool.def.action.kind === "hand" ? "" : tool.def.action.kind === "melee" || tool.def.action.kind === "tool" ? "" : `${num(tool.ammo, 0)}`;
|
|
@@ -7140,7 +7879,7 @@ function createTestRoom(options) {
|
|
|
7140
7879
|
inspect: inspectInfo(),
|
|
7141
7880
|
animations: animationsOf(),
|
|
7142
7881
|
locked: player.locked,
|
|
7143
|
-
timeScale,
|
|
7882
|
+
timeScale: scheduler.timeScale,
|
|
7144
7883
|
fast: fastForward,
|
|
7145
7884
|
tap: isTapOn(),
|
|
7146
7885
|
combo: combo ? comboOn : null,
|
|
@@ -7292,6 +8031,8 @@ function createTestRoom(options) {
|
|
|
7292
8031
|
const value = sampled[id];
|
|
7293
8032
|
return value === void 0 ? [] : [{ id, name: t(`metric.${id}`), text: num(value, id === "frame_ms" ? 1 : 0) }];
|
|
7294
8033
|
});
|
|
8034
|
+
const leak = memoryWatch.last;
|
|
8035
|
+
if (leak?.leaking) room2.push({ id: "memory_leak", name: "\u26A0", text: t("memory.leak_short", { geometries: leak.grew.geometries, textures: leak.grew.textures, clears: MEMORY_CLEARS }) });
|
|
7295
8036
|
const values = moduleMetrics();
|
|
7296
8037
|
const declared = (activeBench ? benches.get(activeBench)?.metrics : void 0) ?? [];
|
|
7297
8038
|
const module = declared.map((metric) => {
|
|
@@ -7307,11 +8048,11 @@ function createTestRoom(options) {
|
|
|
7307
8048
|
ui.metrics(metricRows());
|
|
7308
8049
|
};
|
|
7309
8050
|
const hostListeners = /* @__PURE__ */ new Set();
|
|
7310
|
-
const frameListeners = /* @__PURE__ */ new Set();
|
|
7311
8051
|
const listenerDirection = new THREE.Vector3();
|
|
7312
8052
|
const environment = options.environment ?? createEnvironmentBus();
|
|
7313
8053
|
const audioSink = audio ? createAudioSink(audio) : void 0;
|
|
7314
8054
|
function emitHost(event) {
|
|
8055
|
+
if (event.type === "explosion") knockPlayer(event);
|
|
7315
8056
|
for (const listener of hostListeners) listener(event);
|
|
7316
8057
|
}
|
|
7317
8058
|
function flushEvents(id, state, recoil) {
|
|
@@ -7360,12 +8101,21 @@ function createTestRoom(options) {
|
|
|
7360
8101
|
scene,
|
|
7361
8102
|
camera,
|
|
7362
8103
|
locale,
|
|
8104
|
+
...graphics ? { graphics: graphics.host } : {},
|
|
7363
8105
|
...audioSink ? { audio: audioSink } : {},
|
|
7364
8106
|
environment,
|
|
7365
8107
|
groundAt,
|
|
8108
|
+
// the terrain's material where the floor is the top; a table or a counter answers undefined (the module's default)
|
|
8109
|
+
surfaceAt: (x, z) => Math.max(sceneryTopAt(x, z), restingTopAt(x, z)) > terrain.heightAt(x, z) ? void 0 : terrain.columnAt(x, z).material,
|
|
7366
8110
|
effects: vfx,
|
|
8111
|
+
particleLimit: () => options.debris?.particleLimit ?? DEFAULT_PARTICLE_LIMIT,
|
|
8112
|
+
lights,
|
|
8113
|
+
pools,
|
|
7367
8114
|
loading,
|
|
8115
|
+
addView: (view) => moduleViews.add(view),
|
|
8116
|
+
setViewCamera: (view) => moduleViews.setMain(view ?? null),
|
|
7368
8117
|
...options.blastShove ? { blastShove: options.blastShove } : {},
|
|
8118
|
+
blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
|
|
7369
8119
|
tool: () => ({
|
|
7370
8120
|
weapon: toolId,
|
|
7371
8121
|
category: tool.def.category,
|
|
@@ -7415,9 +8165,9 @@ function createTestRoom(options) {
|
|
|
7415
8165
|
hostListeners.add(listener);
|
|
7416
8166
|
return () => hostListeners.delete(listener);
|
|
7417
8167
|
},
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
return () =>
|
|
8168
|
+
defer: (queue, queueOptions) => scheduler.budget.register(queue, queueOptions),
|
|
8169
|
+
onFrame(update, frameOptions) {
|
|
8170
|
+
return scheduler.add((ctx) => update(ctx.dt), { phase: frameOptions?.phase ?? "lateSimulate", order: frameOptions?.order ?? 0, id: `module.frame#${moduleFrameCount++}` });
|
|
7421
8171
|
},
|
|
7422
8172
|
holdFrame: () => bodyHoldFrame(),
|
|
7423
8173
|
reachFor(reach) {
|
|
@@ -7475,46 +8225,47 @@ function createTestRoom(options) {
|
|
|
7475
8225
|
return r.ok ? store : null;
|
|
7476
8226
|
};
|
|
7477
8227
|
const LIGHTS_CHECK_MS = 250;
|
|
7478
|
-
const
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
8228
|
+
const materialsOf = (group) => {
|
|
8229
|
+
const found = /* @__PURE__ */ new Set();
|
|
8230
|
+
group.traverse((object) => {
|
|
8231
|
+
const material = object.material;
|
|
8232
|
+
for (const each of Array.isArray(material) ? material : material ? [material] : []) found.add(each);
|
|
8233
|
+
});
|
|
8234
|
+
return [...found];
|
|
8235
|
+
};
|
|
8236
|
+
const probeOf = (id, adapter) => ({
|
|
8237
|
+
id,
|
|
8238
|
+
build() {
|
|
8239
|
+
const probe = adapter.warmUpProbes();
|
|
8240
|
+
const group = probe.group;
|
|
8241
|
+
return { group, materials: materialsOf(group), dispose: () => probe.dispose() };
|
|
7484
8242
|
}
|
|
8243
|
+
});
|
|
8244
|
+
warmup.register(probeOf("destruction", renderer));
|
|
8245
|
+
warmup.register(probeOf("vfx", vfx));
|
|
8246
|
+
warmup.register(probeOf("fluids", fluidView));
|
|
8247
|
+
warmup.register(spriteProbes(THREE));
|
|
8248
|
+
const warmedEffect = (id) => {
|
|
8249
|
+
let handle = null;
|
|
8250
|
+
return {
|
|
8251
|
+
id: `vfx:${id}`,
|
|
8252
|
+
play() {
|
|
8253
|
+
handle = vfx.play({ effect: id, point: [0, 0.1, -4], direction: [0, 1, 0], normal: [0, 1, 0] });
|
|
8254
|
+
},
|
|
8255
|
+
step: (dt) => vfx.update(dt),
|
|
8256
|
+
stop() {
|
|
8257
|
+
if (handle !== null) vfx.stop(handle);
|
|
8258
|
+
vfx.world.clear();
|
|
8259
|
+
vfx.update(0);
|
|
8260
|
+
}
|
|
8261
|
+
};
|
|
7485
8262
|
};
|
|
8263
|
+
const catalogIds = vfx.catalog?.() ?? [];
|
|
8264
|
+
for (const id of [...EXPLOSIVE_EFFECT_IDS, ...catalogIds.filter((id2) => /fire|flame/i.test(id2)).slice(0, 1)]) if (catalogIds.includes(id)) warmup.registerEffect(warmedEffect(id));
|
|
8265
|
+
let warmedUp = false;
|
|
7486
8266
|
const warmUp = () => {
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
const spriteMap = new THREE.DataTexture(new Uint8Array([255, 255, 255, 255]), 1, 1);
|
|
7490
|
-
spriteMap.needsUpdate = true;
|
|
7491
|
-
const spriteProbes = [true, false].map((fog) => {
|
|
7492
|
-
const probe = new THREE.Sprite(new THREE.SpriteMaterial({ map: spriteMap, transparent: true, fog }));
|
|
7493
|
-
probe.frustumCulled = false;
|
|
7494
|
-
return probe;
|
|
7495
|
-
});
|
|
7496
|
-
groups.push(...spriteProbes);
|
|
7497
|
-
scene.add(...groups);
|
|
7498
|
-
gl.render(scene, camera);
|
|
7499
|
-
pinPrograms();
|
|
7500
|
-
scene.remove(...groups);
|
|
7501
|
-
for (const probe of probes) probe.dispose();
|
|
7502
|
-
for (const probe of spriteProbes) probe.material.dispose();
|
|
7503
|
-
spriteMap.dispose();
|
|
7504
|
-
};
|
|
7505
|
-
const warmUpEffects = () => {
|
|
7506
|
-
const ids = vfx.catalog?.() ?? [];
|
|
7507
|
-
const flames = ids.filter((id) => /fire|flame/i.test(id)).slice(0, 1);
|
|
7508
|
-
gl.compile(scene, camera);
|
|
7509
|
-
const handles = ["m67", ...flames].filter((id) => ids.includes(id)).map((effect) => vfx.play({ effect, point: [0, 0.1, -4], direction: [0, 1, 0], normal: [0, 1, 0] }));
|
|
7510
|
-
for (let i = 0; i < 3; i++) {
|
|
7511
|
-
vfx.update(0.02);
|
|
7512
|
-
gl.render(scene, camera);
|
|
7513
|
-
}
|
|
7514
|
-
for (const h of handles) if (h !== null) vfx.stop(h);
|
|
7515
|
-
vfx.world.clear();
|
|
7516
|
-
vfx.update(0);
|
|
7517
|
-
gl.render(scene, camera);
|
|
8267
|
+
warmup.warmUp(gl, scene, camera, { lights, fogStates: WARMED_FOG_STATES, materials: pools.materials.registeredMaterials?.() ?? [] });
|
|
8268
|
+
warmedUp = true;
|
|
7518
8269
|
};
|
|
7519
8270
|
const loadModule = (mod) => {
|
|
7520
8271
|
const report = { module: mod.id, items: [], scenarios: [], ok: true, problems: [] };
|
|
@@ -7539,6 +8290,7 @@ function createTestRoom(options) {
|
|
|
7539
8290
|
impl: ti.impl,
|
|
7540
8291
|
animations: ti.animations,
|
|
7541
8292
|
seats: ti.seats,
|
|
8293
|
+
sounds: ti.sounds,
|
|
7542
8294
|
thumbnail: ti.thumbnail,
|
|
7543
8295
|
thumbnailCredit: ti.thumbnailCredit
|
|
7544
8296
|
};
|
|
@@ -7626,24 +8378,13 @@ function createTestRoom(options) {
|
|
|
7626
8378
|
};
|
|
7627
8379
|
const allScenarios = () => [...extraScenarios];
|
|
7628
8380
|
let scenarioRunning = false;
|
|
7629
|
-
const step = (seconds) => {
|
|
7630
|
-
const dt = seconds > 6 ? 1 / 30 : FIXED_DT;
|
|
7631
|
-
const n = Math.max(1, Math.round(seconds / dt));
|
|
7632
|
-
for (let i = 0; i < n; i++) {
|
|
7633
|
-
seating.update();
|
|
7634
|
-
player.update(0);
|
|
7635
|
-
push.step(dt);
|
|
7636
|
-
doors.step(dt);
|
|
7637
|
-
followHeld(dt);
|
|
7638
|
-
stepSim(dt);
|
|
7639
|
-
}
|
|
7640
|
-
};
|
|
8381
|
+
const step = (seconds) => runHeadless(() => scheduler.advance(seconds, { dt: seconds > 6 ? 1 / 30 : FIXED_DT }));
|
|
7641
8382
|
const runScenarios = async (list) => {
|
|
7642
8383
|
const all = list ?? allScenarios();
|
|
7643
8384
|
const report = { passed: 0, failed: 0, results: [] };
|
|
7644
8385
|
scenarioRunning = true;
|
|
7645
|
-
const wasPaused = paused;
|
|
7646
|
-
|
|
8386
|
+
const wasPaused = scheduler.paused;
|
|
8387
|
+
scheduler.resume();
|
|
7647
8388
|
try {
|
|
7648
8389
|
for (const sc of all) {
|
|
7649
8390
|
hardReset();
|
|
@@ -7675,7 +8416,7 @@ function createTestRoom(options) {
|
|
|
7675
8416
|
}
|
|
7676
8417
|
} finally {
|
|
7677
8418
|
scenarioRunning = false;
|
|
7678
|
-
|
|
8419
|
+
if (wasPaused) scheduler.pause();
|
|
7679
8420
|
restoreScenery();
|
|
7680
8421
|
}
|
|
7681
8422
|
return report;
|
|
@@ -7807,19 +8548,25 @@ function createTestRoom(options) {
|
|
|
7807
8548
|
useItem,
|
|
7808
8549
|
use(hold = 0) {
|
|
7809
8550
|
if (tool.def.category === "hand") {
|
|
7810
|
-
handUse();
|
|
8551
|
+
const status2 = handUse();
|
|
7811
8552
|
flush();
|
|
7812
|
-
return;
|
|
8553
|
+
return status2;
|
|
7813
8554
|
}
|
|
7814
|
-
useTool(FIXED_DT, false);
|
|
8555
|
+
const status = useTool(FIXED_DT, false);
|
|
7815
8556
|
flush();
|
|
7816
8557
|
let t0 = 0;
|
|
7817
8558
|
while (t0 < hold) {
|
|
7818
8559
|
useTool(FIXED_DT, true);
|
|
7819
|
-
|
|
8560
|
+
directStep = true;
|
|
8561
|
+
try {
|
|
8562
|
+
runHeadless(() => scheduler.advance(FIXED_DT));
|
|
8563
|
+
} finally {
|
|
8564
|
+
directStep = false;
|
|
8565
|
+
}
|
|
7820
8566
|
t0 += FIXED_DT;
|
|
7821
8567
|
}
|
|
7822
8568
|
releaseTrigger();
|
|
8569
|
+
return status;
|
|
7823
8570
|
},
|
|
7824
8571
|
grab,
|
|
7825
8572
|
throwHeld,
|
|
@@ -7835,14 +8582,14 @@ function createTestRoom(options) {
|
|
|
7835
8582
|
spawned() {
|
|
7836
8583
|
return [...entries.values()].filter((e) => !e.arena).map((e) => e.so);
|
|
7837
8584
|
},
|
|
7838
|
-
update,
|
|
8585
|
+
update: (dt) => runHeadless(() => scheduler.frame(dt, { render: false, exact: true })),
|
|
7839
8586
|
waitUntilReady(maxSeconds = 5) {
|
|
7840
8587
|
let waited = 0;
|
|
7841
8588
|
while (tool.phase && waited < maxSeconds) {
|
|
7842
|
-
update(FIXED_DT);
|
|
8589
|
+
room.update(FIXED_DT);
|
|
7843
8590
|
waited += FIXED_DT;
|
|
7844
8591
|
}
|
|
7845
|
-
update(FIXED_DT);
|
|
8592
|
+
room.update(FIXED_DT);
|
|
7846
8593
|
return waited + FIXED_DT;
|
|
7847
8594
|
},
|
|
7848
8595
|
on(type, fn) {
|
|
@@ -7859,20 +8606,20 @@ function createTestRoom(options) {
|
|
|
7859
8606
|
blastOptions = { ...blastOptions, ...next };
|
|
7860
8607
|
},
|
|
7861
8608
|
pause() {
|
|
7862
|
-
|
|
8609
|
+
scheduler.pause();
|
|
7863
8610
|
},
|
|
7864
8611
|
resume() {
|
|
7865
|
-
|
|
8612
|
+
scheduler.resume();
|
|
7866
8613
|
},
|
|
7867
8614
|
get paused() {
|
|
7868
|
-
return paused;
|
|
8615
|
+
return scheduler.paused;
|
|
7869
8616
|
},
|
|
7870
8617
|
walk(to, seconds) {
|
|
7871
8618
|
walkPlan = seconds > 0 ? { x: to[0], z: to[2], remaining: seconds } : null;
|
|
7872
8619
|
if (!walkPlan) player.moveBy(to[0] - camera.position.x, to[2] - camera.position.z);
|
|
7873
8620
|
},
|
|
7874
8621
|
setSlowMotion(on) {
|
|
7875
|
-
timeScale = on ? 0.25 : 1;
|
|
8622
|
+
scheduler.timeScale = on ? 0.25 : 1;
|
|
7876
8623
|
},
|
|
7877
8624
|
setFastForward(on) {
|
|
7878
8625
|
fastForward = on;
|
|
@@ -7888,12 +8635,13 @@ function createTestRoom(options) {
|
|
|
7888
8635
|
modules: () => [...modules.values()].map((m) => ({ id: m.id, name: m.name, group: m.group, hasArea: typeof m.area === "function" })),
|
|
7889
8636
|
openAreaAsync: (area) => loading.busy(t("loading.open_area", { area: areaLabel(area) }), () => openArea(area)),
|
|
7890
8637
|
loading,
|
|
8638
|
+
graphics: graphics?.controller ?? null,
|
|
8639
|
+
graphicsScreen: graphics?.screen ?? null,
|
|
7891
8640
|
busy: (label, work, busyOptions) => loading.busy(label, work, busyOptions),
|
|
7892
8641
|
loadModule,
|
|
7893
8642
|
warmUp: () => {
|
|
7894
8643
|
audio?.warmUp();
|
|
7895
8644
|
warmUp();
|
|
7896
|
-
warmUpEffects();
|
|
7897
8645
|
},
|
|
7898
8646
|
animations: animationsOf,
|
|
7899
8647
|
playAnimation,
|
|
@@ -7929,7 +8677,10 @@ function createTestRoom(options) {
|
|
|
7929
8677
|
crouchForced = state.crouch;
|
|
7930
8678
|
avatarState.crouch = state.crouch || player.crouching;
|
|
7931
8679
|
}
|
|
7932
|
-
if (state.down !== void 0)
|
|
8680
|
+
if (state.down !== void 0) {
|
|
8681
|
+
downForced = state.down;
|
|
8682
|
+
avatarState.down = state.down || Boolean(player.knocked?.down);
|
|
8683
|
+
}
|
|
7933
8684
|
if (state.armed !== void 0) armedForced = state.armed;
|
|
7934
8685
|
},
|
|
7935
8686
|
avatarState: () => ({ ...avatarState }),
|
|
@@ -7950,6 +8701,9 @@ function createTestRoom(options) {
|
|
|
7950
8701
|
get playerPosition() {
|
|
7951
8702
|
return player.position;
|
|
7952
8703
|
},
|
|
8704
|
+
get playerKnock() {
|
|
8705
|
+
return player.knocked;
|
|
8706
|
+
},
|
|
7953
8707
|
sit: (objectId, seatId) => seating.sit(objectId, seatId),
|
|
7954
8708
|
stand: () => seating.stand(),
|
|
7955
8709
|
get seat() {
|
|
@@ -7965,20 +8719,30 @@ function createTestRoom(options) {
|
|
|
7965
8719
|
metrics() {
|
|
7966
8720
|
return { room: roomMetrics.sample(), module: moduleMetrics() };
|
|
7967
8721
|
},
|
|
8722
|
+
frameStats: () => scheduler.stats(),
|
|
8723
|
+
lights,
|
|
8724
|
+
webgl: gl,
|
|
7968
8725
|
dispose() {
|
|
7969
8726
|
destructionSound?.dispose();
|
|
7970
8727
|
if (ownAudio) audio?.dispose();
|
|
7971
8728
|
for (const detach of detachers.values()) detach();
|
|
7972
8729
|
detachers.clear();
|
|
8730
|
+
moduleViews.dispose();
|
|
7973
8731
|
for (const store of benches.values()) store.dispose();
|
|
7974
8732
|
benches.clear();
|
|
7975
|
-
|
|
8733
|
+
stopLoop?.();
|
|
8734
|
+
disposePacer?.();
|
|
8735
|
+
scheduler.dispose();
|
|
7976
8736
|
window.removeEventListener("resize", onResize);
|
|
7977
8737
|
player.dispose();
|
|
7978
8738
|
ui?.dispose();
|
|
8739
|
+
graphics?.dispose();
|
|
7979
8740
|
loading.dispose();
|
|
7980
8741
|
renderer.dispose();
|
|
7981
8742
|
vfx.dispose();
|
|
8743
|
+
lights.dispose();
|
|
8744
|
+
pools.materials.dispose();
|
|
8745
|
+
pools.geometries.dispose();
|
|
7982
8746
|
fluidView.dispose();
|
|
7983
8747
|
filmBridge.dispose();
|
|
7984
8748
|
film.dispose();
|
|
@@ -8044,6 +8808,7 @@ function createTestRoom(options) {
|
|
|
8044
8808
|
}
|
|
8045
8809
|
},
|
|
8046
8810
|
quality: qualityName,
|
|
8811
|
+
...graphics?.screen ? { onGraphics: () => graphics.screen?.open() } : {},
|
|
8047
8812
|
scenarios: allScenarios,
|
|
8048
8813
|
onRun: async (sc) => {
|
|
8049
8814
|
await runScenarios([sc]);
|
|
@@ -8077,41 +8842,77 @@ function createTestRoom(options) {
|
|
|
8077
8842
|
ui.hud(hudState());
|
|
8078
8843
|
showBench();
|
|
8079
8844
|
}
|
|
8080
|
-
let
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8845
|
+
let directStep = false;
|
|
8846
|
+
scheduler.add(
|
|
8847
|
+
(ctx) => {
|
|
8848
|
+
if (directStep) return;
|
|
8849
|
+
if (!ctx.scripted) return updateInput(ctx.dt, ctx.paused);
|
|
8850
|
+
seating.update();
|
|
8851
|
+
player.update(0);
|
|
8852
|
+
push.step(ctx.dt);
|
|
8853
|
+
doors.step(ctx.dt);
|
|
8854
|
+
followHeld(ctx.dt);
|
|
8855
|
+
},
|
|
8856
|
+
{ phase: "input", id: "room.input" }
|
|
8857
|
+
);
|
|
8858
|
+
scheduler.add((ctx) => simulateBeforeModules(ctx.dt), { phase: "lateSimulate", order: -1e3, id: "room.simulate" });
|
|
8859
|
+
scheduler.add((ctx) => simulateAfterModules(ctx.dt), { phase: "lateSimulate", order: 1e3, id: "room.simulate-end" });
|
|
8860
|
+
scheduler.add(
|
|
8861
|
+
(ctx) => {
|
|
8862
|
+
if (ctx.simulated && !ctx.scripted) updatePresent(ctx.realDt);
|
|
8863
|
+
},
|
|
8864
|
+
{ phase: "present", id: "room.present" }
|
|
8865
|
+
);
|
|
8866
|
+
scheduler.add((ctx) => lights.present([camera.position.x, camera.position.y, camera.position.z], ctx.realDt), { phase: "present", order: 10, id: "room.lights" });
|
|
8867
|
+
let stopLoop;
|
|
8868
|
+
let disposePacer;
|
|
8869
|
+
if (ownRenderer) {
|
|
8084
8870
|
let nextLightsCheck = 0;
|
|
8085
|
-
let
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
return key2;
|
|
8092
|
-
};
|
|
8093
|
-
const loop = (now) => {
|
|
8094
|
-
raf = requestAnimationFrame(loop);
|
|
8095
|
-
if (now >= nextLightsCheck) {
|
|
8871
|
+
let warnedState = "";
|
|
8872
|
+
scheduler.add(
|
|
8873
|
+
(ctx) => {
|
|
8874
|
+
if (ctx.scripted) return;
|
|
8875
|
+
const now = performance.now();
|
|
8876
|
+
if (now < nextLightsCheck) return;
|
|
8096
8877
|
nextLightsCheck = now + LIGHTS_CHECK_MS;
|
|
8097
|
-
const
|
|
8098
|
-
if (
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
}
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8878
|
+
const state = lights.check(gl, scene);
|
|
8879
|
+
if (warmedUp && (state.ok || state.actual === warnedState)) return;
|
|
8880
|
+
warnedState = state.actual;
|
|
8881
|
+
warmUp();
|
|
8882
|
+
},
|
|
8883
|
+
{ phase: "render", order: -1e3, id: "room.lights-check" }
|
|
8884
|
+
);
|
|
8885
|
+
const pacer = createFramePacer(typeof gl.getContext === "function" ? gl.getContext() : null, options.framesInFlight === void 0 ? {} : { framesInFlight: options.framesInFlight });
|
|
8886
|
+
disposePacer = () => pacer.dispose();
|
|
8887
|
+
scheduler.add(
|
|
8888
|
+
(ctx) => {
|
|
8889
|
+
if (!ctx.scripted && !pacer.ready()) return;
|
|
8890
|
+
for (const batcher of batchers.values()) batcher.update();
|
|
8891
|
+
const restoreCamera = room.applyCamera();
|
|
8892
|
+
graphics?.beforeRender(camera);
|
|
8893
|
+
gl.render(scene, debugView ?? moduleViews.main(camera));
|
|
8894
|
+
moduleViews.render(scene);
|
|
8895
|
+
graphics?.afterRender();
|
|
8896
|
+
restoreCamera();
|
|
8897
|
+
pacer.submitted();
|
|
8898
|
+
roomMetrics.rendered();
|
|
8899
|
+
if (!ctx.scripted) graphics?.frameEnd();
|
|
8900
|
+
},
|
|
8901
|
+
{ phase: "render", id: "room.render" }
|
|
8902
|
+
);
|
|
8903
|
+
try {
|
|
8904
|
+
stopLoop = startLoop(scheduler, {
|
|
8905
|
+
onFrame: (realSeconds) => {
|
|
8906
|
+
graphics?.frameStart();
|
|
8907
|
+
roomMetrics.frame(realSeconds);
|
|
8908
|
+
},
|
|
8909
|
+
skipSimulation: () => scenarioRunning,
|
|
8910
|
+
// the player's frame cap (Gráficos → Limite de FPS)
|
|
8911
|
+
...graphics ? { minFrameMs: () => graphics.controller.minFrameMs() } : {}
|
|
8912
|
+
});
|
|
8913
|
+
ownsLoop = true;
|
|
8914
|
+
} catch {
|
|
8915
|
+
}
|
|
8115
8916
|
}
|
|
8116
8917
|
if (options.initial) {
|
|
8117
8918
|
for (const [i, id] of options.initial.entries()) spawn(id, [(i - (options.initial.length - 1) / 2) * 1.3, 0, 0.5]);
|