incanto 0.69.0 → 0.71.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 (147) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1088 -882
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
package/dist/test.d.ts CHANGED
@@ -1,9 +1,30 @@
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";
1
+ import { P as Scene$1, Pt as LogEntry, b as Engine, jt as Node, n as BehaviorCtor } from "./behavior-Dcz0fr1S.js";
2
+ import { a as SceneJson, c as JsonValue, s as JsonObject } from "./rng-Bb-IutXB.js";
3
+ import { t as LoadSceneOptions } from "./loader-8-IHvbKD.js";
4
+ import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-F7IZHdFR.js";
5
+ import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-CIYf1zSf.js";
6
+ import { Matrix4, Vector3 } from "three";
6
7
 
8
+ //#region src/core/asset-urls.d.ts
9
+ interface SceneAssetUrl {
10
+ /** `/Root/Enemies/Slime` — where in the tree it was written. */
11
+ path: string;
12
+ /** `texture`, `sheet`, `material.map`, … */
13
+ prop: string;
14
+ /** The asset spec's `kind` — `texture`, `spritesheet`, `model`, `audio`, … */
15
+ kind: string;
16
+ url: string;
17
+ }
18
+ /**
19
+ * Walk the tree and collect every URL-valued asset prop.
20
+ *
21
+ * `$ref` values are somebody else's problem (`validateAssetRefs` resolves them
22
+ * against `assets{}` at load). Unknown node types are skipped rather than
23
+ * thrown on: this is a warning path, and the loader reports an unknown type far
24
+ * better than a walker could.
25
+ */
26
+ declare function sceneAssetUrls(json: unknown): SceneAssetUrl[];
27
+ //#endregion
7
28
  //#region src/test/facing.d.ts
