incanto 0.70.0 → 0.72.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.
Files changed (74) hide show
  1. package/bin/incanto-editor.mjs +15 -9
  2. package/bin/incanto-feel.mjs +11 -1
  3. package/bin/incanto-play.mjs +11 -1
  4. package/bin/incanto-playtest.mjs +11 -1
  5. package/bin/incanto-verify.mjs +36 -98
  6. package/dist/2d.d.ts +18 -3
  7. package/dist/2d.js +3 -3
  8. package/dist/3d.d.ts +3 -3
  9. package/dist/3d.js +6 -6
  10. package/dist/{agent8-CmNF01gA.js → agent8-BqZ0_O1u.js} +1 -1
  11. package/dist/{audio-player-DaMxqfNE.d.ts → audio-player-ioZ7VKC6.d.ts} +1 -1
  12. package/dist/{behavior-DZExDn9o.d.ts → behavior-Bf9P9oB2.d.ts} +12 -1
  13. package/dist/{create-game-Bwvh6q8A.js → create-game-C9rWqqAC.js} +37 -13
  14. package/dist/{create-game-C7ffQWW7.js → create-game-CmFAtLZp.js} +8 -7
  15. package/dist/debug.d.ts +1 -1
  16. package/dist/editor.js +1083 -1045
  17. package/dist/{environment-presets-D6Q5BxeE.js → environment-presets-BeHoaGhy.js} +3 -3
  18. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  19. package/dist/{gameplay-BfHkuzVb.js → gameplay-BlQe-M07.js} +108 -6
  20. package/dist/gameplay.d.ts +68 -4
  21. package/dist/gameplay.js +2 -2
  22. package/dist/index.d.ts +27 -4
  23. package/dist/index.js +4 -4
  24. package/dist/{loader-Cff09LMm.d.ts → loader-CBfQzB6S.d.ts} +10 -3
  25. package/dist/net.d.ts +2 -2
  26. package/dist/net.js +2 -2
  27. package/dist/{physics-2d-CE0Qvy3V.js → physics-2d-DrIgTlLk.js} +2 -2
  28. package/dist/{physics-3d-CpH-2gn5.js → physics-3d-BzOCw1VS.js} +3 -3
  29. package/dist/{picking-CQJ_PJKh.js → picking-C9McVJrO.js} +3 -3
  30. package/dist/react.d.ts +1 -1
  31. package/dist/react.js +1 -1
  32. package/dist/{register-6DYnKZcy.js → register-5IYfooAN.js} +49 -15
  33. package/dist/{register-Bkk0wSDB.js → register-C8HuRkOf.js} +100 -8
  34. package/dist/{replay-Dmw-PKQu.d.ts → replay-CBEChqXq.d.ts} +2 -2
  35. package/dist/{replay-DjAkAzMq.js → replay-datiAE-b.js} +15 -8
  36. package/dist/{save-slots-BXVg148r.js → save-slots-CEuJPUle.js} +102 -4
  37. package/dist/{split-screen-PL78oVXP.js → split-screen-Bm5DTElG.js} +3 -3
  38. package/dist/{split-screen--k-XpBjr.d.ts → split-screen-DkqLng6q.d.ts} +2 -2
  39. package/dist/{src-Cxfiv1Hg.js → src-DRSsRleD.js} +1 -1
  40. package/dist/{test-iHYVUcDK.js → test-Djuq3Vhq.js} +252 -24
  41. package/dist/test.d.ts +113 -5
  42. package/dist/test.js +3 -3
  43. package/dist/vite.d.ts +8 -1
  44. package/dist/vite.js +23 -6
  45. package/editor/assets/{agent8-D0MS174y.js → agent8-CMKz2cax.js} +1 -1
  46. package/editor/assets/{debug-BnXkKuYu.js → debug-DLrQm-az.js} +1 -1
  47. package/editor/assets/{index-CIu3uc3l.js → index-BEKuxYcE.js} +94 -94
  48. package/editor/index.html +1 -1
  49. package/package.json +1 -1
  50. package/schemas/scene.schema.json +4 -0
  51. package/skills/incanto-behaviors-and-scripts.md +30 -9
  52. package/skills/incanto-building-2d-games.md +26 -2
  53. package/skills/incanto-editor.md +4 -0
  54. package/skills/incanto-gameplay-behaviors.md +63 -1
  55. package/skills/incanto-hud.md +14 -0
  56. package/skills/incanto-node-reference.md +18 -2
  57. package/skills/incanto-playtesting.md +34 -0
  58. package/skills/incanto-save-slots.md +56 -1
  59. package/skills/incanto-scene-json-authoring.md +30 -0
  60. package/skills/incanto-verifying-your-game.md +11 -0
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/beacon-isle-3d/src/game.scene.json +396 -386
  63. package/templates-app/molehill-2d/package.json +1 -1
  64. package/templates-app/molehill-2d/src/game.scene.json +10 -60
  65. package/templates-app/platformer-2d/package.json +1 -1
  66. package/templates-app/platformer-2d/src/game.scene.json +268 -504
  67. package/templates-app/star-survivor/package.json +1 -1
  68. package/templates-app/star-survivor/src/game.scene.json +23 -33
  69. package/templates-app/tps-3d/package.json +1 -1
  70. package/templates-app/tps-3d/src/game.scene.json +90 -105
  71. package/templates-app/village-quest-3d/package.json +1 -1
  72. package/templates-app/village-quest-3d/src/behaviors.ts +21 -4
  73. package/templates-app/village-quest-3d/src/grove.scene.json +99 -118
  74. package/templates-app/village-quest-3d/src/village.scene.json +449 -245
@@ -1,14 +1,14 @@
1
1
  import { n as jsonEquals, t as jsonClone } from "./json-CfTjpvW8.js";
