@adatechnology/game-test-room 0.21.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.js CHANGED
@@ -1,7 +1,8 @@
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 { surfaceOfMaterial } from "@adatechnology/game-contracts";
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";
5
6
 
6
7
  // src/body-sounds.ts
7
8
  import { BODY_FOLEY_BANK } from "@adatechnology/game-audio";
@@ -31,13 +32,14 @@ function blowOfTool(id, category, table = DEFAULT_TOOL_BLOWS) {
31
32
  }
32
33
  var levelOf = (energyJ) => Math.max(0.5, Math.min(1, 0.5 + energyJ / 800));
33
34
  var STRIDE_METRES = { walk: 0.72, run: 1.05 };
35
+ var SLAM_HURT_SPEED = 3.5;
34
36
  function createBodySounds(audio) {
35
37
  audio?.register(BODY_FOLEY_BANK);
36
38
  const played = [];
37
39
  const play = (id, position, volume, delay = 0) => {
38
40
  played.push(id);
39
41
  if (played.length > 64) played.shift();
40
- audio?.play(id, { position, volume, ...delay > 0 ? { delay } : {} });
42
+ audio?.play(id, { position, volume, urgent: true, ...delay > 0 ? { delay } : {} });
41
43
  };
42
44
  return {
43
45
  swing(blow, position, delay = 0) {
@@ -49,6 +51,10 @@ function createBodySounds(audio) {
49
51
  },
50
52
  jump: (position) => play("jump", position, 0.6),
51
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
+ },
52
58
  played
53
59
  };
54
60
  }
@@ -73,7 +79,6 @@ function createStrideCounter(stride = STRIDE_METRES) {
73
79
  }
74
80
 
75
81
  // src/room.ts
76
- import { createDestructionRenderer, createDestructionSound, dragSoundKey, impactKey, specFromObject3D } from "@adatechnology/game-destruction-three";
77
82
  import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
78
83
  import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
79
84
  import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
@@ -316,6 +321,38 @@ function createArena(THREE, size, scene, scenery, tapOption) {
316
321
  };
317
322
  }
318
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
+
319
356
  // src/bench.ts
320
357
  var snap = (control, value) => {
321
358
  const clamped = Math.min(control.max, Math.max(control.min, value));
@@ -765,7 +802,7 @@ function createExplosives(context) {
765
802
  if (!detonation.cone) told(results);
766
803
  if (detonation.energy > 0) {
767
804
  const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
768
- 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 });
769
806
  }
770
807
  if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
771
808
  const onPlayer = blastEffect(detonation, toArray(camera.position));
@@ -2088,6 +2125,8 @@ function createNarrator(c) {
2088
2125
  }
2089
2126
 
2090
2127
  // src/player.ts
2128
+ import { stepKnock } from "@adatechnology/game-contracts";
2129
+ var SLAM_MIN_SPEED = 1.5;
2091
2130
  var LANDING_MIN_SPEED = 1;
2092
2131
  var EYE = 1.7;
2093
2132
  var PLAYER_RADIUS = 0.3;
@@ -2105,6 +2144,9 @@ var GRAVITY = 13;
2105
2144
  var FLOOR_CLIMB_SPEED = 3;
2106
2145
  var FLOOR_FALL_SPEED = 6;
2107
2146
  var LOOK = 22e-4;
