incanto 0.67.0 → 0.69.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 (87) hide show
  1. package/assets/catalog.json +9 -5
  2. package/bin/_pick-server.mjs +48 -0
  3. package/bin/incanto-assets.mjs +14 -1
  4. package/bin/incanto-check.mjs +69 -5
  5. package/bin/incanto-frame.mjs +10 -25
  6. package/bin/incanto-logs.mjs +8 -18
  7. package/bin/incanto-new.mjs +29 -1
  8. package/bin/incanto-serve.mjs +160 -0
  9. package/bin/incanto-verify.mjs +40 -0
  10. package/bin/incanto.mjs +1 -0
  11. package/dist/2d.d.ts +10 -8
  12. package/dist/2d.js +6 -4
  13. package/dist/3d.d.ts +31 -7
  14. package/dist/3d.js +9 -7
  15. package/dist/{audio-player-BNUHo0JF.d.ts → audio-player-C0j2k-yB.d.ts} +80 -3
  16. package/dist/{behavior-ZnfzKDo3.d.ts → behavior-rm8vVzu_.d.ts} +145 -1
  17. package/dist/boot-failure-CKYrEcGF.js +47 -0
  18. package/dist/{create-game-FAXdHta3.js → create-game-CvJ1wSRP.js} +27 -27
  19. package/dist/{create-game-DMetd3vR.js → create-game-CwxEDlCm.js} +15 -11
  20. package/dist/debug.d.ts +1 -1
  21. package/dist/{duplicate-B8QnX2-r.js → duplicate-Dj4L-5AA.js} +1 -1
  22. package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
  23. package/dist/editor.js +12 -6
  24. package/dist/env.d.ts +1 -1
  25. package/dist/{environment-presets-C-5g2pMh.js → environment-presets-BO-7XoFV.js} +58 -9
  26. package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
  27. package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
  28. package/dist/{gameplay-CGSv-9XR.js → gameplay-D1KixWK8.js} +175 -151
  29. package/dist/gameplay.d.ts +51 -3
  30. package/dist/gameplay.js +2 -2
  31. package/dist/index.d.ts +6 -6
  32. package/dist/index.js +8 -7
  33. package/dist/{loader-KZESOZmE.js → loader-DZE7B07H.js} +48 -7
  34. package/dist/{loader-DyT30ArB.d.ts → loader-DadV2QCT.d.ts} +2 -2
  35. package/dist/net.d.ts +2 -2
  36. package/dist/net.js +1 -1
  37. package/dist/{physics-2d-_B6zj3zU.js → physics-2d-CF5yyjbj.js} +2 -2
  38. package/dist/{physics-3d-i1IeAY0F.js → physics-3d-DbYaEcZy.js} +3 -3
  39. package/dist/react.d.ts +2 -2
  40. package/dist/react.js +1 -1
  41. package/dist/{register-vIxU3Xbv.js → register-DbVArsns.js} +19 -10
  42. package/dist/{register-Cs53CY7s.js → register-mmfugpaQ.js} +159 -6
  43. package/dist/{replay-BkpDV51O.js → replay-BEHLDcQV.js} +28 -5
  44. package/dist/{replay-D0GcEHYU.d.ts → replay-BQfkX4x7.d.ts} +38 -4
  45. package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
  46. package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
  47. package/dist/{split-screen-DS8RaUV_.js → split-screen-CauMwu6y.js} +2 -2
  48. package/dist/{split-screen-DD5eYIgW.d.ts → split-screen-Cg76MCV5.d.ts} +3 -3
  49. package/dist/{src-BLlsGJW9.js → src-CYEu-_S9.js} +1 -1
  50. package/dist/{teardown-LB9cTnXJ.js → teardown-CMqFVxO9.js} +5 -4
  51. package/dist/{test-81uZ967a.js → test-D6ISmuFE.js} +73 -25
  52. package/dist/test.d.ts +17 -6
  53. package/dist/test.js +3 -3
  54. package/dist/vite.d.ts +70 -2
  55. package/dist/vite.js +176 -14
  56. package/dist/{rendering-options-ByAdi8PF.js → webgl-unavailable-Z5_g8x9H.js} +57 -2
  57. package/editor/assets/{agent8-C2jtNKbx.js → agent8-BDvaZMQJ.js} +1 -1
  58. package/editor/assets/{debug-DoU3KeqS.js → debug-CMULinBW.js} +1 -1
  59. package/editor/assets/{index-CNqHojcC.js → index-CZIRr5EL.js} +92 -92
  60. package/editor/index.html +1 -1
  61. package/package.json +4 -3
  62. package/schemas/scene.schema.json +4 -0
  63. package/skills/incanto-3d-character.md +1 -0
  64. package/skills/incanto-assets.md +16 -2
  65. package/skills/incanto-audio.md +65 -0
  66. package/skills/incanto-building-2d-games.md +66 -0
  67. package/skills/incanto-building-3d-games.md +97 -2
  68. package/skills/incanto-gameplay-behaviors.md +46 -2
  69. package/skills/incanto-node-reference.md +1 -0
  70. package/skills/incanto-physics-and-input.md +23 -0
  71. package/skills/incanto-save-slots.md +27 -0
  72. package/skills/incanto-scene-json-authoring.md +12 -1
  73. package/skills/incanto-verifying-your-game.md +96 -2
  74. package/skills/incanto-your-first-game.md +2 -1
  75. package/templates-app/beacon-isle-3d/package.json +1 -1
  76. package/templates-app/beacon-isle-3d/src/main.ts +21 -4
  77. package/templates-app/platformer-2d/index.html +0 -23
  78. package/templates-app/platformer-2d/package.json +1 -1
  79. package/templates-app/platformer-2d/src/behaviors.ts +26 -16
  80. package/templates-app/platformer-2d/src/game.scene.json +17 -27
  81. package/templates-app/platformer-2d/src/main.ts +32 -13
  82. package/templates-app/star-survivor/package.json +1 -1
  83. package/templates-app/star-survivor/src/main.ts +25 -7
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/main.ts +34 -16
  86. package/templates-app/village-quest-3d/package.json +1 -1
  87. package/templates-app/village-quest-3d/src/main.ts +21 -4
