incanto 0.65.0 → 0.67.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 (88) hide show
  1. package/bin/_behaviors-loader.mjs +87 -0
  2. package/bin/incanto-editor.mjs +39 -2
  3. package/bin/incanto-feel.mjs +4 -1
  4. package/bin/incanto-frame.mjs +52 -3
  5. package/bin/incanto-logs.mjs +33 -3
  6. package/bin/incanto-multiplay.mjs +50 -1
  7. package/bin/incanto-play.mjs +3 -1
  8. package/bin/incanto-playtest.mjs +3 -25
  9. package/bin/incanto-verify.mjs +15 -2
  10. package/dist/2d.d.ts +76 -4
  11. package/dist/2d.js +3 -3
  12. package/dist/3d.d.ts +213 -14
  13. package/dist/3d.js +6 -6
  14. package/dist/{audio-player-BrEHwbK1.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
  15. package/dist/{behavior-CmMuQ6CL.d.ts → behavior-ZnfzKDo3.d.ts} +58 -1
  16. package/dist/{create-game-DIyh5Zy9.js → create-game-DMetd3vR.js} +14 -8
  17. package/dist/{create-game-W2xzTJhs.js → create-game-FAXdHta3.js} +138 -11
  18. package/dist/debug.d.ts +1 -1
  19. package/dist/debug.js +3 -2
  20. package/dist/{duplicate-CqSAtdrh.js → duplicate-B8QnX2-r.js} +1 -1
  21. package/dist/editor.js +7 -2
  22. package/dist/{environment-presets-BvHs6HEk.js → environment-presets-C-5g2pMh.js} +113 -9
  23. package/dist/{gameplay-D-H986A4.js → gameplay-CGSv-9XR.js} +295 -12
  24. package/dist/gameplay.d.ts +102 -6
  25. package/dist/gameplay.js +2 -2
  26. package/dist/index.d.ts +50 -6
  27. package/dist/index.js +7 -6
  28. package/dist/{loader-CTB441z4.d.ts → loader-DyT30ArB.d.ts} +1 -1
  29. package/dist/{loader-lQDCwNag.js → loader-KZESOZmE.js} +148 -1
  30. package/dist/net.d.ts +2 -2
  31. package/dist/net.js +1 -1
  32. package/dist/{physics-2d-CmnunaR2.js → physics-2d-_B6zj3zU.js} +57 -4
  33. package/dist/{physics-3d-DULB1lD1.js → physics-3d-i1IeAY0F.js} +74 -5
  34. package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
  35. package/dist/react.d.ts +1 -1
  36. package/dist/react.js +1 -1
  37. package/dist/{register-CgeR4ckF.js → register-Cs53CY7s.js} +110 -16
  38. package/dist/{register-DO5_Qp0Q.js → register-vIxU3Xbv.js} +38 -4
  39. package/dist/rendering-options-ByAdi8PF.js +184 -0
  40. package/dist/{replay-CEw2xAhG.js → replay-BkpDV51O.js} +4 -3
  41. package/dist/{replay-CThvwkcQ.d.ts → replay-D0GcEHYU.d.ts} +1 -1
  42. package/dist/{split-screen-DxZzC1nm.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
  43. package/dist/{split-screen-gb0c3xdb.js → split-screen-DS8RaUV_.js} +2 -2
  44. package/dist/{src-DYffzHbd.js → src-BLlsGJW9.js} +1 -1
  45. package/dist/{teardown-BordHL9o.js → teardown-LB9cTnXJ.js} +2 -2
  46. package/dist/{test-CXZuXs8k.js → test-81uZ967a.js} +127 -24
  47. package/dist/test.d.ts +7 -5
  48. package/dist/test.js +2 -2
  49. package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
  50. package/dist/vite.d.ts +0 -7
  51. package/dist/vite.js +13 -3
  52. package/editor/assets/{agent8-DO5qzIOD.js → agent8-C2jtNKbx.js} +1 -1
  53. package/editor/assets/debug-DoU3KeqS.js +3 -0
  54. package/editor/assets/{index-CqEUVaPN.js → index-CNqHojcC.js} +92 -92
  55. package/editor/index.html +1 -1
  56. package/package.json +1 -1
  57. package/schemas/scene.schema.json +20 -1
  58. package/skills/incanto-building-3d-games.md +19 -0
  59. package/skills/incanto-editor.md +5 -1
  60. package/skills/incanto-environment.md +2 -2
  61. package/skills/incanto-gameplay-behaviors.md +86 -7
  62. package/skills/incanto-hud.md +14 -1
  63. package/skills/incanto-multiplayer.md +14 -0
  64. package/skills/incanto-node-reference.md +60 -2
  65. package/skills/incanto-performance.md +35 -0
  66. package/skills/incanto-physics-and-input.md +69 -0
  67. package/skills/incanto-playtesting.md +15 -1
  68. package/skills/incanto-save-slots.md +21 -1
  69. package/skills/incanto-verifying-your-game.md +19 -0
  70. package/skills/incanto-web-integration.md +24 -3
  71. package/skills/incanto-your-first-game.md +4 -4
  72. package/templates-app/beacon-isle-3d/package.json +1 -1
  73. package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/game.scene.json +99 -53
  76. package/templates-app/star-survivor/package.json +1 -1
  77. package/templates-app/star-survivor/src/game.scene.json +31 -17
  78. package/templates-app/tps-3d/package.json +1 -1
  79. package/templates-app/tps-3d/src/game.scene.json +14 -8
  80. package/templates-app/village-quest-3d/package.json +1 -1
  81. package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
  82. package/templates-app/village-quest-3d/src/village.scene.json +10 -7
  83. package/editor/assets/debug-DntG9CVD.js +0 -3
  84. package/templates-app/beacon-isle-3d/coverage.json +0 -9
  85. package/templates-app/platformer-2d/coverage.json +0 -5
  86. package/templates-app/star-survivor/coverage.json +0 -5
  87. package/templates-app/tps-3d/coverage.json +0 -5
  88. package/templates-app/village-quest-3d/coverage.json +0 -9
@@ -1,13 +1,13 @@
1
- import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-lQDCwNag.js";
2
- import { v as Engine } from "./register-CgeR4ckF.js";
1
+ import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-KZESOZmE.js";
2
+ import { v as Engine } from "./register-Cs53CY7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-CF70EArN.js";
6
- import { n as startRecording } from "./replay-CEw2xAhG.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-D-H986A4.js";
8
- import { t as registerNodes2D } from "./register-DO5_Qp0Q.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BvHs6HEk.js";
10
- 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-gb0c3xdb.js";
6
+ import { n as startRecording } from "./replay-BkpDV51O.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CGSv-9XR.js";
8
+ import { t as registerNodes2D } from "./register-vIxU3Xbv.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-C-5g2pMh.js";
10
+ 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-DS8RaUV_.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
13
13
  /**
@@ -632,12 +632,49 @@ var Driver = class {
632
632
  buttons;
633
633
  moveAction;
634
634
  held = /* @__PURE__ */ new Map();
635
+ /**
636
+ * Everything in the scene a click can reach.
637
+ *
638
+ * The bot drove declared ACTIONS and nothing else, so on a mouse game it could
639
+ * only ever watch. Measured across one round: a turn-based tactics game, a
640
+ * tower defense and a duel all reported the identical
641
+ * `? plays — 8 runs played without reaching a win`, and nobody filed it
642
+ * because the message is gracious about it. A whole class — tactics, RTS,
643
+ * tower defense, point-and-click, board games — was structurally unmeasurable
644
+ * by the rung that exists to answer "does this game work".
645
+ *
646
+ * Headless there is no raycast, so a picker is installed that answers from the
647
+ * TREE — the exact recipe `incanto-physics-and-input.md` gives for testing a
648
+ * mouse game — and the click goes through the real `Clickable`, the real
649
+ * `mouseJustPressed` edge, and the real connection. Nothing is faked but the
650
+ * cursor's arrival.
651
+ */
652
+ clickables;
653
+ clickCooldown = 0;
654
+ clicking = null;
655
+ /** press -> release -> let go. `clicked` fires on RELEASE, so the picker must
656
+ * still be answering with the same node on the frame the release is read. */
657
+ clickPhase = "idle";
635
658
  constructor(engine, rand) {
636
659
  this.engine = engine;
637
660
  this.rand = rand;
638
661
  const declared = engine.input.declaredActions();
639
662
  this.moveAction = declared.find((a) => a.type === "vector2")?.name ?? null;
640
663
  this.buttons = declared.filter((a) => a.type === "button").map((a) => a.name);
664
+ this.clickables = [];
665
+ const root = engine.scene?.root;
666
+ if (root) {
667
+ const walk = (node) => {
668
+ if (node.behavior?.constructor.name === "Clickable") this.clickables.push(node);
669
+ for (const child of node.children) walk(child);
670
+ };
671
+ walk(root);
672
+ if (this.clickables.length > 0 && !engine.picker) engine.picker = () => this.clicking;
673
+ }
674
+ }
675
+ /** How many nodes this bot can click. Reported, so a ✓ says what it drove. */
676
+ get clickableCount() {
677
+ return this.clickables.length;
641
678
  }
642
679
  /** Grid cell key — coarse on purpose, this is novelty not navigation. */
643
680
  cell(p) {
@@ -668,6 +705,36 @@ var Driver = class {
668
705
  this.held.set(button, .08 + this.rand() * .12);
669
706
  }
670
707
  }
708
+ this.maybeClick(dt);
709
+ }
710
+ /**
711
+ * Press and release over one Clickable, occasionally.
712
+ *
713
+ * `clicked` fires on RELEASE over the same node the press started on, so this
714
+ * has to hold the target across two frames — which is also what makes it a
715
+ * real click rather than a synthetic signal.
716
+ */
717
+ maybeClick(dt) {
718
+ if (this.clickables.length === 0) return;
719
+ this.engine.input.setPointerPosition(1, 1);
720
+ if (this.clickPhase === "pressed") {
721
+ this.engine.input.handleMouseButton(0, false);
722
+ this.clickPhase = "released";
723
+ return;
724
+ }
725
+ if (this.clickPhase === "released") {
726
+ this.clicking = null;
727
+ this.clickPhase = "idle";
728
+ return;
729
+ }
730
+ this.clickCooldown -= dt;
731
+ if (this.clickCooldown > 0) return;
732
+ this.clickCooldown = .25 + this.rand() * .6;
733
+ const target = this.clickables[Math.floor(this.rand() * this.clickables.length)];
734
+ if (!target || target.tree === null) return;
735
+ this.clicking = target;
736
+ this.clickPhase = "pressed";
737
+ this.engine.input.handleMouseButton(0, true);
671
738
  }
672
739
  /** Prefer the least-visited neighbouring cell; ties break randomly. */
673
740
  pickHeading(at) {
@@ -724,6 +791,7 @@ async function runOnce(json, seed, opts) {
724
791
  for (const child of node.children) walkAll(child);
725
792
  };
726
793
  walkAll(scene.root);
794
+ const hazards = countHazardsIn(json);
727
795
  const targetNodes = [];
728
796
  const collect = (node) => {
729
797
  if (node !== player && isTarget(node)) targetNodes.push([node.getPath(), node]);
@@ -746,6 +814,10 @@ async function runOnce(json, seed, opts) {
746
814
  const stepMs = 1e3 / 60;
747
815
  const deadline = (opts.seconds ?? 60) * 1e3;
748
816
  let outcome = null;
817
+ let swappedAtMs = null;
818
+ const offSwap = engine.sceneChanged.connect(() => {
819
+ swappedAtMs ??= session.timeMs;
820
+ });
749
821
  let at = start;
750
822
  let wentAsFarAs = 0;
751
823
  while (session.timeMs < deadline) {
@@ -771,8 +843,13 @@ async function runOnce(json, seed, opts) {
771
843
  outcome = "fell";
772
844
  break;
773
845
  }
846
+ if (swappedAtMs !== null) {
847
+ outcome = "swapped";
848
+ break;
849
+ }
774
850
  }
775
851
  driver.release();
852
+ offSwap();
776
853
  const WENT_NOWHERE = scene.dimension === "2d" ? STUCK_2D_PX : STUCK_3D_M;
777
854
  outcome ??= wentAsFarAs > WENT_NOWHERE ? "unfinished" : "stuck";
778
855
  const heard = engine.audio.recent().map((e) => e.from);
@@ -800,9 +877,27 @@ async function runOnce(json, seed, opts) {
800
877
  targets,
801
878
  signals,
802
879
  declaresEnd: oracle.declaresEnd(),
880
+ hazards,
803
881
  hasPlayer: player !== null
804
882
  };
805
883
  }
884
+ /**
885
+ * How many nodes the scene FILE wires to hurt the player.
886
+ *
887
+ * Read from the JSON, not the tree: a `Spawner` detaches its prefab at ready, so
888
+ * a wave game's hazards exist only in the file until the first wave lands.
889
+ */
890
+ function countHazardsIn(json) {
891
+ let n = 0;
892
+ const walk = (node) => {
893
+ if (typeof node !== "object" || node === null) return;
894
+ const nd = node;
895
+ if (nd.script?.name === "DamageOnContact") n += 1;
896
+ if (Array.isArray(nd.children)) for (const child of nd.children) walk(child);
897
+ };
898
+ walk(json?.root);
899
+ return n;
900
+ }
806
901
  /** Play the scene `runs` times and report what happened. */
807
902
  async function playtest(json, opts = {}) {
808
903
  const count = Math.max(1, opts.runs ?? 20);
@@ -812,6 +907,7 @@ async function playtest(json, opts = {}) {
812
907
  let signals = [];
813
908
  let declaresWin = false;
814
909
  let hasPlayer = false;
910
+ let hazards = 0;
815
911
  for (let i = 0; i < count; i++) {
816
912
  const one = await runOnce(json, base + i, opts);
817
913
  runs.push(one.run);
@@ -819,6 +915,7 @@ async function playtest(json, opts = {}) {
819
915
  signals = one.signals;
820
916
  declaresWin = declaresWin || one.declaresEnd;
821
917
  hasPlayer = hasPlayer || one.hasPlayer;
918
+ hazards = one.hazards;
822
919
  }
823
920
  const probe = await createPlaySession(json, {
824
921
  seed: base,
@@ -848,6 +945,7 @@ async function playtest(json, opts = {}) {
848
945
  inertActions: [],
849
946
  declaresWin,
850
947
  hasPlayer,
948
+ hazards,
851
949
  seconds: opts.seconds ?? 60,
852
950
  feedback: {
853
951
  declaredAudio: feedbackNodes.audio,
@@ -872,7 +970,7 @@ function median(values) {
872
970
  * of green is a wall nobody reads.
873
971
  */
874
972
  function playtestText(report) {
875
- const { runs, targets, signals } = report;
973
+ const { runs, targets, signals, hazards } = report;
876
974
  const total = runs.length;
877
975
  const lines = [];
878
976
  lines.push(`${total} runs × up to ${report.seconds}s`);
@@ -889,18 +987,21 @@ function playtestText(report) {
889
987
  "fell",
890
988
  "lost",
891
989
  "stuck",
892
- "unfinished"
990
+ "unfinished",
991
+ "swapped"
893
992
  ]) {
894
993
  const hits = runs.filter((r) => r.outcome === bad);
895
994
  if (hits.length === 0) continue;
896
995
  const where = bad === "fell" ? ` (last at y=${hits[0].endedAt[1].toFixed(1)})` : "";
897
- const mark = bad === "unfinished" ? "·" : "✗";
898
- const what = bad === "unfinished" ? "ran out of time still playing" : `${bad} in ${pct(hits.length, total)}`;
899
- lines.push(bad === "unfinished" ? ` ${mark} ${pct(hits.length, total)} ${what}` : ` ${mark} ${what}${where}`);
996
+ const mark = bad === "unfinished" || bad === "swapped" ? "·" : "✗";
997
+ const what = bad === "unfinished" ? "ran out of time still playing" : bad === "swapped" ? "the game changed scene — this bot is bound to the tree it started in, so nothing after the swap was measured (not a defect)" : `${bad} in ${pct(hits.length, total)}`;
998
+ lines.push(bad === "unfinished" || bad === "swapped" ? ` ${mark} ${pct(hits.length, total)} ${what}` : ` ${mark} ${what}${where}`);
900
999
  }
901
1000
  const everReached = new Set(runs.flatMap((r) => r.reached));
902
1001
  const missed = targets.filter((path) => !everReached.has(path));
903
- if (missed.length > 0) {
1002
+ const anySwapped = runs.some((r) => r.outcome === "swapped");
1003
+ if (missed.length > 0 && anySwapped) lines.push(` · ${missed.length} destination(s) unmeasured — a run changed scene and the bot stopped there. Judge these with a scripted run.`);
1004
+ else if (missed.length > 0) {
904
1005
  lines.push("");
905
1006
  lines.push(` never reached (${missed.length} of ${targets.length}):`);
906
1007
  for (const path of missed.slice(0, 12)) lines.push(` ${path}`);
@@ -916,7 +1017,9 @@ function playtestText(report) {
916
1017
  }
917
1018
  const damage = runs.reduce((n, r) => n + r.damageTaken, 0);
918
1019
  lines.push("");
919
- lines.push(damage === 0 ? ` danger: the player took damage 0 times in ${total} runs — nothing here can hurt you` : ` danger: ${damage} hits across ${total} runs (${(damage / total).toFixed(1)} per run)`);
1020
+ if (damage > 0) lines.push(` danger: ${damage} hits across ${total} runs (${(damage / total).toFixed(1)} per run)`);
1021
+ else if (hazards === 0) lines.push(" danger: nothing in this scene can hurt the player — no DamageOnContact, no hazard group. A game with no way to lose is a walkabout.");
1022
+ else lines.push(` danger: ${hazards} hazard(s) are wired and none of them landed a hit in ${total} runs. They may be unreachable — or the bot may simply have outrun them, which a real player would not. Judge it with a scripted run.`);
920
1023
  return lines.join("\n");
921
1024
  }
922
1025
  /** The failing runs worth saving, newest problem first. */
@@ -1709,8 +1812,8 @@ async function playMultiplayer(opts) {
1709
1812
  */
1710
1813
  async function joinLate(server, first, opts) {
1711
1814
  try {
1712
- const { Engine } = await import("./register-CgeR4ckF.js").then((n) => n.y);
1713
- const { loadScene } = await import("./loader-lQDCwNag.js").then((n) => n.r);
1815
+ const { Engine } = await import("./register-Cs53CY7s.js").then((n) => n.y);
1816
+ const { loadScene } = await import("./loader-KZESOZmE.js").then((n) => n.r);
1714
1817
  const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
1715
1818
  const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
1716
1819
  engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
@@ -1891,9 +1994,9 @@ function findFloatingProps(json, opts = {}) {
1891
1994
  const types = new Set(opts.types ?? GROUNDED_TYPES);
1892
1995
  let scene;
1893
1996
  try {
1894
- scene = loadScene(json);
1895
- } catch {
1896
- return [];
1997
+ scene = loadScene(json, { stubMissingBehaviors: true });
1998
+ } catch (error) {
1999
+ throw new IncantoError("BAD_FORMAT", `findFloatingProps: the scene does not load, so nothing could be measured. ${error?.message ?? String(error)}`);
1897
2000
  }
1898
2001
  let ground = null;
1899
2002
  const findGround = (node) => {
@@ -1901,7 +2004,7 @@ function findFloatingProps(json, opts = {}) {
1901
2004
  for (const child of node.children) findGround(child);
1902
2005
  };
1903
2006
  findGround(scene.root);
1904
- if (!ground) return [];
2007
+ if (!ground) throw new IncantoError("NODE_NOT_FOUND", "findFloatingProps: no Terrain3D in this scene, so there is no ground to measure against. This check reads terrain height; a level built from mesh floors needs a physics raycast instead.");
1905
2008
  const out = [];
1906
2009
  const walk = (node, path, ox, oy, oz, yaw, bodyOwned) => {
1907
2010
  const n = node;
@@ -2072,10 +2175,10 @@ async function runScript(json, opts) {
2072
2175
  if (opts.locale) engine.locale.locale = opts.locale;
2073
2176
  const physics = opts.physics ?? "auto";
2074
2177
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2075
- const { enablePhysics2D } = await import("./physics-2d-CmnunaR2.js").then((n) => n.r);
2178
+ const { enablePhysics2D } = await import("./physics-2d-_B6zj3zU.js").then((n) => n.r);
2076
2179
  await enablePhysics2D(engine);
2077
2180
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2078
- const { enablePhysics3D } = await import("./physics-3d-DULB1lD1.js").then((n) => n.r);
2181
+ const { enablePhysics3D } = await import("./physics-3d-i1IeAY0F.js").then((n) => n.r);
2079
2182
  await enablePhysics3D(engine);
2080
2183
  }
2081
2184
  const failures = [];
@@ -2188,10 +2291,10 @@ async function createPlaySession(json, opts = {}) {
2188
2291
  engine.setScene(scene);
2189
2292
  const physics = opts.physics ?? "auto";
2190
2293
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2191
- const { enablePhysics2D } = await import("./physics-2d-CmnunaR2.js").then((n) => n.r);
2294
+ const { enablePhysics2D } = await import("./physics-2d-_B6zj3zU.js").then((n) => n.r);
2192
2295
  await enablePhysics2D(engine);
2193
2296
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2194
- const { enablePhysics3D } = await import("./physics-3d-DULB1lD1.js").then((n) => n.r);
2297
+ const { enablePhysics3D } = await import("./physics-3d-i1IeAY0F.js").then((n) => n.r);
2195
2298
  await enablePhysics3D(engine);
2196
2299
  }
2197
2300
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
package/dist/test.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-CmMuQ6CL.js";
1
+ import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-ZnfzKDo3.js";
2
2
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
3
- import { t as LoadSceneOptions } from "./loader-CTB441z4.js";
4
- import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-CThvwkcQ.js";
5
- import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-DxZzC1nm.js";
3
+ import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson } from "./replay-D0GcEHYU.js";
5
+ import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-DD5eYIgW.js";
6
6
 
7
7
  //#region src/test/facing.d.ts
8
8
  interface FacingOptions {
@@ -295,7 +295,7 @@ declare function multiplayProblems(detail: MultiplayPlayer[], rooms: string[]):
295
295
  declare function multiplayText(report: MultiplayReport): string;
296
296
  //#endregion
297
297
  //#region src/test/playtest.d.ts
298
- type Outcome = "won" | "lost" | "fell" | "error" | "stuck" | "unfinished";
298
+ type Outcome = "won" | "lost" | "fell" | "error" | "stuck" | "unfinished" | "swapped";
299
299
  interface PlaytestOptions {
300
300
  /** How many independent runs. Each gets its own seed. Default 20. */
301
301
  runs?: number;
@@ -358,6 +358,8 @@ interface PlaytestReport {
358
358
  * author looking for a bug that was never there.
359
359
  */
360
360
  declaresWin: boolean;
361
+ /** How many nodes are wired to hurt the player — see the `danger:` line. */
362
+ hazards: number;
361
363
  /**
362
364
  * Did `findPlayer` find anyone to drive?
363
365
  *
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
- import { r as auditScene } from "./replay-CEw2xAhG.js";
2
- import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-CXZuXs8k.js";
1
+ import { r as auditScene } from "./replay-BkpDV51O.js";
2
+ import { C as describeFraming, S as playtestText, _ as facingReport, a as registerAllNodes, b as findPlayer, c as ladderText, d as multiplayText, f as playMultiplayer, g as agreementProblems, h as takeAgreementSnapshots, i as findFloatingProps, l as ladderVerdict, m as feelText, n as createPlaySession, o as runScript, p as feelReport, r as describeCapture, s as validateScene, t as captureScene, u as multiplayProblems, v as facingText, w as framingText, x as playtest, y as failingReplays } from "./test-81uZ967a.js";
3
3
  export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
@@ -1,186 +1,3 @@
1
- //#region src/core/drive-script.ts
2
- const num = (raw) => {
3
- if (raw === void 0) return null;
4
- const n = Number(raw);
5
- return Number.isFinite(n) ? n : null;
6
- };
7
- /**
8
- * `vector move 0 1; step 2000` → steps, or an error naming the bad command.
9
- *
10
- * Semicolons and newlines both separate, so a whole plan fits in one shell
11
- * argument and a longer one fits in a file. `#` starts a comment.
12
- *
13
- * A command it does not know REFUSES THE WHOLE SCRIPT. Skipping it would leave
14
- * an agent looking at a frame that never moved, believing it had walked
15
- * somewhere — the failure mode this exists to prevent.
16
- */
17
- function parseDrive(text) {
18
- const steps = [];
19
- let totalMs = 0;
20
- const lines = text.split(/[;\n]/).map((l) => l.replace(/#.*$/, "").trim()).filter(Boolean);
21
- for (const line of lines) {
22
- const [word, ...rest] = line.split(/\s+/);
23
- const bad = (why) => ({
24
- steps: [],
25
- error: `${why}: "${line}"`,
26
- totalMs: 0
27
- });
28
- switch (word) {
29
- case "press":
30
- case "release": {
31
- const action = rest[0];
32
- if (!action) return bad(`${word} needs an action name`);
33
- steps.push(word === "press" ? {
34
- kind: "press",
35
- action
36
- } : {
37
- kind: "release",
38
- action
39
- });
40
- break;
41
- }
42
- case "vector": {
43
- const action = rest[0];
44
- const x = num(rest[1]);
45
- const y = num(rest[2]);
46
- if (!action || x === null || y === null) return bad("vector needs an action and two numbers");
47
- steps.push({
48
- kind: "vector",
49
- action,
50
- x,
51
- y
52
- });
53
- break;
54
- }
55
- case "key": {
56
- const code = rest[0];
57
- const dir = rest[1];
58
- if (!code || dir !== "down" && dir !== "up") return bad("key needs a code and down|up");
59
- steps.push({
60
- kind: "key",
61
- code,
62
- down: dir === "down"
63
- });
64
- break;
65
- }
66
- case "pointer": {
67
- const dx = num(rest[0]);
68
- const dy = num(rest[1]);
69
- if (dx === null || dy === null) return bad("pointer needs two numbers");
70
- steps.push({
71
- kind: "pointer",
72
- dx,
73
- dy
74
- });
75
- break;
76
- }
77
- case "step": {
78
- const ms = num(rest[0]);
79
- if (ms === null || ms < 0) return bad("step needs a duration in ms");
80
- steps.push({
81
- kind: "step",
82
- ms
83
- });
84
- totalMs += ms;
85
- break;
86
- }
87
- default: return bad("not a command");
88
- }
89
- }
90
- return {
91
- steps,
92
- error: null,
93
- totalMs
94
- };
95
- }
96
- //#endregion
97
- //#region src/core/log-report.ts
98
- const RANK = {
99
- debug: 0,
100
- info: 1,
101
- warn: 2,
102
- error: 3
103
- };
104
- /** Under this, with the loop running, is a frame rate somebody can feel. */
105
- const SLOW_FPS = 50;
106
- /** How many distinct messages to carry. Beyond this it is a log file, not a report. */
107
- const MAX_GROUPS = 20;
108
- /** The engine logs values, not strings — format them the way a console would. */
109
- function format(parts) {
110
- return parts.map((p) => {
111
- if (typeof p === "string") return p;
112
- if (p instanceof Error) return p.message;
113
- try {
114
- return JSON.stringify(p);
115
- } catch {
116
- return String(p);
117
- }
118
- }).join(" ");
119
- }
120
- function logReport(input) {
121
- const counts = {
122
- debug: 0,
123
- info: 0,
124
- warn: 0,
125
- error: 0
126
- };
127
- const byMessage = /* @__PURE__ */ new Map();
128
- for (const e of input.entries) {
129
- counts[e.level] += 1;
130
- const message = format(e.parts);
131
- const key = `${e.level}\0${message}`;
132
- const seen = byMessage.get(key);
133
- if (seen) {
134
- seen.count += 1;
135
- seen.lastMs = Math.max(seen.lastMs, e.timeMs);
136
- } else byMessage.set(key, {
137
- level: e.level,
138
- message,
139
- count: 1,
140
- lastMs: e.timeMs
141
- });
142
- }
143
- const grouped = [...byMessage.values()].sort((a, b) => RANK[b.level] - RANK[a.level] || b.lastMs - a.lastMs).slice(0, MAX_GROUPS);
144
- const hidden = input.hidden === true;
145
- const slow = !hidden && input.stats.running && input.stats.fps > 0 && input.stats.fps < SLOW_FPS;
146
- return {
147
- hidden,
148
- ok: counts.error === 0 && input.stats.errors === 0 && input.assetErrors.length === 0,
149
- slow,
150
- counts,
151
- grouped,
152
- worst: grouped.find((g) => g.level === "error") ?? grouped[0] ?? null,
153
- stats: input.stats,
154
- assetErrors: input.assetErrors
155
- };
156
- }
157
- /** The report as a person or an agent reads it: what is wrong, then the numbers. */
158
- function logText(r) {
159
- const lines = [];
160
- if (r.stats.errors > 0) lines.push(`${r.stats.errors} error${r.stats.errors === 1 ? "" : "s"} were swallowed to keep the game alive — it is running, and something in it is broken`);
161
- for (const a of r.assetErrors) lines.push(`asset ${a.ref} never loaded: ${a.error} (${a.url})`);
162
- for (const g of r.grouped) {
163
- if (g.level === "debug") continue;
164
- lines.push(`[${g.level}] ${g.message}${g.count > 1 ? ` ×${g.count}` : ""}`);
165
- }
166
- if (lines.length === 0) lines.push("nothing wrong — no warnings, no errors, no failed assets");
167
- if (!r.stats.running) lines.push(`not running — the loop is stopped (${r.stats.nodes} nodes in the scene)`);
168
- else if (r.hidden) lines.push(`${r.stats.nodes} nodes · frame rate not measurable — the page is hidden (a window covered by another counts), and the browser throttles it. Bring it to the front`);
169
- else lines.push(`${Math.round(r.stats.fps)} fps · ${r.stats.frameMs.toFixed(1)}ms per frame · ${r.stats.nodes} nodes${r.slow ? " — slow enough to feel" : ""}`);
170
- return lines.join("\n");
171
- }
172
- //#endregion
173
- //#region src/core/rendering-options.ts
174
- function resolveRendering(environment, fallback, devicePixelRatio, explicit) {
175
- const rendering = environment?.rendering ?? {};
176
- const scenePixelRatio = rendering.pixelRatio === "device" ? devicePixelRatio : rendering.pixelRatio;
177
- return {
178
- antialias: explicit?.antialias ?? rendering.antialias ?? fallback.antialias,
179
- pixelRatio: explicit?.pixelRatio ?? scenePixelRatio ?? fallback.pixelRatio,
180
- preserveDrawingBuffer: explicit?.preserveDrawingBuffer ?? rendering.preserveDrawingBuffer ?? fallback.preserveDrawingBuffer
181
- };
182
- }
183
- //#endregion
184
1
  //#region src/core/touch.ts
185
2
  /** Drags inside this fraction of the radius read as neutral. */
186
3
  const DEADZONE = .08;
@@ -228,6 +45,33 @@ function attachTouchControls(engine, container, opts = {}) {
228
45
  function isCoarsePointer() {
229
46
  return typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches;
230
47
  }
48
+ /**
49
+ * Retarget moves and ups back to this element — and never at the cost of the press.
50
+ *
51
+ * `setPointerCapture` THROWS `NotFoundError` for a pointerId that is not
52
+ * currently active, and `?.` guards a missing method rather than a throwing
53
+ * one. It ran BEFORE `feed()` — the try/catch that would have absorbed it was
54
+ * the very next line — so a synthetic `pointerdown` lost its press entirely.
55
+ *
56
+ * Real fingers are unaffected: a real touch is an active pointer. What it broke
57
+ * was every `dispatchEvent`-based driver, which is how a touch game is tested
58
+ * at all. And the instrument agreed with the bug: happy-dom's
59
+ * `setPointerCapture` throws nothing, so a harness printed `ALL CHECKS PASSED`
60
+ * while Chrome lost the same game. Controlled A/B, one page, one autopilot, one
61
+ * variable:
62
+ *
63
+ * capture throws gameover @6.48s, 7/9 shards, 19 uncaught errors
64
+ * capture guarded WON @6.30s, 9/9 shards, 0 errors
65
+ *
66
+ * Worse than losing the press: `activePointer` is assigned first, so the
67
+ * matching `pointerup` still fired `releaseAction` — a release the state
68
+ * machine never earned.
69
+ */
70
+ function capturePointer(el, pointerId) {
71
+ try {
72
+ el.setPointerCapture?.(pointerId);
73
+ } catch {}
74
+ }
231
75
  var TouchControls = class {
232
76
  elements = [];
233
77
  detachers = [];
@@ -294,7 +138,7 @@ var TouchControls = class {
294
138
  cx = rect.left + rect.width / 2;
295
139
  cy = rect.top + rect.height / 2;
296
140
  radius = rect.width / 2;
297
- base.setPointerCapture?.(e.pointerId);
141
+ capturePointer(base, e.pointerId);
298
142
  apply(e.clientX, e.clientY);
299
143
  };
300
144
  const onMove = (e) => {
@@ -344,8 +188,8 @@ var TouchControls = class {
344
188
  let activePointer = null;
345
189
  const onDown = (e) => {
346
190
  activePointer = e.pointerId;
347
- button.setPointerCapture?.(e.pointerId);
348
191
  feed(() => engine.input.pressAction(action));
192
+ capturePointer(button, e.pointerId);
349
193
  button.style.background = "rgba(255,255,255,0.3)";
350
194
  };
351
195
  const release = (e) => {
@@ -378,4 +222,4 @@ function style(el, rules) {
378
222
  for (const [key, value] of Object.entries(rules)) el.style[key] = value;
379
223
  }
380
224
  //#endregion
381
- export { logReport as a, resolveRendering as i, attachTouchControls as n, logText as o, joystickVector as r, parseDrive as s, TouchControls as t };
225
+ export { joystickVector as i, attachTouchControls as n, capturePointer as r, TouchControls as t };
package/dist/vite.d.ts CHANGED
@@ -74,13 +74,6 @@ interface FrameEndpointOptions {
74
74
  */
75
75
  store?: Map<string, FrameSignature>;
76
76
  }
77
- /**
78
- * Wire `/__incanto/ping` and `/__incanto/frame` onto a dev server.
79
- *
80
- * `ping` is how the CLI tells our server apart from anything else listening in
81
- * the container: it enumerates the ports that are actually open and asks each
82
- * one. That answer has to be cheap and unmistakable.
83
- */
84
77
  declare function serveFrameEndpoints(host: FrameHost, version: string, opts?: FrameEndpointOptions): void;
85
78
  //#endregion
86
79
  //#region src/vite/index.d.ts
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as VERSION } from "./src-DYffzHbd.js";
1
+ import { t as VERSION } from "./src-BLlsGJW9.js";
2
2
  import { n as diffSignatures } from "./frame-report-BSMny7oe.js";
3
- import { s as validateScene } from "./test-CXZuXs8k.js";
3
+ import { s as validateScene } from "./test-81uZ967a.js";
4
4
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
5
5
  import { basename, dirname, join, normalize, relative, resolve, sep } from "node:path";
6
6
  //#region src/vite/frame-endpoint.ts
@@ -28,6 +28,14 @@ const DEFAULT_LABEL = "last";
28
28
  * the container: it enumerates the ports that are actually open and asks each
29
29
  * one. That answer has to be cheap and unmistakable.
30
30
  */
31
+ /** The consuming project's package name, for the ping's identity. */
32
+ function projectName(root) {
33
+ try {
34
+ return JSON.parse(readFileSync(join(root, "package.json"), "utf8")).name ?? "";
35
+ } catch {
36
+ return "";
37
+ }
38
+ }
31
39
  function serveFrameEndpoints(host, version, opts = {}) {
32
40
  const channels = [
33
41
  host.environments?.client?.hot,
@@ -65,7 +73,9 @@ function serveFrameEndpoints(host, version, opts = {}) {
65
73
  host.middlewares.use("/__incanto/ping", (_req, res) => {
66
74
  json$1(res, 200, {
67
75
  incanto: version,
68
- frame: Boolean(channel)
76
+ frame: Boolean(channel),
77
+ root: host.config?.root ?? process.cwd(),
78
+ project: projectName(host.config?.root ?? process.cwd())
69
79
  });
70
80
  });
71
81
  serveLogs(host, channels, waiting, () => nextId++, timeoutMs);
@@ -1 +1 @@
1
- import{n as e}from"./index-CqEUVaPN.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};
1
+ import{n as e}from"./index-CNqHojcC.js";async function t(t){return new n((await e(()=>import(`./GameServer-C56iOUgF.js`),[],import.meta.url)).GameServer,t)}var n=class{raw;active=new Map;reconnecting=!1;disposed=!1;constructor(e,t){this.raw=new e({...t})}get account(){return this.raw.account}get connected(){return this.raw.connected}connect(){return this.raw.connected?Promise.resolve(!0):(this.disposed=!1,this.rawConnect())}rawConnect(){return this.raw.connect({onDisconnect:()=>void this.reconnect()})}disconnect(){this.disposed=!0;for(let e of this.active.values())e.off();return this.active.clear(),this.raw.disconnect()}remoteFunction(e,t,n){return this.raw.remoteFunction(e,t,n)}track(e){let t=Symbol(`sub`),n={make:e,off:e()};return this.active.set(t,n),()=>{n.off(),this.active.delete(t)}}async reconnect(){if(!this.disposed&&!this.reconnecting){this.reconnecting=!0;try{await this.rawConnect();for(let e of this.active.values())e.off(),e.off=e.make()}finally{this.reconnecting=!1}}}subscribeRoomState(e,t){return this.track(()=>this.raw.subscribeRoomState(e,t))}subscribeRoomMyState(e,t){return this.track(()=>this.raw.subscribeRoomMyState(e,t))}subscribeRoomAllUserStates(e,t){return this.track(()=>this.raw.subscribeRoomAllUserStates(e,e=>{let n={};for(let t of e??[]){if(!t||typeof t.account!=`string`||t.__leaved)continue;let{account:e,__updated:r,__leaved:i,...a}=t;n[e]=a}t(n)}))}subscribeRoomCollection(e,t,n){return this.track(()=>this.raw.subscribeRoomCollection(e,t,({items:e})=>{let t={};for(let n of e??[])n&&typeof n.__id==`string`&&(t[n.__id]=n);n(t)}))}onRoomMessage(e,t,n){return this.track(()=>this.raw.onRoomMessage(e,t,n))}onRoomUserJoin(e,t){return this.track(()=>this.raw.onRoomUserJoin(e,t))}onRoomUserLeave(e,t){return this.track(()=>this.raw.onRoomUserLeave(e,t))}subscribeGlobalState(e){return this.track(()=>this.raw.subscribeGlobalState(e))}subscribeGlobalMyState(e){return this.track(()=>this.raw.subscribeGlobalMyState(e))}subscribeGlobalUserState(e,t){return this.track(()=>this.raw.subscribeGlobalUserState(e,t))}subscribeGlobalCollection(e,t){return this.track(()=>this.raw.subscribeGlobalCollection(e,({items:e})=>{let n={};for(let t of e??[])t&&typeof t.__id==`string`&&(n[t.__id]=t);t(n)}))}subscribeAsset(e,t){return this.track(()=>this.raw.subscribeAsset(e,t))}onGlobalMessage(e,t){return this.track(()=>this.raw.onGlobalMessage(e,t))}};export{t as createAgent8Server};