2
- import { $ as StubBehavior, A as resetMemorySaves, E as resolveViewport, R as HudLayer, it as registerBehavior, p as loadScene, z as HudWidgetBase } from "./save-slots-BXVg148r.js";
3
- import { v as Engine } from "./register-6DYnKZcy.js";
2
+ import { B as HudLayer, M as resetMemorySaves, O as resolveViewport, V as HudWidgetBase, c as isCloned, h as loadScene, ot as registerBehavior, tt as StubBehavior } from "./save-slots-CEuJPUle.js";
3
+ import { v as Engine } from "./register-5IYfooAN.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { t as Rng } from "./rng-CDOMybym.js";
6
6
  import { c as getNodeSchema, d as mergeStaticProps, u as getNodeType } from "./touch-BnCyPA0G.js";
7
- import { n as startRecording } from "./replay-DjAkAzMq.js";
8
- import { Z as worldPosition, n as registerGameplayBehaviors } from "./gameplay-BfHkuzVb.js";
9
- import { t as registerNodes2D } from "./register-Bkk0wSDB.js";
10
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-D6Q5BxeE.js";
11
- import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-PL78oVXP.js";
7
+ import { n as startRecording } from "./replay-datiAE-b.js";
8
+ import { Q as worldPosition, n as registerGameplayBehaviors } from "./gameplay-BlQe-M07.js";
9
+ import { t as registerNodes2D } from "./register-C8HuRkOf.js";
10
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BeHoaGhy.js";
11
+ import { a as NetworkManager, c as readSyncKey, i as sanitizeName, n as registerNodesNet, r as NetworkSpawner, s as findOwnerNode, t as createSplitScreen } from "./split-screen-Bm5DTElG.js";
12
12
  import { Box3, Euler, Frustum, Matrix4, PerspectiveCamera, Quaternion, Ray, Vector3 } from "three";
13
13
  //#region src/core/asset-urls.ts
14
14
  /** Keys inside a `material` object that hold a texture URL. */
@@ -1102,6 +1102,18 @@ const REACH_2D_PX = 32;
1102
1102
  * a platformer that never wrote a physics header still falls, and reading
1103
1103
  * absent as zero would have stopped reporting the pit it fell into.
1104
1104
  */