@@ -1,5 +1,5 @@
1
- import { b as Engine } from "./behavior-ZnfzKDo3.js";
2
- import { s as JsonObject } from "./schema-CFeioQRE.js";
1
+ import { b as Engine } from "./behavior-rm8vVzu_.js";
2
+ import { s as JsonObject } from "./schema-B6ugCV1Q.js";
3
3
 
4
4
  //#region src/core/audit.d.ts
5
5
  /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
@@ -15,7 +15,41 @@ interface AuditOptions {
15
15
  * has one.
16
16
  */
17
17
  declaredElsewhere?: Record<string, Record<string, string>>;
18
+ /**
19
+ * Another scene in this project embeds this file with `instance`.
20
+ *
21
+ * That, and only that, makes a scene a FRAGMENT — a thing lit and framed by
22
+ * whatever drops it in, which must not be told it will render black.
23
+ *
24
+ * The discriminator used to be "it has no camera", which is the same shape a
25
+ * beginner's FIRST 3D scene has. Measured on a ground plane and a capsule
26
+ * with no camera and no light — a guaranteed black screen:
27
+ *
28
+ * $ incanto-check
29
+ * ok src/game.scene.json
30
+ * 1/1 scene(s) valid
31
+ *
32
+ * Not the lighting warning, and not the camera one either. The checker walks
33
+ * the whole project already (it unions `strings` and `assets` the same way),
34
+ * so it can answer this by looking instead of guessing.
35
+ */
36
+ instanced?: boolean;
18
37
  }
38
+ /**
39
+ * Which files in a corpus some OTHER scene embeds with `instance`.
40
+ *
41
+ * A fragment cannot be recognised by looking at itself — `enemy.scene.json` and
42
+ * a broken first level are the same file shape. It is recognised by the project
43
+ * around it, which every tool that audits more than one scene already walks.
44
+ *
45
+ * `resolveRef(fromFile, ref)` turns a scene-relative `instance` path into the
46
+ * same spelling the corpus uses for that file, so the caller owns all path
47
+ * logic and this stays runnable in a browser.
48
+ */
49
+ declare function instancedScenes(corpus: {
50
+ path: string;
51
+ json: JsonObject;
52
+ }[], resolveRef: (fromFile: string, ref: string) => string): Set<string>;
19
53
  declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
20
54
  //#endregion
21
55
  //#region src/core/errors.d.ts
@@ -25,7 +59,7 @@ declare function auditScene(scene: JsonObject, opts?: AuditOptions): string[];
25
59
  * Incanto fails loudly with machine-readable codes and messages that list valid
