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.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1205 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- 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-
|
|
2
|
-
import {
|
|
3
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
4
|
-
import { l as auditScene, o as IncantoError, r as ReplayJson, u as instancedScenes } from "./replay-
|
|
5
|
-
import { l as LocalGameServerOptions, n as SplitScreenPlayer } from "./split-screen-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
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
|
-
/**
|
|
610
|
-
|
|
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 };
|