1105
+ /** Is anything in this tree driving the player without a down? */
1106
+ function hasTopDownController(root) {
1107
+ const stack = [root];
1108
+ while (stack.length > 0) {
1109
+ const node = stack.pop();
1110
+ if (node.constructor.typeName === "CharacterController2D") {
1111
+ if (node.mode === "topDown") return true;
1112
+ }
1113
+ for (const child of node.children) stack.push(child);
1114
+ }
1115
+ return false;
1116
+ }
1105
1117
  function gravityMagnitude(physics) {
1106
1118
  const g = physics?.gravity;
1107
1119
  if (g === void 0) return 1;
@@ -1268,9 +1280,19 @@ var Driver = class {
1268
1280
  * way a player would: the world stopped moving.
1269
1281
  */
1270
1282
  frozeTheGame = /* @__PURE__ */ new Set();
1283
+ /**
1284
+ * Buttons an EARLIER run proved reload the scene, so this one never tries.
1285
+ *
1286
+ * Unlike a pause, a restart cannot be recovered from mid-run — the tree the
1287
+ * driver holds is gone — so the blame has to outlive the run that paid for
1288
+ * it. One run of the ten is spent learning; the other nine play the game.
1289
+ */
1290
+ avoid = /* @__PURE__ */ new Set();
1271
1291
  /** What was pressed most recently, so a freeze can be attributed to it. */
1272
1292
  lastPressed = null;
1273
1293
  moveAction;
1294
+ /** The button the scene's controller calls its jump — `jump` unless it says otherwise. */
1295
+ jumpButton = "jump";
1274
1296
  /**
1275
1297
  * The action vocabulary this driver was bound to, so it can notice a swap.
1276
1298
  *
@@ -1384,6 +1406,18 @@ var Driver = class {
1384
1406
  engine.toScreen ??= view.toScreen;
1385
1407
  }
1386
1408
  }
1409
+ if (root) {
1410
+ const stack = [root];
1411
+ while (stack.length > 0) {
1412
+ const node = stack.pop();
1413
+ if (CONTROLLERS.has(node.constructor.typeName ?? "")) {
1414
+ const want = node.jumpAction;
1415
+ if (typeof want === "string" && want !== "") this.jumpButton = want;
1416
+ break;
1417
+ }
1418
+ for (const child of node.children) stack.push(child);
1419
+ }
1420
+ }
1387
1421
  this.readsMouse = asks;
1388
1422
  const design = engine.scene?.viewport?.design;
1389
1423
  this.view = [design?.[0] ?? 960, design?.[1] ?? 540];
@@ -1434,6 +1468,14 @@ var Driver = class {
1434
1468
  paused() {
1435
1469
  return [...this.frozeTheGame];
1436
1470
  }
1471
+ /** Never press these — an earlier run watched them reload the scene. */
1472
+ avoidPressing(buttons) {
1473
+ this.avoid = buttons;
1474
+ }
1475
+ /** Whatever was pressed most recently, so a reload can be attributed to it. */
1476
+ blame() {
1477
+ return this.lastPressed;
1478
+ }
1437
1479
  step(dt, at) {
1438
1480
  this.rebind();
1439
1481
  if (this.engine.timeScale === 0) {
@@ -1459,7 +1501,7 @@ var Driver = class {
1459
1501
  }
1460
1502
  if (this.moveAction) this.engine.input.setActionVector(this.moveAction, Math.cos(this.heading), Math.sin(this.heading));
1461
1503
  for (const button of this.buttons) {
1462
- if (this.frozeTheGame.has(button)) continue;
1504
+ if (this.frozeTheGame.has(button) || this.avoid.has(button)) continue;
1463
1505
  const left = (this.held.get(button) ?? 0) - dt;
1464
1506
  if (left > 0) {
1465
1507
  this.held.set(button, left);
@@ -1470,7 +1512,7 @@ var Driver = class {
1470
1512
  this.held.set(button, -1);
1471
1513
  continue;
1472
1514
  }
1473
- const chance = button === "jump" ? .06 : .02;
1515
+ const chance = button === this.jumpButton ? .06 : .02;
1474
1516
  if (this.rand() < chance) {
1475
1517
  this.engine.input.pressAction(button);
1476
1518
  this.actionPresses += 1;
@@ -1514,7 +1556,7 @@ var Driver = class {
1514
1556
  if (widgets.length === 0) return;
1515
1557
  const target = widgets[Math.floor(this.rand() * widgets.length)];
1516
1558
  if (!target) return;
1517
- target._activate(0);
1559
+ target._activate(this.rand() < .5 ? 0 : this.rand() < .5 ? -1 : 1);
1518
1560
  this.presses += 1;
1519
1561
  }
1520
1562
  /** Every widget in the tree that can be picked up, or dropped onto. */
@@ -1678,7 +1720,7 @@ var Driver = class {
1678
1720
  if (this.moveAction) this.engine.input.setActionVector(this.moveAction, 0, 0);
1679
1721
  }
1680
1722
  };
1681
- async function runOnce(json, seed, opts) {
1723
+ async function runOnce(json, seed, opts, avoid = /* @__PURE__ */ new Set()) {
1682
1724
  const session = await createPlaySession(json, {
1683
1725
  seed,
1684
1726
  ...opts.behaviors ? { behaviors: opts.behaviors } : {},
@@ -1745,26 +1787,47 @@ async function runOnce(json, seed, opts) {
1745
1787
  ];
1746
1788
  const down = scene.dimension === "2d" ? 1 : -1;
1747
1789
  const pull = gravityMagnitude(scene.physics);
1748
- const floor = opts.fallBelow ?? (pull > 0 ? start[1] + down * (down > 0 ? FALL_2D_PX : 50) : null);
1790
+ const topDown = hasTopDownController(scene.root);
1791
+ const floor = opts.fallBelow ?? (pull > 0 && !topDown ? start[1] + down * (down > 0 ? FALL_2D_PX : 50) : null);
1749
1792
  const radius = opts.reachRadius ?? (down > 0 ? REACH_2D_PX : 2);
1750
1793
  const recorder = startRecording(engine);
1751
1794
  const botRng = new Rng((seed ^ 2654435769) >>> 0);
1752
1795
  const rand = () => botRng.next();
1753
1796
  const driver = new Driver(engine, rand);
1797
+ driver.avoidPressing(avoid);
1754
1798
  const reached = /* @__PURE__ */ new Set();
1755
1799
  const stepMs = 1e3 / 60;
1756
1800
  const deadline = (opts.seconds ?? 60) * 1e3;
1757
1801
  let outcome = null;
1758
1802
  let swappedAtMs = null;
1803
+ let restartedBy = null;
1759
1804
  const heardSoFar = /* @__PURE__ */ new Set();
1760
1805
  const shownSoFar = /* @__PURE__ */ new Set();
1761
- const offSwap = engine.sceneChanged.connect(() => {
1806
+ const playing = scene.source.name;
1807
+ const offSwap = engine.sceneChanged.connect((next) => {
1762
1808
  swappedAtMs ??= session.timeMs;
1809
+ if (next && next.source.name === playing) restartedBy ??= driver.blame();
1763
1810
  });
1764
1811
  let at = start;
1765
1812
  let wentAsFarAs = 0;
1766
1813
  const travelled = (a, b) => scene.dimension === "2d" ? Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[1] ?? 0) - (b[1] ?? 0)) : Math.hypot((a[0] ?? 0) - (b[0] ?? 0), (a[2] ?? 0) - (b[2] ?? 0));
1814
+ const cloneCensus = () => {
1815
+ const out = /* @__PURE__ */ new Map();
1816
+ const walk = (node) => {
1817
+ if (isCloned(node)) {
1818
+ const at2 = node.wiringPath();
1819
+ out.set(at2, (out.get(at2) ?? 0) + 1);
1820
+ return;
1821
+ }
1822
+ for (const child of node.children) walk(child);
1823
+ };
1824
+ const root = engine.scene?.root;
1825
+ if (root) walk(root);
1826
+ return out;
1827
+ };
1828
+ let halfway = null;
1767
1829
  while (session.timeMs < deadline) {
1830
+ if (halfway === null && session.timeMs >= deadline / 2) halfway = cloneCensus();
1768
1831
  at = player ? worldOf(player) : start;
1769
1832
  wentAsFarAs = Math.max(wentAsFarAs, travelled(at, start));
1770
1833
  driver.step(stepMs / 1e3, at);
@@ -1796,6 +1859,11 @@ async function runOnce(json, seed, opts) {
1796
1859
  }
1797
1860
  driver.release();
1798
1861
  offSwap();
1862
+ const aliveClones = [...cloneCensus()].map(([path, count]) => ({
1863
+ path,
1864
+ count,
1865
+ half: halfway?.get(path) ?? 0
1866
+ })).sort((a, b) => b.count - a.count);
1799
1867
  const WENT_NOWHERE = scene.dimension === "2d" ? STUCK_2D_PX : STUCK_3D_M;
1800
1868
  outcome ??= wentAsFarAs > WENT_NOWHERE || driver.clicksLanded > 0 || driver.dragsMade > 0 || driver.widgetsPressed > 0 || driver.slotsDragged > 0 ? "unfinished" : "stuck";
1801
1869
  for (const path of engine.audio.sources()) heardSoFar.add(path);
@@ -1812,6 +1880,7 @@ async function runOnce(json, seed, opts) {
1812
1880
  damageTaken: oracle.damage(),
1813
1881
  errors: engine.stats().errors,
1814
1882
  errorMessages: logLines(engine, "error"),
1883
+ aliveClones,
1815
1884
  warnings: logLines(engine, "warn"),
1816
1885
  replay: bad ? recorder.stop() : null,
1817
1886
  endedAt: at,
@@ -1836,6 +1905,7 @@ async function runOnce(json, seed, opts) {
1836
1905
  hazards,
1837
1906
  hasPlayer: player !== null,
1838
1907
  froze: driver.paused(),
1908
+ restartedBy,
1839
1909
  unread: unreadNow
1840
1910
  };
1841
1911
  }
@@ -1867,6 +1937,7 @@ async function playtest(json, opts = {}) {
1867
1937
  let hasPlayer = false;
1868
1938
  let hazards = 0;
1869
1939
  const froze = /* @__PURE__ */ new Set();
1940
+ const restarted = /* @__PURE__ */ new Set();
1870
1941
  /**
1871
1942
  * An action is inert only if NO run read it — one run that took a branch the
1872
1943
  * others missed is enough to clear it.
@@ -1874,7 +1945,7 @@ async function playtest(json, opts = {}) {
1874
1945
  let unread = null;
1875
1946
  for (let i = 0; i < count; i++) {
1876
1947
  resetMemorySaves();
1877
- const one = await runOnce(json, base + i, opts);
1948
+ const one = await runOnce(json, base + i, opts, restarted);
1878
1949
  runs.push(one.run);
1879
1950
  targets = one.targets;
1880
1951
  signals = one.signals;
@@ -1882,6 +1953,7 @@ async function playtest(json, opts = {}) {
1882
1953
  hasPlayer = hasPlayer || one.hasPlayer;
1883
1954
  hazards = one.hazards;
1884
1955
  for (const action of one.froze) froze.add(action);
1956
+ if (one.restartedBy) restarted.add(one.restartedBy);
1885
1957
  unread = unread === null ? one.unread : unread.filter((a) => one.unread.includes(a));
1886
1958
  }
1887
1959
  const probe = await createPlaySession(json, {
@@ -1930,6 +2002,7 @@ async function playtest(json, opts = {}) {
1930
2002
  hasPlayer,
1931
2003
  hazards,
1932
2004
  froze: [...froze],
2005
+ restarted: [...restarted],
1933
2006
  seconds: opts.seconds ?? 60,
1934
2007
  feedback: {
1935
2008
  declaredAudio: feedbackNodes.audio,
@@ -1997,6 +2070,33 @@ function playtestText(report) {
1997
2070
  for (const line of said.slice(1, 3)) lines.push(` ! ${line}`);
1998
2071
  if (said.length > 3) lines.push(` ! … and ${said.length - 3} more`);
1999
2072
  };
2073
+ /**
2074
+ * What the runs were still holding when they ended, by template.
2075
+ *
2076
+ * The MEDIAN across runs, because one run that happened to end mid-wave says
2077
+ * less than what every run ends with. Reported, never judged: `4 × Tower` is
2078
+ * furniture and `5 × Bolt` is a leak, and the engine cannot tell which of an
2079
+ * author's clones are meant to outlive the run — but the author can, in one
2080
+ * glance, which is exactly what was missing when a shipped game kept every
2081
+ * projectile that missed alive forever.
2082
+ */
2083
+ const pushAlive = () => {
2084
+ const worst = /* @__PURE__ */ new Map();
2085
+ for (const run of runs) for (const { path, count, half } of run.aliveClones ?? []) {
2086
+ const at = worst.get(path);
2087
+ if (!at || count > at.end) worst.set(path, {
2088
+ end: count,
2089
+ half
2090
+ });
2091
+ }
2092
+ const named = [...worst].filter(([, x]) => x.end >= 4).map(([path, x]) => ({
2093
+ path,
2094
+ ...x
2095
+ })).sort((a, b) => b.end - a.end);
2096
+ if (named.length === 0) return;
2097
+ const shown = named.slice(0, 3).map((x) => `${x.end} × ${x.path} (${x.half} at halfway)`);
2098
+ lines.push(` · made and still held: ${shown.join(", ")}` + (named.length > 3 ? ` (+${named.length - 3} more)` : "") + " — clones alive when the run ended, and how many there were at the halfway mark. Furniture holds steady; a leak climbs.");
2099
+ };
2000
2100
  const pushOutcomes = () => {
2001
2101
  for (const bad of [
2002
2102
  "error",
@@ -2041,6 +2141,8 @@ function playtestText(report) {
2041
2141
  pushOutcomes();
2042
2142
  pushWarnings();
2043
2143
  if (report.froze.length > 0) lines.push(` · the bot stopped pressing ${report.froze.map((x) => `\`${x}\``).join(", ")} — pressing it stopped the clock, and a run spent inside a pause menu measures nothing. Your game is fine; this is the bot getting out of its own way.`);
2144
+ if (report.restarted.length > 0) lines.push(` · the bot stopped pressing ${report.restarted.map((x) => `\`${x}\``).join(", ")} — pressing it reloaded the scene, which ends the run: the bot is bound to the tree it started in. One run paid for that; the rest played the game.`);
2145
+ pushAlive();
2044
2146
  return lines.join("\n");
2045
2147
  }
2046
2148
  pushOutcomes();
@@ -2069,6 +2171,11 @@ function playtestText(report) {
2069
2171
  lines.push("");
2070
2172
  lines.push(` · the bot stopped pressing ${report.froze.map((a) => `\`${a}\``).join(", ")} — pressing it stopped the clock, and a run spent inside a pause menu measures nothing. Your game is fine; this is the bot getting out of its own way.`);
2071
2173
  }
2174
+ if (report.restarted.length > 0) {
2175
+ lines.push("");
2176
+ lines.push(` · the bot stopped pressing ${report.restarted.map((a) => `\`${a}\``).join(", ")} — pressing it reloaded the scene, which ends the run: the bot is bound to the tree it started in. One run paid for that; the rest played the game.`);
2177
+ }
2178
+ pushAlive();
2072
2179
  if (report.inertActions.length > 0) {
2073
2180
  lines.push("");
2074
2181
  lines.push(` · declared and never read: ${report.inertActions.map((a) => `\`${a}\``).join(", ")} — the scene binds a key for it and no code asks whether it is pressed, so the control list promises something the game does not do. Read it, or drop it from \`input\`. (An action only polled on a screen no run reached is a false alarm.)`);
@@ -2646,8 +2753,12 @@ async function makeProbe(json, opts) {
2646
2753
  const player = findPlayer(session.scene.root);
2647
2754
  const declared = session.engine.input.declaredActions();
2648
2755
  const moveOptions = declared.filter((a) => a.type === "vector2").map((a) => a.name);
2649
- const move = opts.moveAction ?? moveOptions[0] ?? null;
2650
- const jump = opts.jumpAction ?? (declared.some((a) => a.name === "jump" && a.type === "button") ? "jump" : null);
2756
+ const named = (prop, type) => {
2757
+ const want = controllerProp(session.scene.root, prop);
2758
+ return want && declared.some((a) => a.name === want && a.type === type) ? want : null;
2759
+ };
2760
+ const move = opts.moveAction ?? named("moveAction", "vector2") ?? moveOptions[0] ?? null;
2761
+ const jump = opts.jumpAction ?? named("jumpAction", "button") ?? (declared.some((a) => a.name === "jump" && a.type === "button") ? "jump" : null);
2651
2762
  if (!player) {
2652
2763
  session.dispose();
2653
2764
  return {
@@ -3181,6 +3292,20 @@ function feelText(report) {
3181
3292
  }
3182
3293
  return lines.join("\n");
3183
3294
  }
3295
+ /** The action name a `CharacterController2D/3D` in this tree says it reads. */
3296
+ function controllerProp(root, prop) {
3297
+ const stack = [root];
3298
+ while (stack.length > 0) {
3299
+ const node = stack.pop();
3300
+ const type = node.constructor.typeName ?? "";
3301
+ if (type === "CharacterController2D" || type === "CharacterController3D") {
3302
+ const value = node[prop];
3303
+ return typeof value === "string" && value !== "" ? value : null;
3304
+ }
3305
+ for (const child of node.children) stack.push(child);
3306
+ }
3307
+ return null;
3308
+ }
3184
3309
  //#endregion
3185
3310
  //#region src/test/multiplay.ts
3186
3311
  /** Run a whole match headlessly and report what the clients ended up sharing. */
@@ -3264,8 +3389,8 @@ async function playMultiplayer(opts) {
3264
3389
  */
3265
3390
  async function joinLate(server, first, opts) {
3266
3391
  try {
3267
- const { Engine } = await import("./register-6DYnKZcy.js").then((n) => n.y);
3268
- const { loadScene } = await import("./save-slots-BXVg148r.js").then((n) => n.m);
3392
+ const { Engine } = await import("./register-5IYfooAN.js").then((n) => n.y);
3393
+ const { loadScene } = await import("./save-slots-CEuJPUle.js").then((n) => n.g);
3269
3394
  const { jsonClone } = await import("./json-CfTjpvW8.js").then((n) => n.i);
3270
3395
  const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
3271
3396
  engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
@@ -3466,6 +3591,106 @@ function saysRung(report) {
3466
3591
  ...warnings.length > 3 ? { fix: "the rest are in `incanto-logs`" } : {}
3467
3592
  };
3468
3593
  }
3594
+ /**
3595
+ * What to say when the bot could not WALK — which is not the same as could not
3596
+ * play.
3597
+ *
3598
+ * This branch was "nobody to move", said about games the bot had played for
3599
+ * eight runs. The MOUSE half was fixed when a flip puzzle whose every tile the
3600
+ * bot clicked was told to add a character controller; the KEYBOARD half was
3601
+ * found the same way, one genre along: a sokoban declares `move`, `undo` and
3602
+ * `restart`, a behaviour reads them every frame and moves plain nodes, and the
3603
+ * bot pressed them 989 times across eight runs — while the rung said
3604
+ *
3605
+ * ? plays — nothing here is drivable — 8 runs had nobody to move and
3606
+ * nothing to click
3607
+ *
3608
+ * Both halves of that sentence were false. A whole class — sokoban, snake,
3609
+ * tetris, roguelikes, typing games, anything on a grid — has no character to
3610
+ * add, and the advice was to add one anyway.
3611
+ *
3612
+ * PRESSED and READ, not merely declared: `inertActions` is the report's own
3613
+ * list of actions nothing asks about, and a scene where every one of them is
3614
+ * inert really does have nothing driving it.
3615
+ *
3616
+ * Pure, so the wording can be tested without running a game.
3617
+ */
3618
+ function unmeasuredPlays(report) {
3619
+ const runs = report.runs ?? [];
3620
+ const total = runs.length;
3621
+ const clicks = runs.reduce((n, r) => n + (r.clicks ?? 0), 0);
3622
+ const keyed = runs.reduce((n, r) => n + (r.actionPresses ?? 0), 0);
3623
+ const live = (report.actions ?? []).filter((a) => !(report.inertActions ?? []).includes(a));
3624
+ const ending = report.declaresWin ? "and no run reached the win" : "and no win to reach";
3625
+ if (clicks > 0) return {
3626
+ summary: `played with the MOUSE — ${clicks} click(s) across ${total} runs, ${ending}`,
3627
+ fix: "a mouse game is played, not walked — emit `won`/`lost` and this rung can pass"
3628
+ };
3629
+ if (keyed > 0 && live.length > 0) return {
3630
+ summary: `played with the KEYBOARD — ${keyed} press(es) of ${live.length} declared action(s) (${live.slice(0, 4).join(", ")}) across ${total} runs, ${ending}`,
3631
+ fix: "a game played by KEYS is played, not walked — emit `won`/`lost` and this rung can pass. (A random presser will not solve a puzzle: drive the solution in a scripted `runScript` harness, and let this rung tell you about crashes.)"
3632
+ };
3633
+ if (report.hasPlayer === false) return {
3634
+ summary: `nothing here is drivable — ${total} runs had nobody to move and nothing to click`,
3635
+ fix: "give the player a character controller, the `player` group, or the name Player — or `Clickable` on what the cursor drives"
3636
+ };
3637
+ return {
3638
+ summary: `${total} runs went nowhere, and your behaviours were not loaded — that is most of what moves a character`,
3639
+ fix: `name them: \`incanto-verify ${report.scene ?? "src/game.scene.json"} --behaviors src/behaviors.ts\``
3640
+ };
3641
+ }
3642
+ /**
3643
+ * What the run HEARD and SHOWED, as one rung.
3644
+ *
3645
+ * Pure, like `saysRung` and `unmeasuredPlays`: the bin owns running the
3646
+ * playtest, this owns what its feedback log means. Extracted when the
3647
+ * all-silent branch was found printing a COUNT with nothing beside it —
3648
+ * `racing-3d` reports 20 emitters and its scene declares none of them (they are
3649
+ * a pool of `new Particles3D()` built in `onReady`), so an author counted
3650
+ * twenty in a message and found zero in their JSON.
3651
+ */
3652
+ function feelsRung(fb, opts = {}) {
3653
+ if (!fb) return {
3654
+ name: "feels",
3655
+ status: "skipped",
3656
+ summary: "not run — the playtest did not report"
3657
+ };
3658
+ const declaredPaths = [...fb.declaredAudio ?? [], ...fb.declaredEffects ?? []];
3659
+ const declared = declaredPaths.length;
3660
+ const fired = new Set([...fb.heard ?? [], ...fb.shown ?? []]);
3661
+ if (declared === 0) return {
3662
+ name: "feels",
3663
+ status: "skipped",
3664
+ summary: "this game declares no sound and no effects"
3665
+ };
3666
+ const some = (list) => `${list.slice(0, 3).join(", ")}${list.length > 3 ? ` +${list.length - 3}` : ""}`;
3667
+ if (fired.size === 0) {
3668
+ if (opts.played === false) return {
3669
+ name: "feels",
3670
+ status: "unmeasured",
3671
+ summary: `${declared} sound/effect node(s) in the tree and none fired — but no run got the game moving, so nothing here was measured — ${some(declaredPaths)}`,
3672
+ fix: "drive it in a scripted run (`runScript` from `incanto/test`): a sustained throttle, a real fight, the actual verb. A random bot that never gets going triggers nothing, and that is a fact about the bot."
3673
+ };
3674
+ return {
3675
+ name: "feels",
3676
+ status: "fail",
3677
+ summary: `${declared} sound/effect node(s) in the tree, and not one fired in any run — ${some(declaredPaths)}`,
3678
+ fix: "connect them — a signal to `play` on an AudioPlayer, or `replay()` on a one-shot; `engine.audio.recent()` and `engine.effects.recent()` say what did fire"
3679
+ };
3680
+ }
3681
+ const silent = declaredPaths.filter((path) => !fired.has(path));
3682
+ if (silent.length === 0) return {
3683
+ name: "feels",
3684
+ status: "pass",
3685
+ summary: `every one of the ${declared} sound/effect nodes fired`
3686
+ };
3687
+ return {
3688
+ name: "feels",
3689
+ status: "unmeasured",
3690
+ summary: `${declared - silent.length} of ${declared} fired — silent: ${some(silent)}`,
3691
+ fix: "a silent emitter is either a broken wire or a moment random play never reached — the two look identical from here. Reach it in a scripted run (`runScript` from `incanto/test`), and this rung can tell you which."
3692
+ };
3693
+ }
3469
3694
  function ladderVerdict(rungs, ctx = {}) {
3470
3695
  const failed = rungs.find((r) => r.status === "fail");
3471
3696
  const unmeasured = rungs.filter((r) => r.status === "unmeasured");
@@ -3556,7 +3781,10 @@ function findFloatingProps(json, opts = {}) {
3556
3781
  const types = new Set(opts.types ?? GROUNDED_TYPES);
3557
3782
  let scene;
3558
3783
  try {
3559
- scene = loadScene(json, { stubMissingBehaviors: true });
3784
+ scene = loadScene(json, {
3785
+ stubMissingBehaviors: true,
3786
+ engine: new Engine({ scheduler: () => () => {} })
3787
+ });
3560
3788
  } catch (error) {
3561
3789
  throw new IncantoError("BAD_FORMAT", `findFloatingProps: the scene does not load, so nothing could be measured. ${error?.message ?? String(error)}`);
3562
3790
  }
@@ -3760,10 +3988,10 @@ async function runScript(json, opts) {
3760
3988
  if (opts.locale) engine.locale.locale = opts.locale;
3761
3989
  const physics = opts.physics ?? "auto";
3762
3990
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
3763
- const { enablePhysics2D } = await import("./physics-2d-CE0Qvy3V.js").then((n) => n.r);
3991
+ const { enablePhysics2D } = await import("./physics-2d-DrIgTlLk.js").then((n) => n.r);
3764
3992
  await enablePhysics2D(engine);
3765
3993
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
3766
- const { enablePhysics3D } = await import("./physics-3d-CpH-2gn5.js").then((n) => n.r);
3994
+ const { enablePhysics3D } = await import("./physics-3d-BzOCw1VS.js").then((n) => n.r);
3767
3995
  await enablePhysics3D(engine);
3768
3996
  }
3769
3997
  const failures = [];
@@ -3990,10 +4218,10 @@ async function createPlaySession(json, opts = {}) {
3990
4218
  engine.setScene(scene);
3991
4219
  const physics = opts.physics ?? "auto";
3992
4220
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
3993
- const { enablePhysics2D } = await import("./physics-2d-CE0Qvy3V.js").then((n) => n.r);
4221
+ const { enablePhysics2D } = await import("./physics-2d-DrIgTlLk.js").then((n) => n.r);
3994
4222
  await enablePhysics2D(engine);
3995
4223
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
3996
- const { enablePhysics3D } = await import("./physics-3d-CpH-2gn5.js").then((n) => n.r);
4224
+ const { enablePhysics3D } = await import("./physics-3d-BzOCw1VS.js").then((n) => n.r);
3997
4225
  await enablePhysics3D(engine);
3998
4226
  }
3999
4227
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -4033,4 +4261,4 @@ async function createPlaySession(json, opts = {}) {
4033
4261
  };
4034
4262
  }
4035
4263
  //#endregion
4036
- export { pointAtNode as A, failingReplays as C, playtestText as D, playtestFailed as E, framingText as M, sceneAssetUrls as N, geometricPicker as O, facingText as S, playtest as T, takeAgreementSnapshots as _, registerAllNodes as a, collectionProblems as b, firstLine as c, saysRung as d, multiplayProblems as f, feelText as g, feelReport as h, findFloatingProps as i, describeFraming as j, geometricView as k, ladderText as l, playMultiplayer as m, createPlaySession as n, runScript as o, multiplayText as p, describeCapture as r, validateScene as s, captureScene as t, ladderVerdict as u, takeCollectionSnapshots as v, findPlayer as w, facingReport as x, agreementProblems as y };
4264
+ export { geometricPicker as A, facingReport as C, playtest as D, findPlayer as E, sceneAssetUrls as F, pointAtNode as M, describeFraming as N, playtestFailed as O, framingText as P, collectionProblems as S, failingReplays as T, feelReport as _, registerAllNodes as a, takeCollectionSnapshots as b, feelsRung as c, ladderVerdict as d, saysRung as f, playMultiplayer as g, multiplayText as h, findFloatingProps as i, geometricView as j, playtestText as k, firstLine as l, multiplayProblems as m, createPlaySession as n, runScript as o, unmeasuredPlays as p, describeCapture as r, validateScene as s, captureScene as t, ladderText as u, feelText as v, facingText as w, agreementProblems as x, takeAgreementSnapshots as y };
package/dist/test.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { P as Scene$1, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-DZExDn9o.js";
1
+ import { P as Scene$1, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-Bf9P9oB2.js";
2
2
  import { a as SceneJson, c as JsonValue, s as JsonObject } from "./rng-Bb-IutXB.js";
3
- import { t as LoadSceneOptions } from "./loader-Cff09LMm.js";
4
- import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-Dmw-PKQu.js";
5
- import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen--k-XpBjr.js";
3
+ import { t as LoadSceneOptions } from "./loader-CBfQzB6S.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-CBEChqXq.js";
5
+ import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-DkqLng6q.js";
6
6
  import { Matrix4, Vector3 } from "three";
7
7
 
8
8
  //#region src/core/asset-urls.d.ts
@@ -639,6 +639,24 @@ interface PlaytestRun {
639
639
  * said `✗ error in 2/2` and nothing an author could act on.
640
640
  */
641
641
  errorMessages: string[];
642
+ /**
643
+ * What this run MADE and was still holding when it ended, by template.
644
+ *
645
+ * A shipped tower defense kept every projectile that MISSED alive forever —
646
+ * its `Lifetime` had been left asleep on the prefab shelf — and no instrument
647
+ * said so: the playtest reported the game as fine, the audit passed, the
648
+ * harness passed, every rung was green. Total node count is too blunt to be
649
+ * the answer, because a tower defense legitimately ENDS with four towers
650
+ * standing and a survivor with a swarm. Grouped by the AUTHORED template
651
+ * (`Node.wiringPath`), the two read differently at a glance: `4 × Tower` is
652
+ * furniture and `5 × Bolt` is a leak, and only the author can tell them
653
+ * apart — so this is reported and never judged.
654
+ */
655
+ aliveClones: {
656
+ path: string;
657
+ count: number;
658
+ half: number;
659
+ }[];
642
660
  /** Only for a run worth watching — replay it with `incanto-play`. */
643
661
  replay: ReplayJson | null;
644
662
  /** Where the player ended up, for the fall report. */
@@ -753,6 +771,23 @@ interface PlaytestReport {
753
771
  */
754
772
  froze: string[];
755
773
  /**
774
+ * Actions the bot stopped pressing because they STARTED THE LEVEL OVER.
775
+ *
776
+ * The sibling of `froze`, and it hid behind it: a run inside a pause menu
777
+ * measures nothing, and a run that reloads the scene measures nothing EITHER
778
+ * — except that one is over, because the bot is bound to the tree it began
779
+ * in. Measured on one losable scene, ten runs: `lost 10/10` without a
780
+ * mid-play restart, `lost 0/10 · swapped 10/10` with one, and the rung read
781
+ * `? 10 runs played without reaching a win (10 changed scene)`.
782
+ *
783
+ * Mid-run retry is ordinary — puzzle, racing, tower defense — and the
784
+ * engine's own `GameFlow` restarts only from a terminal state, so the games
785
+ * this ruined are the ones that wired `restartScene` themselves, exactly as
786
+ * the skills tell them to. Observed, never read off the word "restart": the
787
+ * scene being played was replaced by that same scene.
788
+ */
789
+ restarted: string[];
790
+ /**
756
791
  * Did `findPlayer` find anyone to drive?
757
792
  *
758
793
  * `findPlayer`'s own comment says a scene it cannot drive "says so rather
@@ -941,6 +976,79 @@ declare function saysRung(report: {
941
976
  errors?: number;
942
977
  };
943
978
  } | null): RungResult;
979
+ /**
980
+ * What to say when the bot could not WALK — which is not the same as could not
981
+ * play.
982
+ *
983
+ * This branch was "nobody to move", said about games the bot had played for
984
+ * eight runs. The MOUSE half was fixed when a flip puzzle whose every tile the
985
+ * bot clicked was told to add a character controller; the KEYBOARD half was
986
+ * found the same way, one genre along: a sokoban declares `move`, `undo` and
987
+ * `restart`, a behaviour reads them every frame and moves plain nodes, and the
988
+ * bot pressed them 989 times across eight runs — while the rung said
989
+ *
990
+ * ? plays — nothing here is drivable — 8 runs had nobody to move and
991
+ * nothing to click
992
+ *
993
+ * Both halves of that sentence were false. A whole class — sokoban, snake,
994
+ * tetris, roguelikes, typing games, anything on a grid — has no character to
995
+ * add, and the advice was to add one anyway.
996
+ *
997
+ * PRESSED and READ, not merely declared: `inertActions` is the report's own
998
+ * list of actions nothing asks about, and a scene where every one of them is
999
+ * inert really does have nothing driving it.
1000
+ *
1001
+ * Pure, so the wording can be tested without running a game.
1002
+ */
1003
+ declare function unmeasuredPlays(report: {
1004
+ runs?: readonly {
1005
+ clicks?: number;
1006
+ actionPresses?: number;
1007
+ }[];
1008
+ actions?: readonly string[];
1009
+ inertActions?: readonly string[];
1010
+ hasPlayer?: boolean;
1011
+ declaresWin?: boolean;
1012
+ behaviorsLoaded?: boolean;
1013
+ scene?: string;
1014
+ }): {
1015
+ summary: string;
1016
+ fix: string;
1017
+ };
1018
+ /**
1019
+ * What the run HEARD and SHOWED, as one rung.
1020
+ *
1021
+ * Pure, like `saysRung` and `unmeasuredPlays`: the bin owns running the
1022
+ * playtest, this owns what its feedback log means. Extracted when the
1023
+ * all-silent branch was found printing a COUNT with nothing beside it —
1024
+ * `racing-3d` reports 20 emitters and its scene declares none of them (they are
1025
+ * a pool of `new Particles3D()` built in `onReady`), so an author counted
1026
+ * twenty in a message and found zero in their JSON.
1027
+ */
1028
+ declare function feelsRung(fb: {
1029
+ declaredAudio?: readonly string[];
1030
+ declaredEffects?: readonly string[];
1031
+ heard?: readonly string[];
1032
+ shown?: readonly string[];
1033
+ } | null | undefined, opts?: {
1034
+ /**
1035
+ * Did the bot get the game MOVING at all?
1036
+ *
1037
+ * Zero emitters firing is a broken wire only if something was played. On
1038
+ * `racing-3d` — 20 pooled `Particles3D`, a car that needs a SUSTAINED
1039
+ * throttle — a random presser never gets it rolling, every run comes back
1040
+ * `stuck`, and this rung printed `✗ 20 … and not one fired in any run`
1041
+ * about a game whose emitters all fire the moment anyone drives it
1042
+ * (measured: hold throttle + steer for 12s, all 20 fire).
1043
+ *
1044
+ * The partial case beside it has always said the two are indistinguishable
1045
+ * — "either a broken wire or a moment random play never reached" — and the
1046
+ * zero case has exactly the same ambiguity when nothing was driven. Same
1047
+ * flag the `plays` rung uses, so the two never disagree about whether the
1048
+ * bot played.
1049
+ */
1050
+ played?: boolean;
1051
+ }): RungResult;
944
1052
  declare function ladderVerdict(rungs: RungResult[], ctx?: LadderContext): LadderVerdict;
945
1053
  /** The ladder as a person or an agent reads it: every rung, then the one action. */
946
1054
  declare function ladderText(v: LadderVerdict): string;
@@ -1201,4 +1309,4 @@ interface PlaySession {
1201
1309
  */
1202
1310
  declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
1203
1311
  //#endregion
1204
- export { type AgreementSnapshot, type CollectionSnapshot, type Disagreement, type DisagreementKind, type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, type LadderVerdict, type MultiplayOptions, type MultiplayPlayer, type MultiplayReport, NodeCapture, type Outcome, type PickerOptions, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, type SceneAssetUrl, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, agreementProblems, auditScene, captureScene, collectionProblems, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, firstLine, framingText, geometricPicker, geometricView, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestFailed, playtestText, pointAtNode, registerAllNodes, runScript, saysRung, sceneAssetUrls, takeAgreementSnapshots, takeCollectionSnapshots, validateScene };
1312
+ export { type AgreementSnapshot, type CollectionSnapshot, type Disagreement, type DisagreementKind, type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, type LadderVerdict, type MultiplayOptions, type MultiplayPlayer, type MultiplayReport, NodeCapture, type Outcome, type PickerOptions, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, type SceneAssetUrl, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, agreementProblems, auditScene, captureScene, collectionProblems, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, feelsRung, findFloatingProps, findPlayer, firstLine, framingText, geometricPicker, geometricView, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestFailed, playtestText, pointAtNode, registerAllNodes, runScript, saysRung, sceneAssetUrls, takeAgreementSnapshots, takeCollectionSnapshots, unmeasuredPlays, validateScene };
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
- import { i as instancedScenes, r as auditScene } from "./replay-DjAkAzMq.js";
2
- import { A as pointAtNode, C as failingReplays, D as playtestText, E as playtestFailed, M as framingText, N as sceneAssetUrls, O as geometricPicker, S as facingText, T as playtest, _ as takeAgreementSnapshots, a as registerAllNodes, b as collectionProblems, c as firstLine, d as saysRung, f as multiplayProblems, g as feelText, h as feelReport, i as findFloatingProps, j as describeFraming, k as geometricView, l as ladderText, m as playMultiplayer, n as createPlaySession, o as runScript, p as multiplayText, r as describeCapture, s as validateScene, t as captureScene, u as ladderVerdict, v as takeCollectionSnapshots, w as findPlayer, x as facingReport, y as agreementProblems } from "./test-iHYVUcDK.js";
3
- export { agreementProblems, auditScene, captureScene, collectionProblems, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, firstLine, framingText, geometricPicker, geometricView, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestFailed, playtestText, pointAtNode, registerAllNodes, runScript, saysRung, sceneAssetUrls, takeAgreementSnapshots, takeCollectionSnapshots, validateScene };
1
+ import { i as instancedScenes, r as auditScene } from "./replay-datiAE-b.js";
2
+ import { A as geometricPicker, C as facingReport, D as playtest, E as findPlayer, F as sceneAssetUrls, M as pointAtNode, N as describeFraming, O as playtestFailed, P as framingText, S as collectionProblems, T as failingReplays, _ as feelReport, a as registerAllNodes, b as takeCollectionSnapshots, c as feelsRung, d as ladderVerdict, f as saysRung, g as playMultiplayer, h as multiplayText, i as findFloatingProps, j as geometricView, k as playtestText, l as firstLine, m as multiplayProblems, n as createPlaySession, o as runScript, p as unmeasuredPlays, r as describeCapture, s as validateScene, t as captureScene, u as ladderText, v as feelText, w as facingText, x as agreementProblems, y as takeAgreementSnapshots } from "./test-Djuq3Vhq.js";
3
+ export { agreementProblems, auditScene, captureScene, collectionProblems, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, feelsRung, findFloatingProps, findPlayer, firstLine, framingText, geometricPicker, geometricView, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestFailed, playtestText, pointAtNode, registerAllNodes, runScript, saysRung, sceneAssetUrls, takeAgreementSnapshots, takeCollectionSnapshots, unmeasuredPlays, validateScene };