2147
+ var LYING_EYE = 0.3;
2148
+ var LYING_PITCH = 1.05;
2149
+ var LYING_ROLL = 0.35;
2108
2150
  function createPlayer(THREE, o) {
2109
2151
  const cam = o.camera;
2110
2152
  let yaw = 0;
@@ -2142,10 +2184,15 @@ function createPlayer(THREE, o) {
2142
2184
  const reach = (gap > 0 ? FLOOR_CLIMB_SPEED : FLOOR_FALL_SPEED) * dt;
2143
2185
  floorLevel += Math.max(-reach, Math.min(reach, gap));
2144
2186
  };
2187
+ let knocked = null;
2188
+ let rollSide = 1;
2145
2189
  const apply = () => {
2146
- euler.set(pitch, yaw, 0, "YXZ");
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");
2147
2193
  cam.quaternion.setFromEuler(euler);
2148
- cam.position.set(px.x, eye + floorLevel + y - CROUCH_DROP * crouch - bend, px.z);
2194
+ const standing = eye - CROUCH_DROP * crouch - bend;
2195
+ cam.position.set(px.x, floorLevel + y + standing + (LYING_EYE - standing) * fallen, px.z);
2149
2196
  cam.updateMatrixWorld(true);
2150
2197
  };
2151
2198
  apply();
@@ -2320,6 +2367,41 @@ function createPlayer(THREE, o) {
2320
2367
  window.addEventListener("keyup", onKeyUp);
2321
2368
  window.addEventListener("blur", onBlur);
2322
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
+ };
2323
2405
  return {
2324
2406
  get walkIntent() {
2325
2407
  return walkKeys();
@@ -2375,7 +2457,28 @@ function createPlayer(THREE, o) {
2375
2457
  return isCrouched();
2376
2458
  },
2377
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
+ },
2378
2477
  update(dt) {
2478
+ if (knocked) {
2479
+ stepKnocked(dt);
2480
+ return;
2481
+ }
2379
2482
  if (crouchPress) crouchPress.held += dt;
2380
2483
  const crouched = isCrouched();
2381
2484
  crouch += ((crouched ? 1 : 0) - crouch) * Math.min(1, dt * CROUCH_RATE);
@@ -2415,6 +2518,7 @@ function createPlayer(THREE, o) {
2415
2518
  apply();
2416
2519
  },
2417
2520
  setPose(position, yw, pt) {
2521
+ knocked = null;
2418
2522
  px.x = position[0];
2419
2523
  px.z = position[2];
2420
2524
  floorLevel = floorHere();
@@ -2426,6 +2530,7 @@ function createPlayer(THREE, o) {
2426
2530
  apply();
2427
2531
  },
2428
2532
  look(position, target) {
2533
+ knocked = null;
2429
2534
  px.x = position[0];
2430
2535
  px.z = position[2];
2431
2536
  floorLevel = floorHere();
@@ -2782,6 +2887,8 @@ var TURN_RATE = 8;
2782
2887
  var MAX_WALK_SECONDS = 1.5;
2783
2888
  var APPROACH_SPEED2 = 3;
2784
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);
2785
2892
  var key = (objectId, part) => `${objectId}\0${part}`;
2786
2893
  var slides = (clip, part) => {
2787
2894
  const last = clip.keys[clip.keys.length - 1]?.parts?.[part];
@@ -3027,7 +3134,7 @@ function createRoomDoors(options) {
3027
3134
  using = { objectId, leaf, obj, grip, pull, phase: control.phase, progress: 0, walked: 0, there: goal === null };
3028
3135
  const at = obj.localToWorld(grip.clone());
3029
3136
  if (goal === null) {
3030
- 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);
3031
3138
  event("locked", using);
3032
3139
  } else event("reach", using);
3033
3140
  return true;
@@ -3112,17 +3219,27 @@ function createRoomDoors(options) {
3112
3219
  const at = u.obj.localToWorld(tmp.copy(u.grip));
3113
3220
  const point = [at.x, at.y, at.z];
3114
3221
  const slide = slides(u.leaf.clip, u.leaf.part);
3115
- const shut = options.impactKey(u.objectId, u.leaf.part).replace("impact_", "leaf_shut_");
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_");
3116
3225
  if (wasPhase === "reach" && s.phase !== "reach") {
3117
- if (slide) options.sound("leaf_slide", point, 0.5);
3118
- else {
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 {
3119
3234
  if (was < 0.02) options.sound("leaf_latch", point, 0.5);
3120
3235
  options.sound(/metal|glass/.test(shut) ? "leaf_creak_metal" : "leaf_creak_wood", point, 0.35);
3121
3236
  }
3122
3237
  }
3123
3238
  if (s.arrived) {
3124
3239
  if (s.opening <= 0) {
3125
- options.sound(slide ? "leaf_slide" : shut, point, Math.min(1, 0.3 + massOf(u.objectId, u.leaf.part) * 0.03));
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);
3126
3243
  event("closed", u);
3127
3244
  } else event("opened", u);
3128
3245
  }
@@ -3393,6 +3510,10 @@ var en = {
3393
3510
  "loading.open_area": "Opening {area}\u2026",
3394
3511
  "loading.spawn": "Building {name}\u2026",
3395
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)",
3396
3517
  "act.came_apart": "{name} came apart: what stayed is loose and falls",
3397
3518
  "act.shoved": "{name} stands free: the shots moved it",
3398
3519
  "act.impact": "{name} hit {where} at {speed} m/s: {energy} J of blunt damage",
@@ -3751,6 +3872,10 @@ var ptBR = {
3751
3872
  "loading.open_area": "Abrindo {area}\u2026",
3752
3873
  "loading.spawn": "Montando {name}\u2026",
3753
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)",
3754
3879
  "act.came_apart": "{name} se desmontou: o que ficou est\xE1 solto e cai",
3755
3880
  "act.shoved": "{name} estava solto no ch\xE3o: os tiros o moveram",
3756
3881
  "act.impact": "{name} bateu em {where} a {speed} m/s: {energy} J de dano de impacto",
@@ -4576,9 +4701,16 @@ function createUi(o) {
4576
4701
  nodes.delete(oldest.value);
4577
4702
  logBody.firstElementChild?.remove();
4578
4703
  }
4579
- if (autoscroll) logBody.scrollTop = logBody.scrollHeight;
4704
+ if (autoscroll) isScrollDue = true;
4580
4705
  }
4706
+ let isScrollDue = false;
4707
+ const scrollToEnd = () => {
4708
+ if (!isScrollDue) return;
4709
+ isScrollDue = false;
4710
+ logBody.scrollTop = logBody.scrollHeight;
4711
+ };
4581
4712
  const flushLog = () => {
4713
+ scrollToEnd();
4582
4714
  if (pendingAdds.length === 0 && pendingUpdates.size === 0) return;
4583
4715
  if (!isLogVisible()) {
4584
4716
  isLogStale = true;
@@ -4971,6 +5103,134 @@ function createUi(o) {
4971
5103
  return api;
4972
5104
  }
4973
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
+
4974
5234
  // src/room.ts
4975
5235
  var FIXED_DT = 1 / 60;
4976
5236
  var EXPLOSIVE_EFFECT_IDS = ["m67", "molotov", "pipe_bomb", "m79_40mm", "m72_66mm", "at4_84mm", "dynamite", "c4", "claymore"];
@@ -4999,6 +5259,12 @@ var CAP_RISE = 0.03;
4999
5259
  var MAX_STRIDE_SPEED = 10;
5000
5260
  var v3 = (v) => [v.x, v.y, v.z];
5001
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"]);
5002
5268
  function createTestRoom(options) {
5003
5269
  const THREE = options.THREE;
5004
5270
  const locale = options.locale ?? "en";
@@ -5045,6 +5311,7 @@ function createTestRoom(options) {
5045
5311
  };
5046
5312
  const onResize = () => resize();
5047
5313
  window.addEventListener("resize", onResize);
5314
+ const moduleViews = createModuleViews(THREE, gl);
5048
5315
  const arenaSize = options.arenaSize ?? 14;
5049
5316
  const arena = createArena(THREE, arenaSize, scene, options.scenery, options.tap);
5050
5317
  scene.add(arena.group);
@@ -5104,10 +5371,23 @@ function createTestRoom(options) {
5104
5371
  const ownAudio = options.audio === void 0;
5105
5372
  const audio = options.audio === false ? null : options.audio ?? createAudioEngine({ deferredStart: {} });
5106
5373
  const destructionSound = audio ? createDestructionSound(audio) : null;
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
+ }
5107
5386
  const bodySounds = createBodySounds(audio);
5108
- const playSound = (key2, point, volume) => {
5109
- destructionSound?.onSound(key2, point, volume);
5110
- options.onSound?.(key2, point, volume);
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);
5111
5391
  };
5112
5392
  const lights = createLightBudget(THREE, scene, { ...ROOM_LIGHT_SIZES, ...options.lightSizes });
5113
5393
  const warmup = createProgramWarmup(THREE);
@@ -5177,7 +5457,7 @@ function createTestRoom(options) {
5177
5457
  const size = Math.min(GROUND_BURST_MAX, Math.ceil(result.radius * GROUND_BURST_PER_METRE));
5178
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));
5179
5459
  if (result.radius > GROUND_DUST_RADIUS) renderer.emit("dust", result.point, [0, 1, 0], size, 1.4);
5180
- 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));
5181
5461
  };
5182
5462
  terrain.onDeform((region) => renderer.wakeRegion(region.x, region.z, region.radius));
5183
5463
  const groundImpl = createTerrainImpl(terrain, { id: GROUND_ID, name: t("ground.name"), onBlow: showGroundBlow });
@@ -5405,6 +5685,7 @@ function createTestRoom(options) {
5405
5685
  floorAt: terrain.heightAt,
5406
5686
  start: arena.playerStart,
5407
5687
  crouchKeys: options.crouch?.keys,
5688
+ onSlam: (event) => slamPlayer(event),
5408
5689
  onFeet: (event) => {
5409
5690
  const at = player.position;
5410
5691
  if (event.type === "jump") bodySounds.jump(at);
@@ -5484,7 +5765,11 @@ function createTestRoom(options) {
5484
5765
  return piece;
5485
5766
  },
5486
5767
  dragSound: (objectId, point, volume) => playSound(dragSoundKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, volume),
5487
- hitSound: (objectId, point) => playSound(impactKey(world.materials.get(heaviestMaterial(objectId) ?? "")), point, 0.5),
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
+ },
5488
5773
  onEvent: (ev) => {
5489
5774
  const vars = { name: ev.name, kg: num(ev.massKg, 0) };
5490
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 } });
@@ -5574,7 +5859,13 @@ function createTestRoom(options) {
5574
5859
  isLocked: (objectId, part) => lockedLeaves.has(`${objectId}\0*`) || lockedLeaves.has(`${objectId}\0${part}`),
5575
5860
  nameOf,
5576
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
+ },
5577
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),
5578
5869
  onEvent: (ev) => {
5579
5870
  const vars = { name: ev.name, percent: Math.round(ev.opening * 100) };
5580
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 } : {} } });
@@ -5983,6 +6274,37 @@ function createTestRoom(options) {
5983
6274
  let avatar = null;
5984
6275
  let cameraMode = options.cameraMode ?? "first";
5985
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
+ };
5986
6308
  const avatarState = { speed: 0, running: false, crouch: false, down: false, armed: false, pitch: 0 };
