@adatechnology/game-test-room 0.6.1 → 0.8.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,10 +1,13 @@
1
1
  // src/room.ts
2
2
  import { createDestruction, JOINT_DEFAULTS as JOINT_DEFAULTS2, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
3
- import { createDestructionRenderer, specFromObject3D } from "@adatechnology/game-destruction-three";
3
+ import { createAudioEngine, createAudioSink } from "@adatechnology/game-audio";
4
+ import { createDestructionRenderer, createDestructionSound, specFromObject3D } from "@adatechnology/game-destruction-three";
4
5
  import { createArsenal, DEFAULT_LOADOUT } from "@adatechnology/game-weapons";
5
- import { createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
6
- import { createFluidRenderer } from "@adatechnology/game-fluids-three";
7
- import { checkAnimation, checkBench, checkDestructible, checkSpec } from "@adatechnology/game-contracts";
6
+ import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
7
+ import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
8
+ import { createGroundField, createVfx } from "@adatechnology/game-vfx";
9
+ import { createGroundPatch, createVfxRenderer } from "@adatechnology/game-vfx-three";
10
+ import { checkAnimation, checkBench, checkDestructible, checkSpec, createEnvironmentBus } from "@adatechnology/game-contracts";
8
11
 
9
12
  // src/items.ts
10
13
  import { buildFrame } from "@adatechnology/game-destruction";
@@ -1255,6 +1258,7 @@ function createBenchStore(module, locale, onListenerError) {
1255
1258
  const changed = () => {
1256
1259
  for (const watcher of watchers) watcher();
1257
1260
  };
1261
+ let sheet = null;
1258
1262
  const controlOf = (id) => controls.find((control) => control.id === id);
1259
1263
  const store = {
1260
1264
  moduleId: module.id,
@@ -1296,6 +1300,11 @@ function createBenchStore(module, locale, onListenerError) {
1296
1300
  watchers.add(listener);
1297
1301
  return () => watchers.delete(listener);
1298
1302
  },
1303
+ sheet: () => sheet,
1304
+ setSheet(next) {
1305
+ sheet = next;
1306
+ changed();
1307
+ },
1299
1308
  patch(id, patch) {
1300
1309
  const index = controls.findIndex((control2) => control2.id === id);
1301
1310
  if (index < 0) return;
@@ -1517,7 +1526,10 @@ function createExplosives(context) {
1517
1526
  const weaponName = context.toolName(projectile.weapon);
1518
1527
  const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
1519
1528
  const reached = detonation.energy <= 0 ? 0 : detonation.cone ? coneBlast(detonation) : world.explode(detonation.point, detonation.energy, reach, "player").length;
1520
- if (detonation.energy > 0) renderer.explode(detonation.point, detonation.energy, reach);
1529
+ if (detonation.energy > 0) {
1530
+ const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
1531
+ renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed });
1532
+ }
1521
1533
  if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
1522
1534
  const onPlayer = blastEffect(detonation, toArray(camera.position));
1523
1535
  action("explode", translate("act.explode", { weapon: weaponName, energy: format(detonation.energy, 0), radius: format(reach, 1), n: reached }), {
@@ -1526,7 +1538,7 @@ function createExplosives(context) {
1526
1538
  if (onPlayer.zone !== "none") {
1527
1539
  action("blast_player", translate("act.blast_player", { weapon: weaponName, zone: translate(`blast.zone.${onPlayer.zone}`), distance: format(onPlayer.distance, 1), stun: format(onPlayer.stunSeconds, 1) }), { data: { ...onPlayer } });
1528
1540
  }
1529
- context.emitHost({ type: "explosion", source: projectile.weapon, point: detonation.point, energy: detonation.energy, radius: reach, cause: detonation.cause });
1541
+ context.emitHost({ type: "explosion", source: projectile.weapon, point: detonation.point, energy: detonation.energy, radius: reach, cause: detonation.cause, explosive: detonation.explosive });
1530
1542
  if (detonation.fire) context.startFire(projectile.weapon, detonation.point, detonation.fire, detonation.cause === "impact");
1531
1543
  };
1532
1544
  const detonateBare = (projectile, isImpact) => {
@@ -1853,9 +1865,10 @@ function createRoomMetrics(sources) {
1853
1865
  if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
1854
1866
  const destruction = sources.destruction.stats();
1855
1867
  const fluids = sources.fluids.stats();
1868
+ const effects = sources.effects.stats();
1856
1869
  put("objects", destruction.pieces);
1857
1870
  put("fragments", destruction.fragments);
1858
- put("particles", destruction.particles + fluids.particles);
1871
+ put("particles", destruction.particles + fluids.particles + effects.puffs + effects.sparks + effects.glows + effects.chunks);
1859
1872
  put("decals", destruction.decals + fluids.stains);
1860
1873
  return out;
1861
1874
  }
@@ -2955,7 +2968,7 @@ var mk = (tag, cls, text) => {
2955
2968
  if (text !== void 0) e.textContent = text;
2956
2969
  return e;
2957
2970
  };
2958
- var shapeOf = (view) => JSON.stringify([view.title, view.controls.map((c) => ({ ...c, value: void 0 })), view.layers.map((l) => [l.id, l.name])]);
2971
+ var shapeOf = (view) => JSON.stringify([view.title, view.controls.map((c) => ({ ...c, value: void 0 })), view.layers.map((l) => [l.id, l.name]), view.sheet ?? null]);
2959
2972
  function createBenchPanel(o) {
2960
2973
  const { t } = o;
2961
2974
  const element = mk("div", "tr-panel tr-bench");
@@ -3010,7 +3023,8 @@ function createBenchPanel(o) {
3010
3023
  const choiceControl = (control) => {
3011
3024
  const wrap = mk("div", "tr-ctl");
3012
3025
  wrap.appendChild(mk("span", void 0, control.name));
3013
- if (control.options.length <= SEGMENTED_MAX) {
3026
+ const grouped = control.options.some((option) => option.group !== void 0);
3027
+ if (control.options.length <= SEGMENTED_MAX && !grouped) {
3014
3028
  const group = mk("div", "tr-seg");
3015
3029
  group.setAttribute("role", "group");
3016
3030
  const buttons = control.options.map((option) => {
@@ -3033,10 +3047,22 @@ function createBenchPanel(o) {
3033
3047
  }
3034
3048
  const select = mk("select");
3035
3049
  select.disabled = control.disabled;
3050
+ let heading;
3051
+ let section = select;
3036
3052
  for (const option of control.options) {
3053
+ if (option.group !== heading) {
3054
+ heading = option.group;
3055
+ section = select;
3056
+ if (heading !== void 0) {
3057
+ const optgroup = mk("optgroup");
3058
+ optgroup.label = heading;
3059
+ select.appendChild(optgroup);
3060
+ section = optgroup;
3061
+ }
3062
+ }
3037
3063
  const op = mk("option", void 0, option.name);
3038
3064
  op.value = option.id;
3039
- select.appendChild(op);
3065
+ section.appendChild(op);
3040
3066
  }
3041
3067
  select.onchange = () => o.onControl(control.id, select.value);
3042
3068
  wrap.appendChild(select);
@@ -3074,7 +3100,63 @@ function createBenchPanel(o) {
3074
3100
  }
3075
3101
  return wrap;
3076
3102
  };
3103
+ let playhead = null;
3104
+ const sheetCard = (sheet) => {
3105
+ const card = mk("div", "tr-sheet");
3106
+ if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
3107
+ card.appendChild(mk("h4", void 0, sheet.title));
3108
+ if (sheet.body) card.appendChild(mk("p", void 0, sheet.body));
3109
+ if (sheet.facts?.length) {
3110
+ const facts = mk("dl", "tr-facts");
3111
+ for (const fact of sheet.facts) facts.append(mk("dt", void 0, fact.label), mk("dd", void 0, fact.value));
3112
+ card.appendChild(facts);
3113
+ }
3114
+ if (sheet.steps?.length) {
3115
+ const steps = mk("ol", "tr-steps");
3116
+ for (const step of sheet.steps) {
3117
+ const li = mk("li");
3118
+ li.append(mk("b", void 0, step.at), document.createTextNode(` ${step.text}`));
3119
+ steps.appendChild(li);
3120
+ }
3121
+ card.appendChild(steps);
3122
+ }
3123
+ if (sheet.swatches?.length) {
3124
+ const row = mk("div", "tr-swatches");
3125
+ for (const swatch of sheet.swatches) {
3126
+ const chip = mk("span", "tr-swatch");
3127
+ const dot = mk("i");
3128
+ dot.style.background = swatch.color;
3129
+ chip.append(dot, document.createTextNode(swatch.name));
3130
+ row.appendChild(chip);
3131
+ }
3132
+ card.appendChild(row);
3133
+ }
3134
+ if (sheet.timeline) {
3135
+ const { segments, ticks, metric } = sheet.timeline;
3136
+ const end = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
3137
+ const strip = mk("div", "tr-timeline");
3138
+ for (const segment of segments) {
3139
+ const bar = mk("div", "tr-segment", segment.name);
3140
+ bar.style.left = `${segment.start / end * 100}%`;
3141
+ bar.style.width = `${(segment.end - segment.start) / end * 100}%`;
3142
+ bar.style.background = segment.color;
3143
+ strip.appendChild(bar);
3144
+ }
3145
+ for (const tick of ticks) {
3146
+ const mark = mk("u", "tr-tick");
3147
+ mark.style.left = `${tick / end * 100}%`;
3148
+ strip.appendChild(mark);
3149
+ }
3150
+ const head = mk("s", "tr-playhead");
3151
+ strip.appendChild(head);
3152
+ playhead = { node: head, metric, end };
3153
+ card.appendChild(strip);
3154
+ }
3155
+ if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
3156
+ return card;
3157
+ };
3077
3158
  const rebuild = (view) => {
3159
+ playhead = null;
3078
3160
  body.textContent = "";
3079
3161
  refreshers.length = 0;
3080
3162
  body.appendChild(mk("div", "sub", view.title));
@@ -3082,6 +3164,7 @@ function createBenchPanel(o) {
3082
3164
  for (const control of view.controls) if (!control.hidden) list.appendChild(controlElement(control));
3083
3165
  body.appendChild(list);
3084
3166
  if (view.layers.length) body.appendChild(layerList(view.layers));
3167
+ if (view.sheet) body.appendChild(sheetCard(view.sheet));
3085
3168
  };
3086
3169
  const renderMetrics = (rows) => {
3087
3170
  metricList.textContent = "";
@@ -3112,6 +3195,7 @@ function createBenchPanel(o) {
3112
3195
  hasBench = view !== null;
3113
3196
  syncDisplay();
3114
3197
  if (!view) {
3198
+ playhead = null;
3115
3199
  shape = "";
3116
3200
  body.textContent = "";
3117
3201
  refreshers.length = 0;
@@ -3141,6 +3225,10 @@ function createBenchPanel(o) {
3141
3225
  node.text.textContent = row.text;
3142
3226
  if (node.bar && row.ratio !== void 0) fillBar(node.bar, row.ratio);
3143
3227
  }
3228
+ if (playhead) {
3229
+ const at = rows.module.find((row) => row.id === playhead?.metric)?.value;
3230
+ playhead.node.style.left = `${Math.max(0, Math.min(1, (at ?? 0) / playhead.end)) * 100}%`;
3231
+ }
3144
3232
  },
3145
3233
  get visible() {
3146
3234
  if (element.style.display === "none") return false;
@@ -3173,6 +3261,13 @@ var CSS = `
3173
3261
  .tr-status{margin-top:4px;min-height:16px;color:#ffd23f;text-shadow:0 1px 2px #000}
3174
3262
  .tr-prog{margin:4px auto 0;width:260px;background:rgba(10,12,16,.72);border-radius:6px;padding:4px 8px;display:none}
3175
3263
  .tr-prog b{display:block;font-weight:500;margin-bottom:3px}
3264
+ .tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
3265
+ .tr-sheet h4{margin:2px 0 4px;font-size:13px}.tr-sheet p{margin:4px 0}
3266
+ .tr-facts{display:grid;grid-template-columns:auto 1fr;gap:2px 8px;margin:4px 0}.tr-facts dt{opacity:.7}.tr-facts dd{margin:0}
3267
+ .tr-steps{margin:4px 0;padding-left:16px}.tr-swatches{display:flex;flex-wrap:wrap;gap:6px}.tr-swatch i{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:4px}
3268
+ .tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
3269
+ .tr-segment{position:absolute;top:0;height:100%;font-size:10px;overflow:hidden;white-space:nowrap;opacity:.85;padding:0 3px;box-sizing:border-box;color:#000}
3270
+ .tr-tick{position:absolute;top:0;bottom:0;width:1px;background:#fff6;text-decoration:none}.tr-playhead{position:absolute;top:0;bottom:0;width:2px;background:#fff;text-decoration:none}
3176
3271
  .tr-bar{height:7px;background:#2a2e36;border-radius:4px;overflow:hidden}
3177
3272
  .tr-bar i{display:block;height:100%;background:#4fc3f7;width:0}
3178
3273
  .tr-bar.hp i{background:#66bb6a}.tr-bar.warn i{background:#ffb300}.tr-bar.bad i{background:#ef5350}
@@ -3777,13 +3872,40 @@ function createTestRoom(options) {
3777
3872
  let simTime = 0;
3778
3873
  const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
3779
3874
  log.setClock(() => simTime);
3875
+ const ownAudio = options.audio === void 0;
3876
+ const audio = options.audio === false ? null : options.audio ?? createAudioEngine();
3877
+ const destructionSound = audio ? createDestructionSound(audio) : null;
3878
+ const playSound = (key, point, volume) => {
3879
+ destructionSound?.onSound(key, point, volume);
3880
+ options.onSound?.(key, point, volume);
3881
+ };
3882
+ const craterField = options.craters ? createGroundField() : void 0;
3883
+ const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
3884
+ if (craterPatch) {
3885
+ const floor = arena.group.getObjectByName("floor");
3886
+ if (floor) floor.visible = false;
3887
+ scene.add(craterPatch.mesh);
3888
+ }
3889
+ const vfx = createVfxRenderer(THREE, {
3890
+ scene,
3891
+ camera,
3892
+ overlay: container,
3893
+ quality,
3894
+ pointScale,
3895
+ vfx: createVfx({
3896
+ profile: quality,
3897
+ groundAt: (x, z) => groundAt(x, z) + (craterField?.heightAt(x, z) ?? 0),
3898
+ ...craterField ? { ground: craterField } : {}
3899
+ })
3900
+ });
3780
3901
  const renderer = createDestructionRenderer(THREE, {
3781
3902
  scene,
3782
3903
  world,
3904
+ vfx: vfx.world,
3783
3905
  quality,
3784
3906
  groundAt,
3785
3907
  pointScale,
3786
- onSound: options.onSound,
3908
+ onSound: playSound,
3787
3909
  onPiece: (piece) => {
3788
3910
  rendererPieces.set(piece.objectId, piece);
3789
3911
  loose.push(piece);
@@ -3805,7 +3927,17 @@ function createTestRoom(options) {
3805
3927
  if (best) splitOf.set(piece.objectId, best);
3806
3928
  }
3807
3929
  });
3808
- const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale });
3930
+ const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
3931
+ const filmRandom = createFilmLcg(1);
3932
+ const film = createFilmManager(THREE, {
3933
+ scene,
3934
+ camera,
3935
+ random: filmRandom,
3936
+ fluids,
3937
+ groundAt,
3938
+ ...typeof gl.copyTextureToTexture === "function" ? { gpu: gl } : {}
3939
+ });
3940
+ const filmBridge = createFilmBridge({ fluids, manager: film, random: filmRandom });
3809
3941
  const specs = /* @__PURE__ */ new Map();
3810
3942
  const loose = [];
3811
3943
  const arsenal = options.arsenal ?? createArsenal({ locale, weapons: options.weapons, seed: 1 });
@@ -3921,7 +4053,7 @@ function createTestRoom(options) {
3921
4053
  for (const ev of batch) {
3922
4054
  if (ev.type === "leak") puncture(ev);
3923
4055
  else if (ev.type === "ignite") igniteNear(ev.point, 0.9);
3924
- else if (ev.type === "explode") emitHost({ type: "explosion", source: ev.object, point: ev.point, energy: ev.energy, radius: ev.radius, cause: "part" });
4056
+ else if (ev.type === "explode") emitHost({ type: "explosion", source: ev.object, point: ev.point, energy: ev.energy, radius: ev.radius, cause: "part", ...ev.effect !== void 0 ? { explosive: ev.effect } : {} });
3925
4057
  fire("event", ev);
3926
4058
  options.onEvent?.(ev);
3927
4059
  }
@@ -4109,8 +4241,10 @@ function createTestRoom(options) {
4109
4241
  renderer.restore(e.so.objectId);
4110
4242
  }
4111
4243
  renderer.clear();
4244
+ vfx.world.clear();
4112
4245
  fluids.clearStains([0, 0, 0], 1e4);
4113
4246
  fluidView.clear();
4247
+ film.clear();
4114
4248
  fluids.setTap(TAP_ID, false);
4115
4249
  };
4116
4250
  const clear = () => {
@@ -4501,6 +4635,7 @@ function createTestRoom(options) {
4501
4635
  camera,
4502
4636
  world,
4503
4637
  renderer,
4638
+ effects: vfx.world,
4504
4639
  arsenal,
4505
4640
  objects: () => [...entries.values()].filter((e) => !e.arena).map((e) => e.so.root),
4506
4641
  bodies: crossingBodies,
@@ -4934,7 +5069,7 @@ function createTestRoom(options) {
4934
5069
  let metricsAcc = 0;
4935
5070
  let ownsLoop = false;
4936
5071
  const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
4937
- const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, ids: roomMetricIds });
5072
+ const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds });
4938
5073
  let qualityName = quality;
4939
5074
  const syncContainers = (sdt) => {
4940
5075
  for (const e of entries.values()) {
@@ -4983,12 +5118,20 @@ function createTestRoom(options) {
4983
5118
  }
4984
5119
  syncLiters();
4985
5120
  renderer.update(dt);
5121
+ vfx.update(dt);
5122
+ craterPatch?.sync();
4986
5123
  updateImpacts();
4987
5124
  fluidView.update(dt);
5125
+ filmBridge.update(dt);
5126
+ film.update(dt);
4988
5127
  for (const [id, s] of tools) {
4989
5128
  s.update(dt);
4990
5129
  flushEvents(id, s);
4991
5130
  }
5131
+ if (audio) {
5132
+ const look = camera.getWorldDirection(listenerDirection);
5133
+ audio.setListener({ position: [camera.position.x, camera.position.y, camera.position.z], yaw: Math.atan2(-look.x, -look.z) });
5134
+ }
4992
5135
  for (const update2 of frameListeners) update2(dt);
4993
5136
  for (const tr of tracers) {
4994
5137
  if (tr.life > 0) {
@@ -5098,7 +5241,7 @@ function createTestRoom(options) {
5098
5241
  const ev = evs[pl.fired++];
5099
5242
  action("animation_event", t("act.anim_event", { name: pl.entry.name, clip: pl.clip.id, type: ev.type }), { objectId: pl.entry.so.objectId, itemId: pl.entry.so.itemId, data: { clip: pl.clip.id, type: ev.type, at: ev.t, ...ev.data ?? {} } });
5100
5243
  const key = ev.type === "sound" ? ev.data?.key : void 0;
5101
- if (typeof key === "string") options.onSound?.(key, v3(pl.entry.so.root.getWorldPosition(tmpV)), 1);
5244
+ if (typeof key === "string") playSound(key, v3(pl.entry.so.root.getWorldPosition(tmpV)), 1);
5102
5245
  }
5103
5246
  if (pl.t >= pl.clip.duration) {
5104
5247
  playing.splice(i, 1);
@@ -5145,7 +5288,7 @@ function createTestRoom(options) {
5145
5288
  const benchView = () => {
5146
5289
  const store = activeBench ? benches.get(activeBench) : void 0;
5147
5290
  if (!store) return null;
5148
- return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers() };
5291
+ return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers(), sheet: store.sheet() };
5149
5292
  };
5150
5293
  const METRIC_UNAVAILABLE = "n/a";
5151
5294
  const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
@@ -5180,7 +5323,7 @@ function createTestRoom(options) {
5180
5323
  const module = declared.map((metric) => {
5181
5324
  const value = values[metric.id];
5182
5325
  const ratio = metric.max && typeof value === "number" ? value / metric.max : void 0;
5183
- return { id: metric.id, name: metric.name, text: metricText(value, metric.digits ?? 0, metric.unit), ...ratio === void 0 ? {} : { ratio } };
5326
+ return { id: metric.id, name: metric.name, text: metricText(value, metric.digits ?? 0, metric.unit), ...typeof value === "number" ? { value } : {}, ...ratio === void 0 ? {} : { ratio } };
5184
5327
  });
5185
5328
  return { room: room2, module };
5186
5329
  };
@@ -5191,6 +5334,9 @@ function createTestRoom(options) {
5191
5334
  };
5192
5335
  const hostListeners = /* @__PURE__ */ new Set();
5193
5336
  const frameListeners = /* @__PURE__ */ new Set();
5337
+ const listenerDirection = new THREE.Vector3();
5338
+ const environment = options.environment ?? createEnvironmentBus();
5339
+ const audioSink = audio ? createAudioSink(audio) : void 0;
5194
5340
  function emitHost(event) {
5195
5341
  for (const listener of hostListeners) listener(event);
5196
5342
  }
@@ -5240,7 +5386,10 @@ function createTestRoom(options) {
5240
5386
  scene,
5241
5387
  camera,
5242
5388
  locale,
5389
+ ...audioSink ? { audio: audioSink } : {},
5390
+ environment,
5243
5391
  groundAt,
5392
+ effects: vfx,
5244
5393
  tool: () => ({
5245
5394
  weapon: toolId,
5246
5395
  category: tool.def.category,
@@ -5294,6 +5443,7 @@ function createTestRoom(options) {
5294
5443
  layer: store.layer,
5295
5444
  onChange: store.onChange,
5296
5445
  patch: store.patch,
5446
+ sheet: store.setSheet,
5297
5447
  look: (eye, target) => room.look(eye, target)
5298
5448
  });
5299
5449
  const checkModuleBench = (mod, fail) => {
@@ -5491,6 +5641,8 @@ function createTestRoom(options) {
5491
5641
  arsenal,
5492
5642
  fluids,
5493
5643
  fluidRenderer: fluidView,
5644
+ audio,
5645
+ environment,
5494
5646
  log,
5495
5647
  get tool() {
5496
5648
  return tool;
@@ -5579,10 +5731,13 @@ function createTestRoom(options) {
5579
5731
  const id = moduleId ?? activeBench;
5580
5732
  return (id ? benches.get(id) : void 0) ?? null;
5581
5733
  },
5734
+ applyCamera: () => vfx.applyCamera(),
5582
5735
  metrics() {
5583
5736
  return { room: roomMetrics.sample(), module: moduleMetrics() };
5584
5737
  },
5585
5738
  dispose() {
5739
+ destructionSound?.dispose();
5740
+ if (ownAudio) audio?.dispose();
5586
5741
  for (const detach of detachers.values()) detach();
5587
5742
  detachers.clear();
5588
5743
  for (const store of benches.values()) store.dispose();
@@ -5592,7 +5747,11 @@ function createTestRoom(options) {
5592
5747
  player.dispose();
5593
5748
  ui?.dispose();
5594
5749
  renderer.dispose();
5750
+ vfx.dispose();
5751
+ craterPatch?.dispose();
5595
5752
  fluidView.dispose();
5753
+ filmBridge.dispose();
5754
+ film.dispose();
5596
5755
  for (const tr of tracers) tr.line.removeFromParent();
5597
5756
  explosives.clear();
5598
5757
  if (ownRenderer) {
@@ -5682,7 +5841,9 @@ function createTestRoom(options) {
5682
5841
  const dt = Math.min(0.05, realDelta);
5683
5842
  last = now;
5684
5843
  if (!scenarioRunning) update(dt);
5844
+ const restoreCamera = vfx.applyCamera();
5685
5845
  gl.render(scene, camera);
5846
+ restoreCamera();
5686
5847
  roomMetrics.rendered();
5687
5848
  };
5688
5849
  raf = requestAnimationFrame(loop);