@adatechnology/game-test-room 0.6.0 → 0.7.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
@@ -2,8 +2,10 @@
2
2
  import { createDestruction, JOINT_DEFAULTS as JOINT_DEFAULTS2, resolveStrings as resolveDestructionStrings } from "@adatechnology/game-destruction";
3
3
  import { createDestructionRenderer, specFromObject3D } from "@adatechnology/game-destruction-three";
4
4
  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";
5
+ import { createFilmLcg, createFluids, createItemUse, ITEM_USES, resolveStrings as resolveFluidStrings } from "@adatechnology/game-fluids";
6
+ import { createFilmBridge, createFilmManager, createFluidRenderer } from "@adatechnology/game-fluids-three";
7
+ import { createGroundField, createVfx } from "@adatechnology/game-vfx";
8
+ import { createGroundPatch, createVfxRenderer } from "@adatechnology/game-vfx-three";
7
9
  import { checkAnimation, checkBench, checkDestructible, checkSpec } from "@adatechnology/game-contracts";
8
10
 
9
11
  // src/items.ts
@@ -1255,6 +1257,7 @@ function createBenchStore(module, locale, onListenerError) {
1255
1257
  const changed = () => {
1256
1258
  for (const watcher of watchers) watcher();
1257
1259
  };
1260
+ let sheet = null;
1258
1261
  const controlOf = (id) => controls.find((control) => control.id === id);
1259
1262
  const store = {
1260
1263
  moduleId: module.id,
@@ -1296,6 +1299,11 @@ function createBenchStore(module, locale, onListenerError) {
1296
1299
  watchers.add(listener);
1297
1300
  return () => watchers.delete(listener);
1298
1301
  },
1302
+ sheet: () => sheet,
1303
+ setSheet(next) {
1304
+ sheet = next;
1305
+ changed();
1306
+ },
1299
1307
  patch(id, patch) {
1300
1308
  const index = controls.findIndex((control2) => control2.id === id);
1301
1309
  if (index < 0) return;
@@ -1517,7 +1525,10 @@ function createExplosives(context) {
1517
1525
  const weaponName = context.toolName(projectile.weapon);
1518
1526
  const reach = Math.max(detonation.injuryRadius, detonation.lethalRadius);
1519
1527
  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);
1528
+ if (detonation.energy > 0) {
1529
+ const isPlayed = context.effects.explosion(detonation.explosive, detonation.point, { direction: detonation.direction }) !== null;
1530
+ renderer.explode(detonation.point, detonation.energy, reach, { visual: !isPlayed });
1531
+ }
1521
1532
  if (detonation.stunSeconds !== void 0) renderer.emit("flash", detonation.point, [0, 1, 0], 1, 1);
1522
1533
  const onPlayer = blastEffect(detonation, toArray(camera.position));
1523
1534
  action("explode", translate("act.explode", { weapon: weaponName, energy: format(detonation.energy, 0), radius: format(reach, 1), n: reached }), {
@@ -1526,7 +1537,7 @@ function createExplosives(context) {
1526
1537
  if (onPlayer.zone !== "none") {
1527
1538
  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
1539
  }
1529
- context.emitHost({ type: "explosion", source: projectile.weapon, point: detonation.point, energy: detonation.energy, radius: reach, cause: detonation.cause });
1540
+ context.emitHost({ type: "explosion", source: projectile.weapon, point: detonation.point, energy: detonation.energy, radius: reach, cause: detonation.cause, explosive: detonation.explosive });
1530
1541
  if (detonation.fire) context.startFire(projectile.weapon, detonation.point, detonation.fire, detonation.cause === "impact");
1531
1542
  };
1532
1543
  const detonateBare = (projectile, isImpact) => {
@@ -1853,9 +1864,10 @@ function createRoomMetrics(sources) {
1853
1864
  if (!["objects", "fragments", "particles", "decals"].some((id) => wanted.has(id))) return out;
1854
1865
  const destruction = sources.destruction.stats();
1855
1866
  const fluids = sources.fluids.stats();
1867
+ const effects = sources.effects.stats();
1856
1868
  put("objects", destruction.pieces);
1857
1869
  put("fragments", destruction.fragments);
1858
- put("particles", destruction.particles + fluids.particles);
1870
+ put("particles", destruction.particles + fluids.particles + effects.puffs + effects.sparks + effects.glows + effects.chunks);
1859
1871
  put("decals", destruction.decals + fluids.stains);
1860
1872
  return out;
1861
1873
  }
@@ -2955,7 +2967,7 @@ var mk = (tag, cls, text) => {
2955
2967
  if (text !== void 0) e.textContent = text;
2956
2968
  return e;
2957
2969
  };
2958
- var shapeOf = (view) => JSON.stringify([view.title, view.controls.map((c) => ({ ...c, value: void 0 })), view.layers.map((l) => [l.id, l.name])]);
2970
+ 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
2971
  function createBenchPanel(o) {
2960
2972
  const { t } = o;
2961
2973
  const element = mk("div", "tr-panel tr-bench");
@@ -3010,7 +3022,8 @@ function createBenchPanel(o) {
3010
3022
  const choiceControl = (control) => {
3011
3023
  const wrap = mk("div", "tr-ctl");
3012
3024
  wrap.appendChild(mk("span", void 0, control.name));
3013
- if (control.options.length <= SEGMENTED_MAX) {
3025
+ const grouped = control.options.some((option) => option.group !== void 0);
3026
+ if (control.options.length <= SEGMENTED_MAX && !grouped) {
3014
3027
  const group = mk("div", "tr-seg");
3015
3028
  group.setAttribute("role", "group");
3016
3029
  const buttons = control.options.map((option) => {
@@ -3033,10 +3046,22 @@ function createBenchPanel(o) {
3033
3046
  }
3034
3047
  const select = mk("select");
3035
3048
  select.disabled = control.disabled;
3049
+ let heading;
3050
+ let section = select;
3036
3051
  for (const option of control.options) {
3052
+ if (option.group !== heading) {
3053
+ heading = option.group;
3054
+ section = select;
3055
+ if (heading !== void 0) {
3056
+ const optgroup = mk("optgroup");
3057
+ optgroup.label = heading;
3058
+ select.appendChild(optgroup);
3059
+ section = optgroup;
3060
+ }
3061
+ }
3037
3062
  const op = mk("option", void 0, option.name);
3038
3063
  op.value = option.id;
3039
- select.appendChild(op);
3064
+ section.appendChild(op);
3040
3065
  }
3041
3066
  select.onchange = () => o.onControl(control.id, select.value);
3042
3067
  wrap.appendChild(select);
@@ -3074,7 +3099,63 @@ function createBenchPanel(o) {
3074
3099
  }
3075
3100
  return wrap;
3076
3101
  };
3102
+ let playhead = null;
3103
+ const sheetCard = (sheet) => {
3104
+ const card = mk("div", "tr-sheet");
3105
+ if (sheet.kicker) card.appendChild(mk("div", "sub", sheet.kicker));
3106
+ card.appendChild(mk("h4", void 0, sheet.title));
3107
+ if (sheet.body) card.appendChild(mk("p", void 0, sheet.body));
3108
+ if (sheet.facts?.length) {
3109
+ const facts = mk("dl", "tr-facts");
3110
+ for (const fact of sheet.facts) facts.append(mk("dt", void 0, fact.label), mk("dd", void 0, fact.value));
3111
+ card.appendChild(facts);
3112
+ }
3113
+ if (sheet.steps?.length) {
3114
+ const steps = mk("ol", "tr-steps");
3115
+ for (const step of sheet.steps) {
3116
+ const li = mk("li");
3117
+ li.append(mk("b", void 0, step.at), document.createTextNode(` ${step.text}`));
3118
+ steps.appendChild(li);
3119
+ }
3120
+ card.appendChild(steps);
3121
+ }
3122
+ if (sheet.swatches?.length) {
3123
+ const row = mk("div", "tr-swatches");
3124
+ for (const swatch of sheet.swatches) {
3125
+ const chip = mk("span", "tr-swatch");
3126
+ const dot = mk("i");
3127
+ dot.style.background = swatch.color;
3128
+ chip.append(dot, document.createTextNode(swatch.name));
3129
+ row.appendChild(chip);
3130
+ }
3131
+ card.appendChild(row);
3132
+ }
3133
+ if (sheet.timeline) {
3134
+ const { segments, ticks, metric } = sheet.timeline;
3135
+ const end = Math.max(1e-3, ...segments.map((segment) => segment.end), ...ticks);
3136
+ const strip = mk("div", "tr-timeline");
3137
+ for (const segment of segments) {
3138
+ const bar = mk("div", "tr-segment", segment.name);
3139
+ bar.style.left = `${segment.start / end * 100}%`;
3140
+ bar.style.width = `${(segment.end - segment.start) / end * 100}%`;
3141
+ bar.style.background = segment.color;
3142
+ strip.appendChild(bar);
3143
+ }
3144
+ for (const tick of ticks) {
3145
+ const mark = mk("u", "tr-tick");
3146
+ mark.style.left = `${tick / end * 100}%`;
3147
+ strip.appendChild(mark);
3148
+ }
3149
+ const head = mk("s", "tr-playhead");
3150
+ strip.appendChild(head);
3151
+ playhead = { node: head, metric, end };
3152
+ card.appendChild(strip);
3153
+ }
3154
+ if (sheet.note) card.appendChild(mk("p", "sub", sheet.note));
3155
+ return card;
3156
+ };
3077
3157
  const rebuild = (view) => {
3158
+ playhead = null;
3078
3159
  body.textContent = "";
3079
3160
  refreshers.length = 0;
3080
3161
  body.appendChild(mk("div", "sub", view.title));
@@ -3082,6 +3163,7 @@ function createBenchPanel(o) {
3082
3163
  for (const control of view.controls) if (!control.hidden) list.appendChild(controlElement(control));
3083
3164
  body.appendChild(list);
3084
3165
  if (view.layers.length) body.appendChild(layerList(view.layers));
3166
+ if (view.sheet) body.appendChild(sheetCard(view.sheet));
3085
3167
  };
3086
3168
  const renderMetrics = (rows) => {
3087
3169
  metricList.textContent = "";
@@ -3112,6 +3194,7 @@ function createBenchPanel(o) {
3112
3194
  hasBench = view !== null;
3113
3195
  syncDisplay();
3114
3196
  if (!view) {
3197
+ playhead = null;
3115
3198
  shape = "";
3116
3199
  body.textContent = "";
3117
3200
  refreshers.length = 0;
@@ -3141,6 +3224,10 @@ function createBenchPanel(o) {
3141
3224
  node.text.textContent = row.text;
3142
3225
  if (node.bar && row.ratio !== void 0) fillBar(node.bar, row.ratio);
3143
3226
  }
3227
+ if (playhead) {
3228
+ const at = rows.module.find((row) => row.id === playhead?.metric)?.value;
3229
+ playhead.node.style.left = `${Math.max(0, Math.min(1, (at ?? 0) / playhead.end)) * 100}%`;
3230
+ }
3144
3231
  },
3145
3232
  get visible() {
3146
3233
  if (element.style.display === "none") return false;
@@ -3173,6 +3260,13 @@ var CSS = `
3173
3260
  .tr-status{margin-top:4px;min-height:16px;color:#ffd23f;text-shadow:0 1px 2px #000}
3174
3261
  .tr-prog{margin:4px auto 0;width:260px;background:rgba(10,12,16,.72);border-radius:6px;padding:4px 8px;display:none}
3175
3262
  .tr-prog b{display:block;font-weight:500;margin-bottom:3px}
3263
+ .tr-sheet{margin-top:8px;padding:8px;border:1px solid #2a2e36;border-radius:6px;font-size:12px;line-height:1.4}
3264
+ .tr-sheet h4{margin:2px 0 4px;font-size:13px}.tr-sheet p{margin:4px 0}
3265
+ .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}
3266
+ .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}
3267
+ .tr-timeline{position:relative;height:22px;margin:8px 0;background:#1b1e24;border-radius:4px;overflow:hidden}
3268
+ .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}
3269
+ .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
3270
  .tr-bar{height:7px;background:#2a2e36;border-radius:4px;overflow:hidden}
3177
3271
  .tr-bar i{display:block;height:100%;background:#4fc3f7;width:0}
3178
3272
  .tr-bar.hp i{background:#66bb6a}.tr-bar.warn i{background:#ffb300}.tr-bar.bad i{background:#ef5350}
@@ -3777,9 +3871,29 @@ function createTestRoom(options) {
3777
3871
  let simTime = 0;
3778
3872
  const log = createLog({ exportCancelled: () => t("log.export_cancelled") });
3779
3873
  log.setClock(() => simTime);
3874
+ const craterField = options.craters ? createGroundField() : void 0;
3875
+ const craterPatch = craterField ? createGroundPatch(THREE, { field: craterField, material: new THREE.MeshStandardMaterial({ color: 9342602, roughness: 0.95 }) }) : void 0;
3876
+ if (craterPatch) {
3877
+ const floor = arena.group.getObjectByName("floor");
3878
+ if (floor) floor.visible = false;
3879
+ scene.add(craterPatch.mesh);
3880
+ }
3881
+ const vfx = createVfxRenderer(THREE, {
3882
+ scene,
3883
+ camera,
3884
+ overlay: container,
3885
+ quality,
3886
+ pointScale,
3887
+ vfx: createVfx({
3888
+ profile: quality,
3889
+ groundAt: (x, z) => groundAt(x, z) + (craterField?.heightAt(x, z) ?? 0),
3890
+ ...craterField ? { ground: craterField } : {}
3891
+ })
3892
+ });
3780
3893
  const renderer = createDestructionRenderer(THREE, {
3781
3894
  scene,
3782
3895
  world,
3896
+ vfx: vfx.world,
3783
3897
  quality,
3784
3898
  groundAt,
3785
3899
  pointScale,
@@ -3805,7 +3919,17 @@ function createTestRoom(options) {
3805
3919
  if (best) splitOf.set(piece.objectId, best);
3806
3920
  }
3807
3921
  });
3808
- const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale });
3922
+ const fluidView = createFluidRenderer(THREE, { scene, fluids, quality, pointScale, vfx: vfx.world });
3923
+ const filmRandom = createFilmLcg(1);
3924
+ const film = createFilmManager(THREE, {
3925
+ scene,
3926
+ camera,
3927
+ random: filmRandom,
3928
+ fluids,
3929
+ groundAt,
3930
+ ...typeof gl.copyTextureToTexture === "function" ? { gpu: gl } : {}
3931
+ });
3932
+ const filmBridge = createFilmBridge({ fluids, manager: film, random: filmRandom });
3809
3933
  const specs = /* @__PURE__ */ new Map();
3810
3934
  const loose = [];
3811
3935
  const arsenal = options.arsenal ?? createArsenal({ locale, weapons: options.weapons, seed: 1 });
@@ -3921,7 +4045,7 @@ function createTestRoom(options) {
3921
4045
  for (const ev of batch) {
3922
4046
  if (ev.type === "leak") puncture(ev);
3923
4047
  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" });
4048
+ 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
4049
  fire("event", ev);
3926
4050
  options.onEvent?.(ev);
3927
4051
  }
@@ -4109,8 +4233,10 @@ function createTestRoom(options) {
4109
4233
  renderer.restore(e.so.objectId);
4110
4234
  }
4111
4235
  renderer.clear();
4236
+ vfx.world.clear();
4112
4237
  fluids.clearStains([0, 0, 0], 1e4);
4113
4238
  fluidView.clear();
4239
+ film.clear();
4114
4240
  fluids.setTap(TAP_ID, false);
4115
4241
  };
4116
4242
  const clear = () => {
@@ -4501,6 +4627,7 @@ function createTestRoom(options) {
4501
4627
  camera,
4502
4628
  world,
4503
4629
  renderer,
4630
+ effects: vfx.world,
4504
4631
  arsenal,
4505
4632
  objects: () => [...entries.values()].filter((e) => !e.arena).map((e) => e.so.root),
4506
4633
  bodies: crossingBodies,
@@ -4934,7 +5061,7 @@ function createTestRoom(options) {
4934
5061
  let metricsAcc = 0;
4935
5062
  let ownsLoop = false;
4936
5063
  const roomMetricIds = options.metrics === false ? [] : options.metrics ?? ROOM_METRICS;
4937
- const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, ids: roomMetricIds });
5064
+ const roomMetrics = createRoomMetrics({ gl, destruction: renderer, fluids: fluidView, effects: vfx.world, ids: roomMetricIds });
4938
5065
  let qualityName = quality;
4939
5066
  const syncContainers = (sdt) => {
4940
5067
  for (const e of entries.values()) {
@@ -4983,8 +5110,12 @@ function createTestRoom(options) {
4983
5110
  }
4984
5111
  syncLiters();
4985
5112
  renderer.update(dt);
5113
+ vfx.update(dt);
5114
+ craterPatch?.sync();
4986
5115
  updateImpacts();
4987
5116
  fluidView.update(dt);
5117
+ filmBridge.update(dt);
5118
+ film.update(dt);
4988
5119
  for (const [id, s] of tools) {
4989
5120
  s.update(dt);
4990
5121
  flushEvents(id, s);
@@ -5145,7 +5276,7 @@ function createTestRoom(options) {
5145
5276
  const benchView = () => {
5146
5277
  const store = activeBench ? benches.get(activeBench) : void 0;
5147
5278
  if (!store) return null;
5148
- return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers() };
5279
+ return { title: modules.get(store.moduleId)?.name ?? store.moduleId, controls: store.controls(), layers: store.layers(), sheet: store.sheet() };
5149
5280
  };
5150
5281
  const METRIC_UNAVAILABLE = "n/a";
5151
5282
  const metricText = (value, digits, unit) => typeof value === "number" ? `${num(value, digits)}${unit ? ` ${unit}` : ""}` : value ?? METRIC_UNAVAILABLE;
@@ -5180,7 +5311,7 @@ function createTestRoom(options) {
5180
5311
  const module = declared.map((metric) => {
5181
5312
  const value = values[metric.id];
5182
5313
  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 } };
5314
+ return { id: metric.id, name: metric.name, text: metricText(value, metric.digits ?? 0, metric.unit), ...typeof value === "number" ? { value } : {}, ...ratio === void 0 ? {} : { ratio } };
5184
5315
  });
5185
5316
  return { room: room2, module };
5186
5317
  };
@@ -5241,6 +5372,7 @@ function createTestRoom(options) {
5241
5372
  camera,
5242
5373
  locale,
5243
5374
  groundAt,
5375
+ effects: vfx,
5244
5376
  tool: () => ({
5245
5377
  weapon: toolId,
5246
5378
  category: tool.def.category,
@@ -5294,6 +5426,7 @@ function createTestRoom(options) {
5294
5426
  layer: store.layer,
5295
5427
  onChange: store.onChange,
5296
5428
  patch: store.patch,
5429
+ sheet: store.setSheet,
5297
5430
  look: (eye, target) => room.look(eye, target)
5298
5431
  });
5299
5432
  const checkModuleBench = (mod, fail) => {
@@ -5579,6 +5712,7 @@ function createTestRoom(options) {
5579
5712
  const id = moduleId ?? activeBench;
5580
5713
  return (id ? benches.get(id) : void 0) ?? null;
5581
5714
  },
5715
+ applyCamera: () => vfx.applyCamera(),
5582
5716
  metrics() {
5583
5717
  return { room: roomMetrics.sample(), module: moduleMetrics() };
5584
5718
  },
@@ -5592,7 +5726,11 @@ function createTestRoom(options) {
5592
5726
  player.dispose();
5593
5727
  ui?.dispose();
5594
5728
  renderer.dispose();
5729
+ vfx.dispose();
5730
+ craterPatch?.dispose();
5595
5731
  fluidView.dispose();
5732
+ filmBridge.dispose();
5733
+ film.dispose();
5596
5734
  for (const tr of tracers) tr.line.removeFromParent();
5597
5735
  explosives.clear();
5598
5736
  if (ownRenderer) {
@@ -5682,7 +5820,9 @@ function createTestRoom(options) {
5682
5820
  const dt = Math.min(0.05, realDelta);
5683
5821
  last = now;
5684
5822
  if (!scenarioRunning) update(dt);
5823
+ const restoreCamera = vfx.applyCamera();
5685
5824
  gl.render(scene, camera);
5825
+ restoreCamera();
5686
5826
  roomMetrics.rendered();
5687
5827
  };
5688
5828
  raf = requestAnimationFrame(loop);