5987
6309
  let feetPrev = null;
5988
6310
  let armedForced = null;
@@ -6038,6 +6360,7 @@ function createTestRoom(options) {
6038
6360
  feetPrev = [x, z];
6039
6361
  avatarState.running = !player.crouching && (player.keys.has("ShiftLeft") || player.keys.has("ShiftRight"));
6040
6362
  avatarState.crouch = crouchForced || player.crouching || push.isBent;
6363
+ avatarState.down = downForced || Boolean(player.knocked?.down || (player.knocked?.lying ?? 0) > 0.5);
6041
6364
  avatarState.armed = armedForced ?? tool.def.action.kind === "firearm";
6042
6365
  avatarState.pitch = player.pitch;
6043
6366
  avatarState.holding = heldPiece !== null;
@@ -6110,14 +6433,18 @@ function createTestRoom(options) {
6110
6433
  if (!avatar?.reach) return;
6111
6434
  avatar.reach(targets, { camera, keepOutside: { contact: (origin, direction, maxDistance) => rayWorld(origin, direction, maxDistance)?.distance ?? null } });
6112
6435
  };
6436
+ const thirdEuler = new THREE.Euler(0, 0, 0, "YXZ");
6113
6437
  const applyThirdView = () => {
6114
6438
  if (!thirdActive()) return () => void 0;
6115
6439
  const home = camera.position.clone();
6440
+ const turn = camera.quaternion.clone();
6116
6441
  const pose = thirdPose(0);
6117
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"));
6118
6444
  camera.updateMatrixWorld(true);
6119
6445
  return () => {
6120
6446
  camera.position.copy(home);
6447
+ camera.quaternion.copy(turn);
6121
6448
  camera.updateMatrixWorld(true);
6122
6449
  };
6123
6450
  };
@@ -6499,7 +6826,13 @@ function createTestRoom(options) {
6499
6826
  action("melee", t("act.melee.miss", { weapon: w }), { data: { weapon: id } });
6500
6827
  return;
6501
6828
  }
6502
- world.hitPart(r.object, r.part, { ...useToHit(u, r.point, r.normal), source: "player" });
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
+ }
6503
6836
  const blow = blowOfTool(id, toolDef(id)?.category ?? "");
6504
6837
  if (blow) bodySounds.hit(blow, blowTarget(r), r.point, u.energy);
6505
6838
  pushPiece(r.object, u.direction, 2 * u.energy / 6, r.point);
@@ -6857,7 +7190,9 @@ function createTestRoom(options) {
6857
7190
  heldYaw = 0;
6858
7191
  heldTilt = 0;
6859
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));
6860
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);
6861
7196
  fire("grab", { held: true, objectId: r.object });
6862
7197
  return true;
6863
7198
  };
@@ -6870,6 +7205,7 @@ function createTestRoom(options) {
6870
7205
  releaseHeld();
6871
7206
  piece.velocity = [0, 0, 0];
6872
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));
6873
7209
  action("throw", t("act.throw", { name, speed: num(speed, 1) }), { objectId: id, itemId: entryOf(id)?.so.itemId, data: { speedMs: speed, massKg: piece.mass } });
6874
7210
  fire("throw", { objectId: id, speed });
6875
7211
  return true;
@@ -6981,6 +7317,7 @@ function createTestRoom(options) {
6981
7317
  e.opened = true;
6982
7318
  if (e.containerId) fluids.open(e.containerId);
6983
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);
6984
7321
  const tool2 = opener.tool ? catalogMap.get(opener.tool)?.name ?? (arsenal.weapons.has(opener.tool) ? toolName(opener.tool) : opener.tool) : void 0;