26
60
  * alternatives — AI agents self-correct on hard failures, not warnings.
27
61
  */
28
- type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE";
62
+ type IncantoErrorCode = "AMBIGUOUS_HANDLER" | "BAD_FORMAT" | "BAD_NODE_PATH" | "DANGLING_CONNECTION" | "DUPLICATE_BEHAVIOR" | "DUPLICATE_NODE_TYPE" | "DUPLICATE_UNIQUE_NAME" | "DUPLICATE_UID" | "NODE_NOT_FOUND" | "PROP_TYPE_MISMATCH" | "TREE_VIOLATION" | "UNKNOWN_ANIMATION" | "UNKNOWN_ASSET" | "UNKNOWN_CONSTANT" | "UNKNOWN_BEHAVIOR" | "UNKNOWN_HANDLER" | "UNKNOWN_NODE_TYPE" | "UNKNOWN_PROP" | "UNKNOWN_SIGNAL" | "UNRESOLVED_INSTANCE" | "WEBGL_UNAVAILABLE";
29
63
  /**
30
64
  * Structured context mirrored from the prose message — tools and agents parse
31
65
  * THESE instead of regexing the message (which is for humans and may reword).
@@ -100,4 +134,4 @@ declare function replay(engine: Engine, recording: ReplayJson, opts?: {
100
134
  onTick?: (index: number) => void;
101
135
  }): void;
102
136
  //#endregion
103
- export { startRecording as a, IncantoErrorDetails as c, replay as i, auditScene as l, ReplayEvent as n, IncantoError as o, ReplayJson as r, IncantoErrorCode as s, Recorder as t };
137
+ export { startRecording as a, IncantoErrorDetails as c, replay as i, auditScene as l, ReplayEvent as n, IncantoError as o, ReplayJson as r, IncantoErrorCode as s, Recorder as t, instancedScenes as u };
@@ -74,6 +74,17 @@ interface SceneJson {
74
74
  input?: JsonObject;
75
75
  /** Physics config — `{gravity: [x,y]}` (2D px/s²) or `{gravity: [x,y,z]}` (3D m/s²). */
76
76
  physics?: JsonObject;
77
+ /**
78
+ * This scene is a PIECE of another one — a spawner's prefab, an instanced
79
+ * sub-scene — and is never rendered on its own.
80
+ *
81
+ * It exists because no tool can tell a prefab from a broken first level by
82
+ * looking at the file: both are a small tree with no camera and no light. The
83
+ * audit used to guess ("no camera means fragment") and so went silent on the
84
+ * commonest beginner mistake there is. One key, written by the author who
85
+ * knows, is the honest version. It changes nothing at runtime.
86
+ */
87
+ fragment?: boolean;
77
88
  /** Multiplayer config (interpreted in M6; preserved verbatim until then). */
78
89
  multiplayer?: JsonObject;
79
90
  /** Scene-level rendering environment (ambient light, background — interpreted by renderers). */