8
29
  interface FacingOptions {
9
30
  behaviors?: Record<string, BehaviorCtor>;
@@ -26,6 +47,20 @@ interface FacingReport {
26
47
  backwards: boolean;
27
48
  /** How far it actually moved, so a report on a stuck character is not trusted. */
28
49
  travelled: number;
50
+ /**
51
+ * WHICH property was read to get a heading — `yaw` (3D, and 2D top-down),
52
+ * `flipX` (a 2D side view), or null when the skin declares neither.
53
+ *
54
+ * It exists because the answer used to be "none of them, silently": in 2D
55
+ * both values this read were `undefined`, so `dot` came out exactly 0,
56
+ * `backwards` could never fire, and the line printed was
57
+ * `faces its travel (dot 0.00)` for any 2D character whatever way its art
58
+ * pointed. A check that cannot fail is worse than no check, so a report that
59
+ * measured nothing now SAYS it measured nothing.
60
+ */
61
+ basis: "yaw" | "flipX" | null;
62
+ /** When there is no `dot`, which of the four reasons it is. */
63
+ noAnswer: "no-player" | "no-skin" | "did-not-move" | "no-heading" | null;
29
64
  }
30
65
  /**
31
66
  * Drive the player forward and compare where the skin points with where it went.
@@ -40,14 +75,34 @@ declare function facingText(report: FacingReport): string;
40
75
  //#endregion
41
76
  //#region src/test/framing.d.ts
42
77
  /** Where a node's origin sits relative to the current camera. */
43
- type Where = "onScreen" | "offscreen" | "behind";
78
+ /**
79
+ * `hidden` is not a position — it is the renderer declining to draw at all.
80
+ *
81
+ * It is its own state rather than a flavour of `offscreen` because the fix
82
+ * differs: an offscreen node is in the wrong PLACE, a hidden one is switched
83
+ * off, and a 2D screen drawing absolutely nothing used to report
84
+ * `2 in view, 0 outside it` — the two numbers an author reads to conclude the
85
+ * frame is fine.
86
+ */
87
+ type Where = "onScreen" | "offscreen" | "behind" | "hidden" | "blank";
44
88
  interface FramingEntry {
45
89
  path: string;
46
90
  type: string;
47
91
  /** World-space origin. */
48
92
  at: [number, number, number];
49
93
  where: Where;
50
- /** Normalised device coords, x/y in −1..1 (right/up). Absent when `behind`. */
94
+ /**
95
+ * Normalised coords, x/y in −1..1. Absent when `behind`.
96
+ *
97
+ * The Y AXIS DIFFERS BY REPORT, because the two spaces do: in the 3D report
98
+ * this is NDC and +y is UP; in the 2D report it is the design-space window
99
+ * and +y is DOWN, like every other 2D coordinate in this engine. The one
100
+ * docstring here used to promise "(right/up)" for both, and the comment above
101
+ * the 2D computation claimed it was "the same reading as the 3D report" — so
102
+ * anything comparing a 2D `screen.y` against a 3D one was reading a sign
103
+ * that was never there. `FramingReport.dimension` already tells you which
104
+ * report you have; this follows it.
105
+ */
51
106
  screen?: [number, number];
52
107
  /** Metres from the camera's eye. */
53
108
  distance: number;
@@ -70,9 +125,55 @@ interface FramingReport {
70
125
  fovDeg?: number; /** 2D only: world pixels visible, after `zoom`. */
71
126
  view?: [number, number]; /** 2D only. */
72
127
  zoom?: number;
128
+ /**
129
+ * 3D: which way is UP on screen — the camera's local +Y in world space.
130
+ *
131
+ * Roll changes nothing else: `at` and `forward` of a camera rolled 180°
132
+ * are identical to an upright one's, so the whole world hanging upside
133
+ * down printed a byte-identical header.
134
+ */
135
+ up?: [number, number, number];
136
+ /**
137
+ * 3D: how far the horizon is tilted, in degrees. 0 is level, 180 inverted.
138
+ *
139
+ * A number rather than a verdict: a dutch angle is a real choice. But the
140
+ * engine's OWN suggested default — `position [7, 6, 9], rotation
141
+ * [-26, 36, 0]` — measures 16° of roll, and nothing anywhere reported it,
142
+ * because no two-angle Euler-XYZ recipe can produce a level camera in the
143
+ * first place (`[pitch, yaw, 0]` is Rx·Ry; a level camera is Ry·Rx).
144
+ */
145
+ rollDeg?: number;
146
+ /**
147
+ * The frame is upside down (`up.y < 0`), near-vertical shots excepted.
148
+ *
149
+ * A plain top-down camera (`rotation: [-90, 0, 0]`) measures
150
+ * `up = (0, 0, -1)` — `up.y` is 0 EXACTLY and the frame is correct, so the
151
+ * test is `< 0` and skips |forward.y| > 0.98 entirely.
152
+ */
153
+ upsideDown?: boolean;
154
+ /**
155
+ * The projection matrix has a non-finite element, so NOTHING is drawn.
156
+ *
157
+ * `fov: 0` / `near: 0` / `near >= far`. Every entry is reported offscreen
158
+ * rather than passed through with `screen [NaN, NaN]` under an `ok`.
159
+ */
160
+ degenerate?: boolean;
73
161
  } | null;
74
162
  entries: FramingEntry[];
75
163
  /**
164
+ * Widgets a HudLayer puts on screen, by path — the part of the picture the
165
+ * camera has nothing to do with.
166
+ *
167
+ * The walk stops at a `HudLayer` on purpose ("asking whether the camera can
168
+ * see it is a category error"), and nothing counted what was inside. So a
169
+ * shop screen made entirely of widgets and a scene file with a root node and
170
+ * nothing else BOTH read `0/0 on screen`, and the check whose header says
171
+ * "N scene(s) a person would call broken on sight" passed the second one:
172
+ * its rule is "the camera sees none of the things there are", and there were
173
+ * none. Hidden widgets are left out, like every other `visible: false` node.
174
+ */
175
+ widgets: string[];
176
+ /**
76
177
  * What lights the scene: light nodes, plus the environment header when it
77
178
  * carries a sky/hdri/preset/ambient. Empty means the render is BLACK.
78
179
  */
@@ -94,10 +195,82 @@ interface FramingOptions {
94
195
  */
95
196
  viewport?: readonly [number, number];
96
197
  }
97
- declare function describeFraming(scene: Scene, opts?: FramingOptions): FramingReport;
198
+ declare function describeFraming(scene: Scene$1, opts?: FramingOptions): FramingReport;
98
199
  /** The report as something to read in a terminal. */
99
200
  declare function framingText(report: FramingReport): string;
100
201
  //#endregion
202
+ //#region src/test/picker.d.ts
203
+ /**
204
+ * "What node is under this point" — from GEOMETRY, with no renderer.
205
+ *
206
+ * `Clickable` reads `engine.picker`, which the renderer installs as a GPU
207
+ * raycast. Headless there is no renderer, so `engine.picker` is null and every
208
+ * mouse-driven genre — match-3, tower defense, cards, point-and-click, RTS,
209
+ * idle, a flip puzzle — was inert in the one place it was supposed to be
210
+ * proved. The recipe for it was published (write your own picker) and every
211
+ * such game therefore hand-wrote the same box hit-test.
212
+ *
213
+ * The boxes come from the same machinery `describeFraming` uses, so a node's
214
+ * pickable extent and its on-screen extent are the same fact rather than two
215
+ * that drift.
216
+ */
217
+ interface PickerOptions {
218
+ /**
219
+ * The pixel rect the coordinates live in.
220
+ *
221
+ * 2D: defaults to the scene's `viewport.design`, then 960×540 — the same
222
+ * fallback the framing report makes, because it is the same question.
223
+ * 3D: the size the ray is cast through; only the ASPECT changes an answer.
224
+ */
225
+ viewport?: readonly [number, number];
226
+ }
227
+ /**
228
+ * A picker over `scene`, in the scene's own units.
229
+ *
230
+ * 2D takes WORLD pixels — headless there is no canvas to convert from, and the
231
+ * design window is the coordinate space every 2D position in the file is
232
+ * already written in. 3D takes pixels within `viewport` and casts a ray from
233
+ * the scene's current camera.
234
+ *
235
+ * A hidden node is never picked, which matches what the player can click.
236
+ */
237
+ declare function geometricPicker(scene: Scene$1 | (() => Scene$1), opts?: PickerOptions): (x: number, y: number) => Node | null;
238
+ /**
239
+ * `engine.toWorld` / `engine.toScreen` from GEOMETRY, with no renderer — the
240
+ * aiming half of a mouse game, the way `geometricPicker` is the clicking half.
241
+ *
242
+ * A renderer installs these on the engine so a Behavior can ask where the
243
+ * cursor IS; headless nothing did, so a drag-and-launch, a tower placed on the
244
+ * map or a unit steered by the cursor could be clicked in a harness and never
245
+ * aimed. 2D is the identity (the picker's coordinates are already world pixels
246
+ * — there is no canvas to convert from); 3D unprojects onto the `groundY`
247
+ * plane, which is where a 3D game puts the thing you pointed at.
248
+ */
249
+ declare function geometricView(scene: Scene$1 | (() => Scene$1), opts?: PickerOptions & {
250
+ groundY?: number;
251
+ }): {
252
+ toWorld: (x: number, y: number) => number[] | null;
253
+ toScreen: (world: readonly number[]) => {
254
+ x: number;
255
+ y: number;
256
+ behind: boolean;
257
+ } | null;
258
+ screenRay: (x: number, y: number) => {
259
+ origin: number[];
260
+ dir: number[];
261
+ } | null;
262
+ };
263
+ /**
264
+ * Where a node is, in the coordinates `geometricPicker` answers in.
265
+ *
266
+ * The click half of a headless driver: `at %Mole` has to become a point before
267
+ * a press can land on it, and the space differs by dimension — 2D pixels are
268
+ * the world, 3D pixels are a projection through the scene's camera. Both
269
+ * drivers that click headlessly (`runScript` and `incanto-play`) need exactly
270
+ * this, and the first one had it inline.
271
+ */
272
+ declare function pointAtNode(scene: Scene$1, path: string, opts?: PickerOptions): [number, number] | null;
273
+ //#endregion
101
274
  //#region src/test/agreement.d.ts
102
275
  /**
103
276
  * Do two clients hold the same value for the same replicated thing?
@@ -126,8 +299,43 @@ interface AgreementSnapshot {
126
299
  * materialised this account at all.
127
300
  */
128
301
  seen: Record<string, Record<string, JsonValue | undefined> | null>;
302
+ /**
303
+ * Declared sync keys the OWNER cannot read.
304
+ *
305
+ * These used to be dropped from `truth` entirely — "the sender's own problem,
306
+ * already diagnosed there". It is diagnosed there, to `engine.log.error`,
307
+ * which does not touch `stats().errors` and so reaches no verdict; and the
308
+ * sender's report is gated on 500 ms of replication, which a short match may
309
+ * not reach. Meanwhile dropping the key means every observer agrees perfectly
310
+ * about a value that is never sent, and `agrees` grades a dead sync key
311
+ * PASS. A key that cannot be read is the most complete failure of
312
+ * replication there is, so it is a finding, not a silence.
313
+ */
314
+ unreadable?: string[];
315
+ }
316
+ /**
317
+ * One `collection:` spawner on one client: what the server sent it, and what it
318
+ * made of that.
319
+ *
320
+ * `NetworkSpawner` has two sources and only `users` was ever compared. A
321
+ * `collection:` spawner — bullets, pickups, every entity a server owns — could
322
+ * receive a hundred entities and materialise none, and the whole `agrees` rung
323
+ * had nothing to say: the comparison is keyed by ACCOUNT, and a bullet has no
324
+ * account.
325
+ */
326
+ interface CollectionSnapshot {
327
+ /** The client this spawner belongs to. */
328
+ account: string;
329
+ /** Node path of the spawner. */
330
+ spawner: string;
331
+ /** The collection id, from `source: "collection:<id>"`. */
332
+ id: string;
333
+ /** Entity ids this client's manager last received for that collection. */
334
+ received: string[];
335
+ /** Child instance names the spawner actually materialised. */
336
+ materialised: string[];
129
337
  }
130
- type DisagreementKind = "missing" | "absent" | "mismatch" | "shape" | "erased";
338
+ type DisagreementKind = "missing" | "absent" | "mismatch" | "shape" | "erased" | "unreadable" | "unspawned";
131
339
  interface Disagreement {
132
340
  kind: DisagreementKind;
133
341
  /** Whose value it is. */
@@ -152,6 +360,14 @@ interface AgreementOptions {
152
360
  * @param late the same, for a client that joined AFTER everything settled
153
361
  */
154
362
  declare function agreementProblems(live: AgreementSnapshot[], late?: AgreementSnapshot[], opts?: AgreementOptions): Disagreement[];
363
+ /**
364
+ * Collections that arrived and became nothing, and collections two clients
365
+ * disagree about.
366
+ *
367
+ * Pure over snapshots, like `agreementProblems`, and reported in the same
368
+ * shape so one verdict covers both halves of what a spawner does.
369
+ */
370
+ declare function collectionProblems(snapshots: CollectionSnapshot[]): Disagreement[];
155
371
  //#endregion
156
372
  //#region src/test/agreement-snapshot.d.ts
157
373
  /**
@@ -175,6 +391,15 @@ interface AgreementClient {
175
391
  * @param observers the clients whose beliefs to record (defaults to `clients`)
176
392
  */
177
393
  declare function takeAgreementSnapshots(clients: AgreementClient[], observers?: AgreementClient[]): AgreementSnapshot[];
394
+ /**
395
+ * What every `collection:` spawner RECEIVED, and what it made of it.
396
+ *
397
+ * The account-keyed comparison above cannot see these: a bullet has no account.
398
+ * A spawner can take a hundred entities off the wire and materialise none of
399
+ * them — an unregistered `scene` key does exactly that — and until this
400
+ * existed the `agrees` rung had nothing to say about it.
401
+ */
402
+ declare function takeCollectionSnapshots(clients: AgreementClient[]): CollectionSnapshot[];
178
403
  //#endregion
179
404
  //#region src/test/feel.d.ts
180
405
  interface FeelOptions {
@@ -191,6 +416,28 @@ interface FeelReport {
191
416
  /** null when the scene has no player this can drive. */
192
417
  player: string | null;
193
418
  moveAction: string | null;
419
+ /**
420
+ * WHICH WAY the move action was pushed to get these numbers.
421
+ *
422
+ * Every probe used to push `(1, 0)` and only `(1, 0)`. A game whose movement
423
+ * reads the other axis of the same action — the engine's own documented
424
+ * forward is `-y` — measured nothing and printed the silence as `0 u/s` with
425
+ * every derived row blank, exit 0. Null means nothing moved on any axis.
426
+ */
427
+ moveAxis: [number, number] | null;
428
+ /** Every vector2 action this run actually pushed, in the order it tried them. */
429
+ moveTried: string[];
430
+ /**
431
+ * What the engine said while these numbers were being taken.
432
+ *
433
+ * A feel report is a page of confident numbers, and it printed them for a
434
+ * game whose controller's sibling script threw on the first frame and was
435
+ * quarantined. The controller still works, so the numbers are honest for what
436
+ * they measure — and a script dying during the measurement is worth a line.
437
+ * Deduped across probes: each probe is a fresh session.
438
+ */
439
+ errorMessages: string[];
440
+ warnings: string[];
194
441
  jumpAction: string | null;
195
442
  /** Top horizontal speed reached, world units per second. */
196
443
  topSpeed: number;
@@ -209,8 +456,38 @@ interface FeelReport {
209
456
  timeToApexMs: number | null;
210
457
  /** Launch → back on the ground. */
211
458
  airtimeMs: number | null;
459
+ /**
460
+ * The fastest single frame, when it is well above the sustained `topSpeed`.
461
+ *
462
+ * Null when they agree. When they do not, something OTHER than the character's
463
+ * own movement moved it during the measurement — a knockback from an enemy the
464
+ * probe walked into, a launcher, a conveyor — and every number in the block is
465
+ * about that instead.
466
+ */
467
+ peakSpeed: number | null;
212
468
  /** MEASURED, not read: how long after leaving ground a jump still fires. */
213
469
  coyoteMs: number | null;
470
+ /**
471
+ * Can this character get off the ground a full second after leaving it?
472
+ *
473
+ * The coyote probe reads a jump as "vertical velocity went up on the frame I
474
+ * pressed", and TWO of the six shipped starters make that true forever, for
475
+ * two different reasons: `platformer-2d` has a double jump, and
476
+ * `beacon-isle-3d` has a glide that clamps the fall while `jump` is held. So
477
+ * both reported `coyote window always (no window)` — which reads as "your
478
+ * ground check is infinitely generous" — while one of them declares
479
+ * `coyoteSeconds: 0.12` in the very same file.
480
+ *
481
+ * A window cannot be measured this way on a character that can jump in
482
+ * mid-air, and saying that is worth more than a number that is wrong.
483
+ */
484
+ jumpsInMidair: boolean;
485
+ /**
486
+ * `coyoteSeconds` as the scene DECLARES it, in ms — the only number in this
487
+ * report that is read rather than measured, and it is here to be CONTRASTED
488
+ * with `coyoteMs` rather than to stand in for it.
489
+ */
490
+ declaredCoyoteMs: number | null;
214
491
  /** MEASURED: how long before landing a press still survives. */
215
492
  jumpBufferMs: number | null;
216
493
  /** Frames between the press and the first movement. */
@@ -258,6 +535,20 @@ interface MultiplayPlayer {
258
535
  room: string;
259
536
  /** Nodes each `NetworkSpawner` in this client's scene ended up holding. */
260
537
  spawned: Record<string, number>;
538
+ /**
539
+ * The same, with each spawner's `source`.
540
+ *
541
+ * "materialised nothing" used to be asked of the CLIENT — every spawner it
542
+ * has, all at zero — so a client with a working spawner and a dead one
543
+ * beside it was silent, and the whole check was additionally gated on nobody
544
+ * being blind. A spawner is the unit that fails, so a spawner is the unit
545
+ * that is asked.
546
+ */
547
+ spawners: Array<{
548
+ name: string;
549
+ source: string;
550
+ children: number;
551
+ }>;
261
552
  /** Other accounts this client's room state actually carried. */
262
553
  sawAccounts: string[];
263
554
  /** Frame errors on this client's engine. */
@@ -330,6 +621,42 @@ interface PlaytestRun {
330
621
  /** How many times the player's Health went down. */
331
622
  damageTaken: number;
332
623
  errors: number;
624
+ /**
625
+ * What the engine WARNED about — one line per distinct warning.
626
+ *
627
+ * The warnings are the things that leave a game running and wrong: a camera
628
+ * whose `follow` resolves to a node with no position, a `DayNight` with no
629
+ * sky, a body with no collider. `incanto-verify`'s `says` rung reads them off
630
+ * the browser; a headless run had them in `engine.log` and printed none.
631
+ */
632
+ warnings: string[];
633
+ /**
634
+ * What threw, in the engine's own words — one line per distinct error.
635
+ *
636
+ * `errors` is a COUNT, and the count was all a run carried. The message that
637
+ * names the behavior, the node and the exception went to the console only,
638
+ * so `playtestText` — what the bin prints and what an agent reads back —
639
+ * said `✗ error in 2/2` and nothing an author could act on.
640
+ */
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
+ }[];
333
660
  /** Only for a run worth watching — replay it with `incanto-play`. */
334
661
  replay: ReplayJson | null;
335
662
  /** Where the player ended up, for the fall report. */
@@ -338,6 +665,48 @@ interface PlaytestRun {
338
665
  heard: string[];
339
666
  /** Node paths that fired a visible effect during the run. */
340
667
  shown: string[];
668
+ /**
669
+ * Mouse clicks this run landed on a `Clickable`.
670
+ *
671
+ * The bot drives the mouse; nothing counted it, so a game played entirely by
672
+ * clicking looked identical to a game nobody touched.
673
+ */
674
+ clicks: number;
675
+ /**
676
+ * Mouse DRAGS this run — press, move, let go.
677
+ *
678
+ * A game can be entirely mouse-driven with no `Clickable` in it: a slingshot,
679
+ * a drawing game, a box-select, a swipe. The bot could only click a listed
680
+ * node, so it reported the whole class as `nothing here is drivable`.
681
+ */
682
+ drags: number;
683
+ /**
684
+ * HUD widgets this run pressed — a menu game's only verb.
685
+ *
686
+ * Counted separately from `clicks` because they are different gestures on
687
+ * different surfaces: a click lands on a world node through the picker, a
688
+ * press lands on a DOM overlay widget through the path a controller takes.
689
+ */
690
+ presses: number;
691
+ /**
692
+ * Declared BUTTON ACTIONS this run pressed — the keyboard, which is the only
693
+ * verb a rhythm game, a typing game, a QTE or a keyboard puzzle has.
694
+ *
695
+ * The bot has always pressed them (`steer` flips a coin per button per
696
+ * frame); nothing counted them. So a game with no character and four declared
697
+ * lanes was reported as one where "there was nobody to move and nothing to
698
+ * press", in the same run that pressed its buttons three hundred times.
699
+ */
700
+ actionPresses: number;
701
+ /**
702
+ * HUD DRAGS this run — an ingredient into a pot, an item between slots.
703
+ *
704
+ * The bot drove the world mouse and the widgets' activation, and never the
705
+ * one gesture the HUD skill says a click cannot express. So a game whose
706
+ * every verb is a drag reported `nothing here is drivable` and `stuck in
707
+ * 6/6`, about a game that plays.
708
+ */
709
+ widgetDrags: number;
341
710
  }
342
711
  interface PlaytestReport {
343
712
  runs: PlaytestRun[];
@@ -347,9 +716,39 @@ interface PlaytestReport {
347
716
  signals: string[];
348
717
  /** Declared actions the driver had available. */
349
718
  actions: string[];
350
- /** Actions that never changed the world in any run. */
719
+ /**
720
+ * Declared actions NOTHING read, in any run — a control the scene promises
721
+ * and no code implements.
722
+ *
723
+ * This field shipped hard-coded to `[]`, with a comment claiming it held
724
+ * "actions that never changed the world". Nothing ever measured that, so the
725
+ * report answered "no dead controls" for every scene ever passed to it —
726
+ * including `examples/village-quest-3d`, which declares
727
+ * `"restart": { "keys": ["KeyR"] }` and never reads it, so its own control
728
+ * list says R restarts and R does nothing.
729
+ *
730
+ * What is measured now is exact rather than heuristic: `engine.input` records
731
+ * every action something ASKED about, and an action nobody asked about cannot
732
+ * have changed anything. Pressing each button and diffing the world — the
733
+ * original wording — is unmeasurable in a scene where NPCs walk and physics
734
+ * settles; this is free and it cannot be fooled by noise.
735
+ *
736
+ * Advisory, not a failure: an action polled only in a state no run reached
737
+ * (a pause menu the bot never opened) lands here honestly.
738
+ */
351
739
  inertActions: string[];
352
740
  /**
741
+ * Every distinct thing that threw, across every run, in the engine's own
742
+ * words. Empty when nothing did.
743
+ */
744
+ errorMessages: string[];
745
+ /**
746
+ * Every distinct WARNING the engine made, across every run — the things
747
+ * that leave a game running and wrong (a camera following a node with no
748
+ * position, a body with no collider). Empty when there were none.
749
+ */
750
+ warnings: string[];
751
+ /**
353
752
  * Does anything in this scene declare an END?
354
753
  *
355
754
  * "No run reached a win" has two causes that need opposite fixes: a goal
@@ -372,6 +771,23 @@ interface PlaytestReport {
372
771
  */
373
772
  froze: string[];
374
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
+ /**
375
791
  * Did `findPlayer` find anyone to drive?
376
792
  *
377
793
  * `findPlayer`'s own comment says a scene it cannot drive "says so rather
@@ -429,6 +845,30 @@ declare function playtest(json: unknown, opts?: PlaytestOptions): Promise<Playte
429
845
  * of green is a wall nobody reads.
430
846
  */
431
847
  declare function playtestText(report: PlaytestReport): string;
848
+ /**
849
+ * Did this playtest find something WRONG? — the exit code, as a function.
850
+ *
851
+ * `incanto-playtest` exited 1 whenever no run reached a win, and its own report
852
+ * says when that is not a failure. Measured on a walkabout scene:
853
+ *
854
+ * ```
855
+ * · no win declared in this scene — nothing emits won/lost, so there is
856
+ * nothing to reach
857
+ * · 2/2 ran out of time still playing
858
+ * $ echo $?
859
+ * 1
860
+ * ```
861
+ *
862
+ * The `·` is this file's own marker for "not applicable", printed above an exit
863
+ * code that says failure — so a CI step on a walkabout, a title screen, or any
864
+ * scene whose ending is a quest a random walker cannot finish is red forever,
865
+ * and its author learns to stop reading it. `incanto-verify` already reads the
866
+ * JSON and calls those `unmeasured`; only the standalone bin disagreed.
867
+ *
868
+ * What stays a failure regardless of a declared win: a run that THREW, and a
869
+ * run that left the world. Neither is a matter of what the scene set out to be.
870
+ */
871
+ declare function playtestFailed(report: PlaytestReport): boolean;
432
872
  /** The failing runs worth saving, newest problem first. */
433
873
  declare function failingReplays(report: PlaytestReport): Array<{
434
874
  seed: number;
@@ -438,22 +878,13 @@ declare function failingReplays(report: PlaytestReport): Array<{
438
878
  //#endregion
439
879
  //#region src/test/verify-ladder.d.ts
440
880
  /**
441
- * The verification ladder, as one answer.
442
- *
443
- * The rungs already exist — `incanto-check` says the scene loads,
444
- * `incanto-playtest` says it can be finished, `incanto-frame` says something
445
- * was drawn. They are documented together and used apart, because using them
446
- * together means knowing three things nobody writes down:
881
+ * What a spawned tool actually SAID, given its stderr.
447
882
  *
448
- * the ORDER (a scene that does not load cannot be played or drawn, so the
449
- * first red rung is the only one worth reading)
450
- * that an unmeasured rung is not a failing one "no dev server" means the
451
- * question was never asked, and answering it "fail" sends an agent editing
452
- * a scene that is fine
453
- * what to do NEXT, which is one sentence and never the whole report
454
- *
455
- * Pure: the rungs are run by the CLI, this decides what they add up to.
883
+ * Used by every rung in `incanto-verify`; it lives here rather than in the bin
884
+ * so it can be tested without a built `dist` — the bin loads the library at
885
+ * runtime, so a regex only the bin knows is a regex nothing checks.
456
886
  */
887
+ declare function firstLine(text: string | null | undefined): string | null;
457
888
  type RungStatus = "pass" | "fail" | "unmeasured" | "skipped";
458
889
  interface RungResult {
459
890
  name: string;
@@ -490,11 +921,134 @@ interface LadderVerdict {
490
921
  next: string | null;
491
922
  /** Scenes found when none was named (empty once one is being verified). */
492
923
  candidates: string[];
924
+ /**
925
+ * Did anything get asked at all?
926
+ *
927
+ * `ok` answers "is anything WRONG", and with no rungs the honest answer to
928
+ * that is yes-nothing-is-wrong — which is how `incanto-verify` printed
929
+ * *"2 scenes here, and guessing between them would verify the wrong one"*
930
+ * and exited 0. A CI step whose whole job is this command then goes green
931
+ * having looked at nothing, which is the exact shape the gate exists to
932
+ * prevent. Callers that need a verdict rather than an absence read this.
933
+ */
934
+ measured: boolean;
493
935
  }
494
936
  interface LadderContext {
495
937
  /** Scenes found when none was named — none, or more than one to choose from. */
496
938
  candidates?: string[];
497
939
  }
940
+ /**
941
+ * The `says` rung, from `incanto-logs`' report — the one rung whose whole job is
942
+ * "what is the running game SAYING".
943
+ *
944
+ * It read `ok` and printed "no errors, no failed assets", which threw away
945
+ * every WARNING the page had reported. The engine warns about exactly the
946
+ * things that leave a game running and wrong — a camera whose `follow` resolves
947
+ * to a node with no position, a `DayNight` with no sky to move, a `Buoyancy`
948
+ * with no water, a body with no collider — and the headline command showed a
949
+ * tick over all of it.
950
+ *
951
+ * Warnings are NOT a failure (an author may have chosen to live with one), so
952
+ * this keeps `pass` and carries them the way `loads` has always carried
953
+ * `incanto-check`'s: as notes under the rung, printed with a `!`.
954
+ *
955
+ * Pure, so it can be tested without a browser — the bin owns fetching the
956
+ * report, this owns what it means.
957
+ */
958
+ declare function saysRung(report: {
959
+ ok?: boolean;
960
+ counts?: {
961
+ warn?: number;
962
+ };
963
+ grouped?: readonly {
964
+ level: string;
965
+ message: string;
966
+ count: number;
967
+ }[];
968
+ assetErrors?: readonly {
969
+ ref?: string;
970
+ error?: string;
971
+ }[];
972
+ worst?: {
973
+ message?: string;
974
+ } | null;
975
+ stats?: {
976
+ errors?: number;
977
+ };
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;
498
1052
  declare function ladderVerdict(rungs: RungResult[], ctx?: LadderContext): LadderVerdict;
499
1053
  /** The ladder as a person or an agent reads it: every rung, then the one action. */
500
1054
  declare function ladderText(v: LadderVerdict): string;
@@ -559,7 +1113,7 @@ interface SceneCapture {
559
1113
  nodes: NodeCapture[];
560
1114
  }
561
1115
  /** Snapshot the live tree: every node, every schema prop's current value. */
562
- declare function captureScene(scene: Scene): SceneCapture;
1116
+ declare function captureScene(scene: Scene$1): SceneCapture;
563
1117
  /**
564
1118
  * Render a capture as one grep-able line per node, showing only NON-DEFAULT
565
1119
  * props — the same delta discipline as scene JSON, so the interesting state
@@ -585,7 +1139,7 @@ interface ValidationResult {
585
1139
  declare function validateScene(json: unknown, opts?: ValidateSceneOptions): ValidationResult;
586
1140
  interface RunContext {
587
1141
  engine: Engine;
588
- scene: Scene;
1142
+ scene: Scene$1;
589
1143
  /** Simulated time of the current step, in ms. */
590
1144
  timeMs: number;
591
1145
  /** Resolve a node path relative to the scene root. */
@@ -604,10 +1158,65 @@ interface ScriptStep {
604
1158
  vector?: [string, number, number];
605
1159
  /** Raw key-code fallback: [code, isDown]. */
606
1160
  key?: [string, boolean];
1161
+ /**
1162
+ * Click a node by PATH, or a point `[x, y]` — the mouse, as one step.
1163
+ *
1164
+ * `Clickable` reads the mouse BUTTON, not an action, and needs
1165
+ * `engine.picker`, which headless is null because there is no renderer to
1166
+ * raycast with. So every mouse-driven genre — match-3, tower defense, cards,
1167
+ * point-and-click, RTS, idle, a flip puzzle — could not be played by its own
1168
+ * harness: `press`/`release`/`vector`/`key` are all ACTIONS.
1169
+ *
1170
+ * The press and the release land on two different frames, which is what
1171
+ * `Clickable` requires and the part a hand-rolled version forgets. A
1172
+ * `geometricPicker` is installed for the run unless the game already has one
1173
+ * of its own — the game's answer always wins.
1174
+ *
1175
+ * A FUNCTION for the case a mouse game is usually in: click whatever is there
1176
+ * NOW. A whack-a-mole cannot name its target when the script is written, and
1177
+ * `null` means "nothing to click this time", which is not a failure.
1178
+ */
1179
+ click?: string | [number, number] | ((ctx: RunContext) => string | [number, number] | null);
1180
+ /**
1181
+ * Move the cursor, without pressing anything.
1182
+ *
1183
+ * A node path or a point, resolved exactly as `click` resolves one. On its
1184
+ * own this is a HOVER — which is a state a game shows (a highlighted tile, a
1185
+ * tooltip, a cursor that changes) and nothing could reach.
1186
+ *
1187
+ * With `mouse` it is the other half of a DRAG, which `click` cannot express
1188
+ * at all: `click` presses and releases at one point, and a slingshot, a
1189
+ * drawn selection box, a swipe and an inventory item carried between slots
1190
+ * are all "press HERE, move THERE, let go". Measured on a drag-and-launch
1191
+ * game: the launch fired on every shot and the drag vector was always zero,
1192
+ * so the ball dropped where it stood and the harness said the run was fine.
1193
+ */
1194
+ at?: string | [number, number] | ((ctx: RunContext) => string | [number, number] | null);
1195
+ /**
1196
+ * Press or release the mouse where the cursor is, as separate steps.
1197
+ *
1198
+ * `'down'` holds the button until a later `'up'` — across as many frames and
1199
+ * moves as the script likes. `Clickable` still fires on the release over the
1200
+ * node the press started on, so a drag that wanders off is not a click, which
1201
+ * is what the player already expects.
1202
+ */
1203
+ mouse?: "down" | "up";
1204
+ /** Which mouse button `mouse` (and `click`) uses: 0 left · 1 middle · 2 right. */
1205
+ button?: number;
607
1206
  /** Arbitrary imperative hook. */
608
1207
  do?: (ctx: RunContext) => void;
609
- /** Inline check: return false or throw to record a failure (run continues). */
610
- assert?: (ctx: RunContext) => boolean | undefined;
1208
+ /**
1209
+ * Inline check. A failure is `false`, a non-empty STRING (used as the
1210
+ * message), or a throw; the run continues either way.
1211
+ *
1212
+ * The string form exists because it is what people write. A harness that
1213
+ * says WHY it failed reads `(ctx) => problem ?? null` — and this API took
1214
+ * only `false`, so every one of those checks passed by not being `false`,
1215
+ * and a tower-defense harness printed `VERIFY OK` over a keep that had
1216
+ * fallen. Anything else — a number, an object — is recorded as a failure
1217
+ * naming the contract, because a check nobody can read is not a check.
1218
+ */
1219
+ assert?: (ctx: RunContext) => boolean | string | null | undefined;
611
1220
  /** Names this step in failure reports. */
612
1221
  label?: string;
613
1222
  }
@@ -671,7 +1280,7 @@ interface PlaySessionOptions {
671
1280
  }
672
1281
  interface PlaySession {
673
1282
  engine: Engine;
674
- scene: Scene;
1283
+ scene: Scene$1;
675
1284
  /** Simulated time so far, ms. */
676
1285
  readonly timeMs: number;
677
1286
  /** Advance the simulation by ~ms (whole fixed steps). */
@@ -700,4 +1309,4 @@ interface PlaySession {
700
1309
  */
701
1310
  declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
702
1311
  //#endregion
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 };
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 };