6985
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" } });
6986
7323
  return true;
@@ -7029,17 +7366,28 @@ function createTestRoom(options) {
7029
7366
  if (consuming && c.closed) fluids.open(e.containerId);
7030
7367
  useItemPlayer = createItemUse(fluids, e.containerId, def);
7031
7368
  useItemName = def.id;
7369
+ useSoundIn = 0;
7032
7370
  if (eating) useLiters = c.liters;
7033
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 } });
7034
7372
  return true;
7035
7373
  };
7036
7374
  let useLiters = null;
7375
+ let useSoundIn = 0;
7037
7376
  const updateItemUse = (dt) => {
7038
7377
  if (!useItemPlayer) return;
7039
7378
  const e = heldEntry;
7040
7379
  const c = e?.containerId ? fluids.container(e.containerId) : void 0;
7041
7380
  const before = c?.liters;
7042
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
+ }
7043
7391
  const seal = e?.item?.seal;
7044
7392
  twisting = null;
7045
7393
  if (e && seal?.openUse === useItemName && !e.opened) {
@@ -7249,7 +7597,9 @@ function createTestRoom(options) {
7249
7597
  if (!loadout.includes(weaponId)) loadout.push(weaponId);
7250
7598
  setTool(weaponId);
7251
7599
  };
7600
+ let hudAimHit;
7252
7601
  const inspectTarget = () => {
7602
+ if (hudAimHit !== void 0) return hudAimHit;
7253
7603
  aim();
7254
7604
  return rayWorld(aimO, aimD, 14);
7255
7605
  };
@@ -7434,6 +7784,7 @@ function createTestRoom(options) {
7434
7784
  }
7435
7785
  syncLiters();
7436
7786
  renderer.update(dt);
7787
+ destructionSound?.update(dt);
7437
7788
  for (const e of entries.values()) e.follower?.update?.();
7438
7789
  terrainView.sync(camera.position);
7439
7790
  showGroundFire(dt);
@@ -7490,12 +7841,12 @@ function createTestRoom(options) {
7490
7841
  const updatePresent = (dt) => {
7491
7842
  reachAvatar();
7492
7843
  if (progress && simTime - progress.at > 0.25) progress = null;
7844
+ ui?.flushLog();
7493
7845
  hudAcc += dt;
7494
7846
  if (hudAcc >= 0.1 && ui) {
7495
7847
  hudAcc = 0;
7496
7848
  ui.hud(hudState());
7497
7849
  }
7498
- ui?.flushLog();
7499
7850
  metricsAcc += dt;
7500
7851
  if (metricsAcc >= METRICS_INTERVAL_S && ui) {
7501
7852
  metricsAcc = 0;
@@ -7503,6 +7854,14 @@ function createTestRoom(options) {
7503
7854
  }
7504
7855
  };
7505
7856
  const hudState = () => {
7857
+ hudAimHit = inspectTarget();
7858
+ try {
7859
+ return readHudState();
7860
+ } finally {
7861
+ hudAimHit = void 0;
7862
+ }
7863
+ };
7864
+ const readHudState = () => {
7506
7865
  const page = Math.floor(toolIndex / 10);
7507
7866
  const slots = loadout.slice(page * 10, page * 10 + 10).map((id) => arsenal.name(id));
7508
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)}`;
@@ -7693,6 +8052,7 @@ function createTestRoom(options) {
7693
8052
  const environment = options.environment ?? createEnvironmentBus();
7694
8053
  const audioSink = audio ? createAudioSink(audio) : void 0;
7695
8054
  function emitHost(event) {
8055
+ if (event.type === "explosion") knockPlayer(event);
7696
8056
  for (const listener of hostListeners) listener(event);
7697
8057
  }
7698
8058
  function flushEvents(id, state, recoil) {
@@ -7752,7 +8112,10 @@ function createTestRoom(options) {
7752
8112
  lights,
7753
8113
  pools,
7754
8114
  loading,
8115
+ addView: (view) => moduleViews.add(view),
8116
+ setViewCamera: (view) => moduleViews.setMain(view ?? null),
7755
8117
  ...options.blastShove ? { blastShove: options.blastShove } : {},
8118
+ blastReach: (point, target, ignore) => world.blastReach(point, target, ignore ? { ignore } : {}),
7756
8119
  tool: () => ({
7757
8120
  weapon: toolId,
7758
8121
  category: tool.def.category,
@@ -7927,6 +8290,7 @@ function createTestRoom(options) {
7927
8290
  impl: ti.impl,
7928
8291
  animations: ti.animations,
7929
8292
  seats: ti.seats,
8293
+ sounds: ti.sounds,
7930
8294
  thumbnail: ti.thumbnail,
7931
8295
  thumbnailCredit: ti.thumbnailCredit
7932
8296
  };
@@ -8313,7 +8677,10 @@ function createTestRoom(options) {
8313
8677
  crouchForced = state.crouch;
8314
8678
  avatarState.crouch = state.crouch || player.crouching;
8315
8679
  }
8316
- if (state.down !== void 0) avatarState.down = state.down;
8680
+ if (state.down !== void 0) {
8681
+ downForced = state.down;
8682
+ avatarState.down = state.down || Boolean(player.knocked?.down);
8683
+ }
8317
8684
  if (state.armed !== void 0) armedForced = state.armed;
8318
8685
  },
8319
8686
  avatarState: () => ({ ...avatarState }),
@@ -8334,6 +8701,9 @@ function createTestRoom(options) {
8334
8701
  get playerPosition() {
8335
8702
  return player.position;
8336
8703
  },
8704
+ get playerKnock() {
8705
+ return player.knocked;
8706
+ },
8337
8707
  sit: (objectId, seatId) => seating.sit(objectId, seatId),
8338
8708
  stand: () => seating.stand(),
8339
8709
  get seat() {
@@ -8357,6 +8727,7 @@ function createTestRoom(options) {
8357
8727
  if (ownAudio) audio?.dispose();
8358
8728
  for (const detach of detachers.values()) detach();
8359
8729
  detachers.clear();
8730
+ moduleViews.dispose();
8360
8731
  for (const store of benches.values()) store.dispose();
8361
8732
  benches.clear();
8362
8733
  stopLoop?.();
@@ -8519,7 +8890,8 @@ function createTestRoom(options) {
8519
8890
  for (const batcher of batchers.values()) batcher.update();
8520
8891
  const restoreCamera = room.applyCamera();
8521
8892
  graphics?.beforeRender(camera);
8522
- gl.render(scene, debugView ?? camera);
8893
+ gl.render(scene, debugView ?? moduleViews.main(camera));
8894
+ moduleViews.render(scene);
8523
8895
  graphics?.afterRender();
8524
8896
  restoreCamera();
8525
8897
  pacer.submitted();