@@ -35,7 +35,7 @@ function divisors(total) {
35
35
  * known — an animation range that runs off the end is the other half of the
36
36
  * same mistake.
37
37
  */
38
- function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame) {
38
+ function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, highestFrame, unit = "frame") {
39
39
  if (!imageWidth || !imageHeight || !frameWidth || !frameHeight) return null;
40
40
  if (imageWidth % frameWidth !== 0 || imageHeight % frameHeight !== 0) {
41
41
  const parts = [];
@@ -51,7 +51,11 @@ function checkSheetGrid(key, frameWidth, frameHeight, imageWidth, imageHeight, h
51
51
  }
52
52
  if (highestFrame !== void 0) {
53
53
  const frames = imageWidth / frameWidth * (imageHeight / frameHeight);
54
- if (highestFrame >= frames) return `[incanto] spritesheet '${key}' has ${frames} frames (0–${frames - 1}), and an animation asks for frame ${highestFrame}. Those frames draw whatever is at the wrong end of the sheet.`;
54
+ if (highestFrame >= frames) {
55
+ const sheet = unit === "tile" ? "tile atlas" : "spritesheet";
56
+ const asks = unit === "tile" ? "a cell asks for tile" : "an animation asks for frame";
57
+ return `[incanto] ${sheet} '${key}' has ${frames} ${unit}s (0–${frames - 1}), and ${asks} ${highestFrame}. Those ${unit}s draw whatever is at the wrong end of the sheet.`;
58
+ }
55
59
  }
56
60
  return null;
57
61
  }
@@ -1,5 +1,5 @@
1
- import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-KZESOZmE.js";
2
- import { t as registerCoreNodes, v as Engine } from "./register-Cs53CY7s.js";
1
+ import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-DZE7B07H.js";
2
+ import { t as registerCoreNodes, v as Engine } from "./register-mmfugpaQ.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 { l as registerNode } from "./registry-CF70EArN.js";
@@ -1,6 +1,6 @@
1
- import { Lt as Signal, b as Engine, jt as Node } from "./behavior-ZnfzKDo3.js";
2
- import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
3
- import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
1
+ import { Lt as Signal, b as Engine, jt as Node } from "./behavior-rm8vVzu_.js";
2
+ import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
+ import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
4
4
 
5
5
  //#region src/net/types.d.ts
6
6
  type Unsubscribe = () => void;
@@ -214,6 +214,6 @@ function newUid() {
214
214
  //#endregion
215
215
  //#region src/index.ts
216
216
  /** Engine version. Kept in sync with package.json by the release pipeline. */
217
- const VERSION = "0.67.0";
217
+ const VERSION = "0.69.0";
218
218
  //#endregion
219
219
  export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
@@ -1,6 +1,6 @@
1
- import { _ as AudioPlayer } from "./register-Cs53CY7s.js";
2
- import { i as parseDrive, n as logReport } from "./rendering-options-ByAdi8PF.js";
3
- import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
1
+ import { _ as AudioPlayer } from "./register-mmfugpaQ.js";
2
+ import { i as logReport, o as parseDrive } from "./webgl-unavailable-Z5_g8x9H.js";
3
+ import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-Dlq13Gyj.js";
4
4
  //#region src/core/audio-errors.ts
5
5
  /** Every AudioPlayer under `root` whose source could not be played. */
6
6
  function audioErrors(root) {
@@ -35,7 +35,7 @@ function claimCanvasGestures(canvas) {
35
35
  * empty list outside a dev server.
36
36
  */
37
37
  function wireDevChannel(opts) {
38
- const hot = import.meta.hot;
38
+ const hot = import.meta.hot ?? globalThis.__incantoInspect;
39
39
  const cleanups = [];
40
40
  if (!hot) return cleanups;
41
41
  const onFrameRequest = (data) => {
@@ -59,6 +59,7 @@ function wireDevChannel(opts) {
59
59
  report: {
60
60
  ...frameStats(shot.pixels, shot.width, shot.height, { grid: request?.grid }),
61
61
  signature: frameSignature(shot.pixels, shot.width, shot.height),
62
+ hidden: typeof document !== "undefined" && document.hidden,
62
63
  ...droveFrames === null ? {} : { droveFrames },
63
64
  ...request?.image ? { image: frameImage(shot.pixels, shot.width, shot.height, (w, h) => Object.assign(document.createElement("canvas"), {
64
65
  width: w,
@@ -1,13 +1,13 @@
1
- import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-KZESOZmE.js";
2
- import { v as Engine } from "./register-Cs53CY7s.js";
1
+ import { f as resolveViewport, n as loadScene, z as registerBehavior } from "./loader-DZE7B07H.js";
2
+ import { v as Engine } from "./register-mmfugpaQ.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-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";
6
+ import { n as startRecording } from "./replay-BEHLDcQV.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-D1KixWK8.js";
8
+ import { t as registerNodes2D } from "./register-DbVArsns.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-BO-7XoFV.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-CauMwu6y.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
13
13
  /**
@@ -252,7 +252,7 @@ function describeFraming2D(scene, placed, opts) {
252
252
  round(cy),
253
253
  0
254
254
  ],
255
- view: [vw, vh],
255
+ view: [round(vw / zoom), round(vh / zoom)],
256
256
  zoom
257
257
  };
258
258
  }
@@ -630,6 +630,23 @@ var Driver = class {
630
630
  headingLeft = 0;
631
631
  visits = /* @__PURE__ */ new Map();
632
632
  buttons;
633
+ /**
634
+ * Buttons the bot pressed and will never press again, because pressing them
635
+ * STOPPED THE CLOCK.
636
+ *
637
+ * It drove every declared button action, and `incanto-your-first-game.md`
638
+ * tells you to declare `pause`. Measured on one losable scene: 0/10 losses
639
+ * with `pause` declared, 10/10 without it, same seed, same everything else.
640
+ * A frozen game cannot hurt you, so the run also reported the game as having
641
+ * no way to lose.
642
+ *
643
+ * This is OBSERVED, not guessed from the name — a game is free to call it
644
+ * `menu`, `escape`, `halt` or nothing at all, and the bot notices the same
645
+ * way a player would: the world stopped moving.
646
+ */
647
+ frozeTheGame = /* @__PURE__ */ new Set();
648
+ /** What was pressed most recently, so a freeze can be attributed to it. */
649
+ lastPressed = null;
633
650
  moveAction;
634
651
  held = /* @__PURE__ */ new Map();
635
652
  /**
@@ -680,7 +697,20 @@ var Driver = class {
680
697
  cell(p) {
681
698
  return `${Math.round(p[0] / 4)},${Math.round(p[2] / 4)}`;
682
699
  }
700
+ /** Actions this run avoided after they stopped the clock. */
701
+ paused() {
702
+ return [...this.frozeTheGame];
703
+ }
683
704
  step(dt, at) {
705
+ if (this.engine.timeScale === 0) {
706
+ if (this.lastPressed) this.frozeTheGame.add(this.lastPressed);
707
+ for (const button of this.buttons) {
708
+ this.engine.input.releaseAction(button);
709
+ this.held.set(button, -1);
710
+ }
711
+ this.engine.timeScale = 1;
712
+ this.lastPressed = null;
713
+ }
684
714
  this.visits.set(this.cell(at), (this.visits.get(this.cell(at)) ?? 0) + 1);
685
715
  this.headingLeft -= dt;
686
716
  if (this.headingLeft <= 0) {
@@ -689,6 +719,7 @@ var Driver = class {
689
719
  }
690
720
  if (this.moveAction) this.engine.input.setActionVector(this.moveAction, Math.cos(this.heading), Math.sin(this.heading));
691
721
  for (const button of this.buttons) {
722
+ if (this.frozeTheGame.has(button)) continue;
692
723
  const left = (this.held.get(button) ?? 0) - dt;
693
724
  if (left > 0) {
694
725
  this.held.set(button, left);
@@ -702,6 +733,7 @@ var Driver = class {
702
733
  const chance = button === "jump" ? .06 : .02;
703
734
  if (this.rand() < chance) {
704
735
  this.engine.input.pressAction(button);
736
+ this.lastPressed = button;
705
737
  this.held.set(button, .08 + this.rand() * .12);
706
738
  }
707
739
  }
@@ -820,9 +852,10 @@ async function runOnce(json, seed, opts) {
820
852
  });
821
853
  let at = start;
822
854
  let wentAsFarAs = 0;
855
+ 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));
823
856
  while (session.timeMs < deadline) {
824
857
  at = player ? worldOf(player) : start;
825
- wentAsFarAs = Math.max(wentAsFarAs, Math.hypot(at[0] - start[0], at[2] - start[2]));
858
+ wentAsFarAs = Math.max(wentAsFarAs, travelled(at, start));
826
859
  driver.step(stepMs / 1e3, at);
827
860
  session.step(stepMs);
828
861
  if (player) for (const [path, node] of targetNodes) {
@@ -878,7 +911,8 @@ async function runOnce(json, seed, opts) {
878
911
  signals,
879
912
  declaresEnd: oracle.declaresEnd(),
880
913
  hazards,
881
- hasPlayer: player !== null
914
+ hasPlayer: player !== null,
915
+ froze: driver.paused()
882
916
  };
883
917
  }
884
918
  /**
@@ -908,6 +942,7 @@ async function playtest(json, opts = {}) {
908
942
  let declaresWin = false;
909
943
  let hasPlayer = false;
910
944
  let hazards = 0;
945
+ const froze = /* @__PURE__ */ new Set();
911
946
  for (let i = 0; i < count; i++) {
912
947
  const one = await runOnce(json, base + i, opts);
913
948
  runs.push(one.run);
@@ -916,6 +951,7 @@ async function playtest(json, opts = {}) {
916
951
  declaresWin = declaresWin || one.declaresEnd;
917
952
  hasPlayer = hasPlayer || one.hasPlayer;
918
953
  hazards = one.hazards;
954
+ for (const action of one.froze) froze.add(action);
919
955
  }
920
956
  const probe = await createPlaySession(json, {
921
957
  seed: base,
@@ -946,6 +982,7 @@ async function playtest(json, opts = {}) {
946
982
  declaresWin,
947
983
  hasPlayer,
948
984
  hazards,
985
+ froze: [...froze],
949
986
  seconds: opts.seconds ?? 60,
950
987
  feedback: {
951
988
  declaredAudio: feedbackNodes.audio,
@@ -1016,8 +1053,14 @@ function playtestText(report) {
1016
1053
  if (silent.length > 12) lines.push(` …and ${silent.length - 12} more`);
1017
1054
  }
1018
1055
  const damage = runs.reduce((n, r) => n + r.damageTaken, 0);
1056
+ const lost = runs.filter((r) => r.outcome === "lost").length;
1057
+ if (report.froze.length > 0) {
1058
+ lines.push("");
1059
+ 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.`);
1060
+ }
1019
1061
  lines.push("");
1020
1062
  if (damage > 0) lines.push(` danger: ${damage} hits across ${total} runs (${(damage / total).toFixed(1)} per run)`);
1063
+ else if (hazards === 0 && lost > 0) lines.push(` danger: nothing DEALT DAMAGE — no DamageOnContact, no hazard group — and yet ${lost} of ${total} runs were lost. The loss comes from something else: a fall, a timer, a trigger, or running out of lives.`);
1021
1064
  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
1065
  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.`);
1023
1066
  return lines.join("\n");
@@ -1812,8 +1855,8 @@ async function playMultiplayer(opts) {
1812
1855
  */
1813
1856
  async function joinLate(server, first, opts) {
1814
1857
  try {
1815
- const { Engine } = await import("./register-Cs53CY7s.js").then((n) => n.y);
1816
- const { loadScene } = await import("./loader-KZESOZmE.js").then((n) => n.r);
1858
+ const { Engine } = await import("./register-mmfugpaQ.js").then((n) => n.y);
1859
+ const { loadScene } = await import("./loader-DZE7B07H.js").then((n) => n.r);
1817
1860
  const { jsonClone } = await import("./json-CwwhxQgb.js").then((n) => n.i);
1818
1861
  const engine = new Engine(opts.seed !== void 0 ? { seed: opts.seed + 999 } : {});
1819
1862
  engine.setScene(loadScene(jsonClone(opts.scene), opts.load));
@@ -2175,20 +2218,23 @@ async function runScript(json, opts) {
2175
2218
  if (opts.locale) engine.locale.locale = opts.locale;
2176
2219
  const physics = opts.physics ?? "auto";
2177
2220
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2178
- const { enablePhysics2D } = await import("./physics-2d-_B6zj3zU.js").then((n) => n.r);
2221
+ const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
2179
2222
  await enablePhysics2D(engine);
2180
2223
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2181
- const { enablePhysics3D } = await import("./physics-3d-i1IeAY0F.js").then((n) => n.r);
2224
+ const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
2182
2225
  await enablePhysics3D(engine);
2183
2226
  }
2184
2227
  const failures = [];
2185
2228
  const snapshots = [];
2229
+ const live = () => engine.scene ?? scene;
2186
2230
  const ctx = {
2187
2231
  engine,
2188
- scene,
2232
+ get scene() {
2233
+ return live();
2234
+ },
2189
2235
  timeMs: 0,
2190
- getNode: (path) => scene.root.getNode(path),
2191
- capture: () => captureScene(scene)
2236
+ getNode: (path) => live().root.getNode(path),
2237
+ capture: () => captureScene(live())
2192
2238
  };
2193
2239
  const applyStep = (step) => {
2194
2240
  if (step.press) engine.input.pressAction(step.press);
@@ -2226,7 +2272,7 @@ async function runScript(json, opts) {
2226
2272
  if (t >= nextSnapshot - 1e-6) {
2227
2273
  snapshots.push({
2228
2274
  atMs: Math.round(t),
2229
- capture: captureScene(scene)
2275
+ capture: captureScene(live())
2230
2276
  });
2231
2277
  nextSnapshot += opts.snapshotEveryMs;
2232
2278
  }
@@ -2239,7 +2285,7 @@ async function runScript(json, opts) {
2239
2285
  message: `step never executed — atMs ${missed.atMs} is beyond the ${opts.durationMs}ms run`
2240
2286
  });
2241
2287
  }
2242
- const finalCapture = captureScene(scene);
2288
+ const finalCapture = captureScene(live());
2243
2289
  const logs = [...engine.log.entries()];
2244
2290
  const swallowed = engine.stats().errors;
2245
2291
  if (swallowed > 0) failures.push({
@@ -2291,10 +2337,10 @@ async function createPlaySession(json, opts = {}) {
2291
2337
  engine.setScene(scene);
2292
2338
  const physics = opts.physics ?? "auto";
2293
2339
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
2294
- const { enablePhysics2D } = await import("./physics-2d-_B6zj3zU.js").then((n) => n.r);
2340
+ const { enablePhysics2D } = await import("./physics-2d-CF5yyjbj.js").then((n) => n.r);
2295
2341
  await enablePhysics2D(engine);
2296
2342
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
2297
- const { enablePhysics3D } = await import("./physics-3d-i1IeAY0F.js").then((n) => n.r);
2343
+ const { enablePhysics3D } = await import("./physics-3d-DbYaEcZy.js").then((n) => n.r);
2298
2344
  await enablePhysics3D(engine);
2299
2345
  }
2300
2346
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -2302,7 +2348,9 @@ async function createPlaySession(json, opts = {}) {
2302
2348
  let logCursor = 0;
2303
2349
  return {
2304
2350
  engine,
2305
- scene,
2351
+ get scene() {
2352
+ return engine.scene ?? scene;
2353
+ },
2306
2354
  get timeMs() {
2307
2355
  return timeMs;
2308
2356
  },
@@ -2312,13 +2360,13 @@ async function createPlaySession(json, opts = {}) {
2312
2360
  timeMs += ticks * stepMs;
2313
2361
  },
2314
2362
  capture() {
2315
- return scene.toJSON();
2363
+ return (engine.scene ?? scene).toJSON();
2316
2364
  },
2317
2365
  describe() {
2318
- return describeCapture(captureScene(scene));
2366
+ return describeCapture(captureScene(engine.scene ?? scene));
2319
2367
  },
2320
2368
  framing() {
2321
- return framingText(describeFraming(scene));
2369
+ return framingText(describeFraming(engine.scene ?? scene));
2322
2370
  },
2323
2371
  drainLogs() {
2324
2372
  const all = engine.log.entries();
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-ZnfzKDo3.js";
2
- import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.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";
1
+ import { P as Scene, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-rm8vVzu_.js";
2
+ import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
3
+ import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-BQfkX4x7.js";
5
+ import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-Cg76MCV5.js";
6
6
 
7
7
  //#region src/test/facing.d.ts
8
8
  interface FacingOptions {
@@ -361,6 +361,17 @@ interface PlaytestReport {
361
361
  /** How many nodes are wired to hurt the player — see the `danger:` line. */
362
362
  hazards: number;
363
363
  /**
364
+ * Actions the bot stopped pressing because they STOPPED THE CLOCK.
365
+ *
366
+ * It drove every declared button, and `incanto-your-first-game.md` tells you
367
+ * to declare `pause`. Measured on one losable scene: 0/10 losses with `pause`
368
+ * declared, 10/10 without, everything else identical — a frozen game cannot
369
+ * hurt you, so the same run also reported the game as having no way to lose.
370
+ * Named here rather than guessed from the word "pause": a game may call it
371
+ * `menu` or `escape`, and the bot notices the way a player would.
372
+ */
373
+ froze: string[];
374
+ /**
364
375
  * Did `findPlayer` find anyone to drive?
365
376
  *
366
377
  * `findPlayer`'s own comment says a scene it cannot drive "says so rather
@@ -689,4 +700,4 @@ interface PlaySession {
689
700
  */
690
701
  declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
691
702
  //#endregion
692
- export { type AgreementSnapshot, 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, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, 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 };
703
+ export { type AgreementSnapshot, 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, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, type RungResult, type RungStatus, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
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
- 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
+ import { i as instancedScenes, r as auditScene } from "./replay-BEHLDcQV.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-D6ISmuFE.js";
3
+ export { agreementProblems, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, instancedScenes, ladderText, ladderVerdict, multiplayProblems, multiplayText, playMultiplayer, playtest, playtestText, registerAllNodes, runScript, takeAgreementSnapshots, validateScene };
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as FrameSignature } from "./frame-report-DCnHFmto.js";
1
+ import { n as FrameSignature } from "./frame-report-DNxDAb1w.js";
2
2
 
3
3
  //#region src/vite/discover.d.ts
4
4
  /**
@@ -76,6 +76,74 @@ interface FrameEndpointOptions {
76
76
  }
77
77
  declare function serveFrameEndpoints(host: FrameHost, version: string, opts?: FrameEndpointOptions): void;
78
78
  //#endregion
79
+ //#region src/vite/static-inspect.d.ts
80
+ interface StaticInspectHost extends FrameHost {
81
+ /** Handle `/__incanto/pull` and `/__incanto/push`; returns false if not ours. */
82
+ handleInspect(req: FrameReq, res: FrameRes): boolean;
83
+ /** The `<script>` to inject ahead of the game, as page source. */
84
+ clientScript(): string;
85
+ }
86
+ /**
87
+ * Build a `FrameHost` whose channel is a long poll.
88
+ *
89
+ * `use(path, handler)` records middleware the caller dispatches; `hot` is a
90
+ * channel that queues outgoing requests and delivers the page's replies.
91
+ */
92
+ declare function createStaticInspectHost(root: string): StaticInspectHost;
93
+ //#endregion
94
+ //#region src/vite/trim.d.ts
95
+ /**
96
+ * Leave out of the BUILD what this game can never run.
97
+ *
98
+ * A 2D game's `dist/` contains the 3D adapter and rapier3d. Measured on the
99
+ * shipped `platformer-2d` flagship:
100
+ *
101
+ * ```
102
+ * 2236503 assets/rapier-*.js # rapier3d
103
+ * 1692150 assets/rapier-*.js # rapier2d — the one it uses
104
+ * 471186 assets/3d-*.js # the 3D adapter
105
+ * ─────────
106
+ * 5942650 total → 45.6% of it can never execute
107
+ * ```
108
+ *
109
+ * Nothing is wrong at RUNTIME: the chunks are lazy and never requested. This is
110
+ * deploy weight — a CDN, a jam upload, an agent8 zip — and the only path that
111
+ * drags them in is the live game⇄editor switch: `createGame*` imports
112
+ * `incanto/editor`, and the editor imports `incanto/3d` so it can offer 3D node
113
+ * types.
114
+ *
115
+ * `editor: false` on `createGame*` is a RUNTIME option and cannot remove bytes
116
+ * from a bundle; it changed the artifact by 10 bytes while the docs said it
117
+ * "opts out". A build-time decision needs a build-time lever, which is this.
118
+ *
119
+ * ```ts
120
+ * import { incantoScenes, incantoTrim } from 'incanto/vite';
121
+ * export default defineConfig({
122
+ * plugins: [incantoScenes(), incantoTrim({ editor: false })],
123
+ * });
124
+ * ```
125
+ *
126
+ * Build only — the dev server keeps the editor, so `☰ debug ▸ ✎ edit this
127
+ * scene` works while you are building the game and is simply absent from what
128
+ * you ship.
129
+ */
130
+ interface IncantoTrimOptions {
131
+ /**
132
+ * Keep the in-game scene editor in the build. Default `false` when this
133
+ * plugin is used at all — using it is the request.
134
+ */
135
+ editor?: boolean;
136
+ }
137
+ /** Minimal plugin shape, so this file needs no vite types. */
138
+ interface TrimPlugin {
139
+ name: string;
140
+ apply: "build";
141
+ enforce: "pre";
142
+ resolveId(id: string): string | null;
143
+ load(id: string): string | null;
144
+ }
145
+ declare function incantoTrim(opts?: IncantoTrimOptions): TrimPlugin;
146
+ //#endregion
79
147
  //#region src/vite/index.d.ts
80
148
  interface HotUpdateContext {
81
149
  file: string;
@@ -259,4 +327,4 @@ declare function serveDeviceAuth(req: LibraryReq, res: LibraryRes, opts?: Incant
259
327
  /** @internal Exported for tests — the whole request/response behaviour. */
260
328
  declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
261
329
  //#endregion
262
- export { type FrameHost, IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, listeningPorts, maskToken, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };
330
+ export { type FrameHost, type FrameReq, type FrameRes, IncantoLibraryOptions, IncantoScenesOptions, type IncantoTrimOptions, ProjectFileEntry, type StaticInspectHost, createStaticInspectHost, discoverScenes, incantoLibrary, incantoScenes, incantoTrim, listeningPorts, maskToken, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };