incanto 0.45.1 → 0.47.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 (38) hide show
  1. package/bin/incanto-check.mjs +5 -1
  2. package/bin/incanto-feel.mjs +5 -1
  3. package/bin/incanto-frame.mjs +57 -6
  4. package/bin/incanto-model.mjs +79 -10
  5. package/bin/incanto-playtest.mjs +5 -1
  6. package/bin/incanto-verify.mjs +214 -0
  7. package/dist/2d.d.ts +1 -1
  8. package/dist/2d.js +1 -1
  9. package/dist/3d.d.ts +90 -5
  10. package/dist/3d.js +92 -5
  11. package/dist/{create-game-DFRbG0Bj.js → create-game-CniOiWzN.js} +1 -1
  12. package/dist/{create-game-Gifrjhnz.js → create-game-D16MVIPO.js} +24 -5
  13. package/dist/{editor-switch-DVwIGZdK.d.ts → editor-switch-DAvWQeld.d.ts} +1 -1
  14. package/dist/editor.js +4674 -4346
  15. package/dist/{environment-presets-DUsBqvtk.js → environment-presets-D1b0ydTS.js} +5 -4
  16. package/dist/{frame-report-Ct8XgsmV.d.ts → frame-report-DZ70IY26.d.ts} +25 -0
  17. package/dist/{frame-report-Lr3VO24R.js → frame-report-njybhZon.js} +130 -3
  18. package/dist/index.js +1 -1
  19. package/dist/{physics-3d-DFqMtBUD.js → physics-3d-CSoGjM8P.js} +1 -1
  20. package/dist/react.js +1 -1
  21. package/dist/{src-DUxGnvq3.js → src-Ca3oV1fe.js} +1 -1
  22. package/dist/{teardown-yePMOE1K.js → teardown-BKTCzLek.js} +3 -2
  23. package/dist/{test-DD5scWfm.js → test-E4-otKqK.js} +54 -7
  24. package/dist/test.d.ts +55 -1
  25. package/dist/test.js +2 -2
  26. package/dist/vite.d.ts +34 -3
  27. package/dist/vite.js +104 -12
  28. package/editor/assets/{agent8-B25qJ5Wo.js → agent8-_007gPF8.js} +1 -1
  29. package/editor/assets/{debug-CUsz-pT8.js → debug-0DI_MJaq.js} +1 -1
  30. package/editor/assets/{index-3u3GVteP.js → index-B-6eYZEi.js} +173 -96
  31. package/editor/index.html +1 -1
  32. package/package.json +3 -2
  33. package/skills/incanto-3d-models.md +30 -0
  34. package/skills/incanto-editor.md +6 -1
  35. package/skills/incanto-verifying-your-game.md +60 -1
  36. package/templates-app/beacon-isle-3d/package.json +1 -1
  37. package/templates-app/tps-3d/package.json +1 -1
  38. package/templates-app/village-quest-3d/package.json +1 -1
@@ -6536,19 +6536,20 @@ var ModelInstance3D = class extends Node3D {
6536
6536
  if (anim.status !== "ready" || !anim.scene) return null;
6537
6537
  const source = (anim.clip ? anim.clips.find((c) => c.name === anim.clip) : void 0) ?? anim.clips[0];
6538
6538
  if (!source) return null;
6539
+ const cacheKey = source.uuid;
6539
6540
  if (this.entry.isVrm && this.entry.vrm) {
6540
- const cached = this.entry.retargeted.get(ref);
6541
+ const cached = this.entry.retargeted.get(cacheKey);
6541
6542
  if (cached) return cached;
6542
6543
  const result = retargetClipToVrm(source, anim.scene, this.entry.vrm);
6543
6544
  if (result.skippedBones.length > 0) this.diagnose("warn", `incanto: retarget '${ref}' skipped bones: ${result.skippedBones.join(", ")}`);
6544
- this.entry.retargeted.set(ref, result.clip);
6545
+ this.entry.retargeted.set(cacheKey, result.clip);
6545
6546
  return result.clip;
6546
6547
  }
6547
- const cached = this.entry.retargeted.get(ref);
6548
+ const cached = this.entry.retargeted.get(cacheKey);
6548
6549
  if (cached) return cached;
6549
6550
  const rig = retargetClipToRig(source, anim.scene, this.fitGroup ?? this._ensureObject3D());
6550
6551
  if (rig.passthrough) this.diagnose("warn", `incanto: animation '${ref}' — ${rig.reason}`);
6551
- this.entry.retargeted.set(ref, rig.clip);
6552
+ this.entry.retargeted.set(cacheKey, rig.clip);
6552
6553
  return rig.clip;
6553
6554
  }
6554
6555
  /** Masked upper/lower variants of a clip for two-layer playback (cached). */
@@ -28,6 +28,31 @@ interface FrameStats {
28
28
  meanLuma: number;
29
29
  /** Coarse mean colour per cell, rows top to bottom. */
30
30
  grid: GridCell[][];
31
+ /** What the frame is OF, or null when it is all background. */
32
+ subject: Subject | null;
33
+ }
34
+ /**
35
+ * The thing in the shot: everything that is not the background.
36
+ *
37
+ * `black` and `uniform` answer "did anything render at all". They do not answer
38
+ * the question that actually goes wrong — "is the thing IN the shot, and can
39
+ * you see it" — and every visual defect reported against the editor's model
40
+ * previews walked straight past them. A white square with a faint sky gradient
41
+ * is neither black nor uniform; a character drawn as a speck in the corner has
42
+ * a perfectly ordinary mean luminance.
43
+ */
44
+ interface Subject {
45
+ /** Bounding box in pixels, origin top-left. */
46
+ box: {
47
+ x: number;
48
+ y: number;
49
+ w: number;
50
+ h: number;
51
+ };
52
+ /** Share of the frame's pixels that are subject, 0..1. */
53
+ coverage: number;
54
+ /** Touching an edge — the shot is a crop, and something is outside it. */
55
+ clipped: boolean;
31
56
  }
32
57
  interface FrameStatsOptions {
33
58
  /** Cells across and down. Default 16x9 — 144 cells, printable. */
@@ -44,6 +44,98 @@ function cellMeans(pixels, width, height, cols, rows) {
44
44
  }
45
45
  return out;
46
46
  }
47
+ /**
48
+ * How far a pixel must sit from the background colour to count as subject.
49
+ * Chosen above the banding in a sky gradient and well below any real material.
50
+ */
51
+ const SUBJECT_DELTA = 24;
52
+ /** Subject smaller than this is present but not visible — say so differently. */
53
+ const TINY_COVERAGE = .01;
54
+ /**
55
+ * The background colour, read off the BORDER rather than voted for.
56
+ *
57
+ * A subject filling most of the shot would win a popularity contest and the
58
+ * report would invert — the sky would become "the thing", and a well-framed
59
+ * character would read as an empty frame. The edge of a frame is background in
60
+ * every shot this is asked about; where it is not, the subject is clipped, and
61
+ * that gets reported too.
62
+ */
63
+ function borderColour(pixels, width, height) {
64
+ let r = 0;
65
+ let g = 0;
66
+ let b = 0;
67
+ let n = 0;
68
+ const take = (x, y) => {
69
+ const o = (y * width + x) * 4;
70
+ r += pixels[o];
71
+ g += pixels[o + 1];
72
+ b += pixels[o + 2];
73
+ n += 1;
74
+ };
75
+ for (let x = 0; x < width; x++) {
76
+ take(x, 0);
77
+ take(x, height - 1);
78
+ }
79
+ for (let y = 1; y < height - 1; y++) {
80
+ take(0, y);
81
+ take(width - 1, y);
82
+ }
83
+ return n === 0 ? [
84
+ 0,
85
+ 0,
86
+ 0
87
+ ] : [
88
+ r / n,
89
+ g / n,
90
+ b / n
91
+ ];
92
+ }
93
+ /** Where the non-background pixels are, or null when there are none worth it. */
94
+ function findSubject(pixels, width, height) {
95
+ const global = borderColour(pixels, width, height);
96
+ const at = (x, y) => {
97
+ const o = (y * width + x) * 4;
98
+ return [
99
+ pixels[o],
100
+ pixels[o + 1],
101
+ pixels[o + 2]
102
+ ];
103
+ };
104
+ const dist = (a, b) => Math.abs(a[0] - b[0]) + Math.abs(a[1] - b[1]) + Math.abs(a[2] - b[2]);
105
+ let minX = width;
106
+ let minY = height;
107
+ let maxX = -1;
108
+ let maxY = -1;
109
+ let count = 0;
110
+ for (let y = 0; y < height; y++) {
111
+ const left = at(0, y);
112
+ const right = at(width - 1, y);
113
+ const row = dist(left, right) > SUBJECT_DELTA ? dist(left, global) <= dist(right, global) ? left : right : [
114
+ (left[0] + right[0]) / 2,
115
+ (left[1] + right[1]) / 2,
116
+ (left[2] + right[2]) / 2
117
+ ];
118
+ for (let x = 0; x < width; x++) {
119
+ if (dist(at(x, y), row) < SUBJECT_DELTA) continue;
120
+ count += 1;
121
+ if (x < minX) minX = x;
122
+ if (x > maxX) maxX = x;
123
+ if (y < minY) minY = y;
124
+ if (y > maxY) maxY = y;
125
+ }
126
+ }
127
+ if (maxX < 0) return null;
128
+ return {
129
+ box: {
130
+ x: minX,
131
+ y: minY,
132
+ w: maxX - minX + 1,
133
+ h: maxY - minY + 1
134
+ },
135
+ coverage: count / (width * height),
136
+ clipped: minX === 0 || minY === 0 || maxX === width - 1 || maxY === height - 1
137
+ };
138
+ }
47
139
  function frameStats(pixels, width, height, opts = {}) {
48
140
  const [cols, rows] = opts.grid ?? [16, 9];
49
141
  const means = cellMeans(pixels, width, height, cols, rows);
@@ -70,21 +162,30 @@ function frameStats(pixels, width, height, opts = {}) {
70
162
  }
71
163
  const cells = cols * rows;
72
164
  const meanLuma = total / cells;
165
+ const black = maxLuma <= BLACK_LEVEL;
73
166
  return {
74
167
  width,
75
168
  height,
76
- black: maxLuma <= BLACK_LEVEL,
169
+ black,
77
170
  uniform: maxLuma - minLuma < UNIFORM_EPSILON,
78
171
  meanLuma: Math.round(meanLuma * 1e3) / 1e3,
79
- grid
172
+ grid,
173
+ subject: black ? null : findSubject(pixels, width, height)
80
174
  };
81
175
  }
176
+ const pct = (v) => `${(v * 100).toFixed(1)}%`;
82
177
  /** The report a person or an agent reads. Verdict first, numbers after. */
83
178
  function frameText(stats) {
84
179
  const lines = [];
85
180
  if (stats.black) lines.push("BLACK SCREEN — nothing was drawn (no light, no camera, or nothing in view)");
86
181
  else if (stats.uniform) lines.push("one flat colour — the camera may be inside geometry, or only the sky is drawn");
182
+ else if (!stats.subject) lines.push("nothing but background — the camera is pointed away from everything in the scene");
183
+ else if (stats.subject.coverage < TINY_COVERAGE) lines.push(`the subject fills almost nothing (${pct(stats.subject.coverage)}) — the camera is too far back, or what you meant to see is not what got drawn`);
87
184
  lines.push(`frame ${stats.width}×${stats.height} · luminance ${stats.meanLuma.toFixed(2)}`);
185
+ if (stats.subject) {
186
+ const { box, coverage, clipped } = stats.subject;
187
+ lines.push(`subject ${box.w}×${box.h} at ${box.x},${box.y} · fills ${pct(coverage)} of frame` + (clipped ? " · CLIPPED by the frame edge" : ""));
188
+ }
88
189
  return lines.join("\n");
89
190
  }
90
191
  /**
@@ -193,5 +294,31 @@ function fromBase64(text) {
193
294
  for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
194
295
  return out;
195
296
  }
297
+ /**
298
+ * A `data:` PNG of the frame, scaled to fit `maxSide`.
299
+ *
300
+ * Rows are taken as they come, TOP-DOWN. `gl.readPixels` is bottom-up, which
301
+ * is why this wants flipping — but `Renderer3D.captureFrame` already flipped
302
+ * once, "rather than in each of" its consumers, and this is one of them.
303
+ * Flipping again shipped an upside-down picture that the unit test happily
304
+ * confirmed, because the test asserted the belief instead of the boundary.
305
+ */
306
+ function frameImage(pixels, width, height, makeCanvas, maxSide = 512) {
307
+ const full = makeCanvas(width, height);
308
+ const ctx = full.getContext("2d");
309
+ if (!ctx) return null;
310
+ const image = ctx.createImageData(width, height);
311
+ image.data.set(pixels.subarray(0, width * height * 4));
312
+ ctx.putImageData(image, 0, 0);
313
+ const scale = Math.min(1, maxSide / Math.max(width, height));
314
+ if (scale >= 1) return full.toDataURL("image/png");
315
+ const w = Math.max(1, Math.round(width * scale));
316
+ const h = Math.max(1, Math.round(height * scale));
317
+ const small = makeCanvas(w, h);
318
+ const sctx = small.getContext("2d");
319
+ if (!sctx) return full.toDataURL("image/png");
320
+ sctx.drawImage(full, 0, 0, w, h);
321
+ return small.toDataURL("image/png");
322
+ }
196
323
  //#endregion
197
- export { frameStats as a, frameSignature as i, diffSignatures as n, frameText as o, diffText as r, SIGNATURE_GRID as t };
324
+ export { frameSignature as a, frameImage as i, diffSignatures as n, frameStats as o, diffText as r, frameText as s, SIGNATURE_GRID as t };
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.
7
7
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-IyWCGe4q.js";
8
8
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
9
9
  import { i as applyParticlePreset, n as PARTICLE_PRESETS, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
10
- import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-DUxGnvq3.js";
10
+ import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-Ca3oV1fe.js";
11
11
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BoNg_MnF.js";
12
12
  import { t as duplicateNode } from "./duplicate-CRtihGmC.js";
13
13
  export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -3,7 +3,7 @@ import { v as diagnose } from "./loader-r49nDwB4.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-BQOeAid6.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-DUsBqvtk.js";
6
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-D1b0ydTS.js";
7
7
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
8
8
  //#region src/3d/physics/collider-lines.ts
9
9
  /**
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-Gifrjhnz.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-DFRbG0Bj.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-D16MVIPO.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CniOiWzN.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -161,6 +161,6 @@ function newUid() {
161
161
  //#endregion
162
162
  //#region src/index.ts
163
163
  /** Engine version. Kept in sync with package.json by the release pipeline. */
164
- const VERSION = "0.45.1";
164
+ const VERSION = "0.47.0";
165
165
  //#endregion
166
166
  export { findPath as a, preloadUrls as i, newUid as n, gridFromRows as o, assetUrls as r, VERSION as t };
@@ -50,14 +50,15 @@ async function settle(ms = VEIL_MS) {
50
50
  * `incanto-editor` server) serve. Used when a game passes `library: true`.
51
51
  */
52
52
  function devServerLibrary(token) {
53
- return async (query) => {
53
+ return async (query, callToken) => {
54
+ const bearer = callToken || token;
54
55
  const params = new URLSearchParams({
55
56
  tags: query.tags.join(","),
56
57
  page: String(query.page ?? 1),
57
58
  limit: String(query.limit ?? 24)
58
59
  });
59
60
  if (query.keyword?.trim()) params.set("keyword", query.keyword.trim());
60
- const res = await fetch(`/api/library?${params}`, { headers: token ? { "x-incanto-v8-token": token } : {} });
61
+ const res = await fetch(`/api/library?${params}`, { headers: bearer ? { "x-incanto-v8-token": bearer } : {} });
61
62
  if (res.status === 401) {
62
63
  const body = await res.json().catch(() => ({}));
63
64
  const error = new Error(body.error ?? "the asset library needs a Verse8 access token");
@@ -6,7 +6,7 @@ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
6
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-IyWCGe4q.js";
7
7
  import { n as registerGameplayBehaviors } from "./gameplay-BQOeAid6.js";
8
8
  import { t as registerNodes2D } from "./register-R2JTnIMw.js";
9
- import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-DUsBqvtk.js";
9
+ import { n as registerNodes3D, t as resolveEnvironmentHdri } from "./environment-presets-D1b0ydTS.js";
10
10
  import { t as registerNodesNet } from "./register-D651it1J.js";
11
11
  import { Box3, Euler, Matrix4, PerspectiveCamera, Quaternion, Vector3 } from "three";
12
12
  //#region src/test/framing.ts
@@ -345,12 +345,14 @@ function makeOracle(engine, player) {
345
345
  let outcome = null;
346
346
  let damage = 0;
347
347
  const offs = [];
348
+ let declaresEnd = false;
348
349
  /** First verdict wins — a run ends at its first conclusion, not its last. */
349
350
  const settle = (verdict) => {
350
351
  if (outcome === null) outcome = verdict;
351
352
  };
352
353
  const walk = (node) => {
353
354
  if (node.behavior) for (const signal of node.declaredSignalNames()) {
355
+ if (signal === "won" || signal === "lost" || signal === "flowChanged") declaresEnd = true;
354
356
  if (signal === "won") offs.push(node.on(signal, () => settle("won")));
355
357
  if (signal === "lost") offs.push(node.on(signal, () => settle("lost")));
356
358
  if (signal === "died" && node === player) offs.push(node.on(signal, () => settle("lost")));
@@ -368,6 +370,7 @@ function makeOracle(engine, player) {
368
370
  if (root) walk(root);
369
371
  return {
370
372
  outcome: () => outcome,
373
+ declaresEnd: () => declaresEnd,
371
374
  damage: () => damage,
372
375
  dispose: () => {
373
376
  for (const off of offs) off();
@@ -549,7 +552,8 @@ async function runOnce(json, seed, opts) {
549
552
  return {
550
553
  run,
551
554
  targets,
552
- signals
555
+ signals,
556
+ declaresEnd: oracle.declaresEnd()
553
557
  };
554
558
  }
555
559
  /** Play the scene `runs` times and report what happened. */
@@ -559,11 +563,13 @@ async function playtest(json, opts = {}) {
559
563
  const runs = [];
560
564
  let targets = [];
561
565
  let signals = [];
566
+ let declaresWin = false;
562
567
  for (let i = 0; i < count; i++) {
563
568
  const one = await runOnce(json, base + i, opts);
564
569
  runs.push(one.run);
565
570
  targets = one.targets;
566
571
  signals = one.signals;
572
+ declaresWin = declaresWin || one.declaresEnd;
567
573
  }
568
574
  const probe = await createPlaySession(json, {
569
575
  seed: base,
@@ -577,7 +583,8 @@ async function playtest(json, opts = {}) {
577
583
  targets,
578
584
  signals: [...new Set(signals)],
579
585
  actions,
580
- inertActions: []
586
+ inertActions: [],
587
+ declaresWin
581
588
  };
582
589
  }
583
590
  function pct(n, total) {
@@ -603,7 +610,7 @@ function playtestText(report) {
603
610
  lines.push("");
604
611
  const won = runs.filter((r) => r.outcome === "won");
605
612
  if (won.length > 0) lines.push(` ✓ reached "won" in ${pct(won.length, total)} median ${Math.round(median(won.map((r) => r.timeMs)) / 1e3)}s`);
606
- else lines.push(` ✗ never reached "won" in ${total} runs — nothing in this scene declares a win, or it cannot be reached`);
613
+ else lines.push(report.declaresWin ? ` ✗ never reached "won" in ${total} runs — a win IS declared, and no run got to it` : ` · no win declared in this scene nothing emits won/lost, so there is nothing to reach`);
607
614
  for (const bad of [
608
615
  "error",
609
616
  "fell",
@@ -1139,6 +1146,46 @@ function feelText(report) {
1139
1146
  return lines.join("\n");
1140
1147
  }
1141
1148
  //#endregion
1149
+ //#region src/test/verify-ladder.ts
1150
+ const MARK = {
1151
+ pass: "✓",
1152
+ fail: "✗",
1153
+ unmeasured: "?",
1154
+ skipped: "·"
1155
+ };
1156
+ function ladderVerdict(rungs, ctx = {}) {
1157
+ const failed = rungs.find((r) => r.status === "fail");
1158
+ const unmeasured = rungs.filter((r) => r.status === "unmeasured");
1159
+ const next = failed ? failed.fix ?? failed.summary : unmeasured[0]?.fix ?? unmeasured[0]?.summary ?? null;
1160
+ const ambiguous = rungs.length === 0 && (ctx.candidates?.length ?? 0) > 1;
1161
+ return {
1162
+ ok: !failed,
1163
+ rungs,
1164
+ unmeasured: unmeasured.map((r) => r.name),
1165
+ next: ambiguous ? "name the one you mean: `incanto-verify <scene.json>`" : next ?? null,
1166
+ candidates: ctx.candidates ?? []
1167
+ };
1168
+ }
1169
+ /** The ladder as a person or an agent reads it: every rung, then the one action. */
1170
+ function ladderText(v) {
1171
+ if (v.rungs.length === 0) {
1172
+ if (v.candidates.length > 1) return [
1173
+ `${v.candidates.length} scenes here, and guessing between them would verify the wrong one:`,
1174
+ ...v.candidates.map((c) => ` ${c}`),
1175
+ "",
1176
+ `next: ${v.next}`
1177
+ ].join("\n");
1178
+ return "nothing to verify — no scene was given and none was found";
1179
+ }
1180
+ const lines = v.rungs.map((r) => `${MARK[r.status]} ${r.name} — ${r.summary}`);
1181
+ lines.push("");
1182
+ if (!v.ok) lines.push("NOT verified.");
1183
+ else if (v.unmeasured.length > 0) lines.push(`passes what was measured — ${v.unmeasured.join(", ")} not measured.`);
1184
+ else lines.push("verified: it loads, it plays, and it draws.");
1185
+ if (v.next) lines.push(`next: ${v.next}`);
1186
+ return lines.join("\n");
1187
+ }
1188
+ //#endregion
1142
1189
  //#region src/test/index.ts
1143
1190
  /**
1144
1191
  * incanto/test — the browserless verification harness.
@@ -1370,7 +1417,7 @@ async function runScript(json, opts) {
1370
1417
  const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1371
1418
  await enablePhysics2D(engine);
1372
1419
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1373
- const { enablePhysics3D } = await import("./physics-3d-DFqMtBUD.js").then((n) => n.r);
1420
+ const { enablePhysics3D } = await import("./physics-3d-CSoGjM8P.js").then((n) => n.r);
1374
1421
  await enablePhysics3D(engine);
1375
1422
  }
1376
1423
  const failures = [];
@@ -1486,7 +1533,7 @@ async function createPlaySession(json, opts = {}) {
1486
1533
  const { enablePhysics2D } = await import("./physics-2d-BmgXBNDB.js").then((n) => n.r);
1487
1534
  await enablePhysics2D(engine);
1488
1535
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
1489
- const { enablePhysics3D } = await import("./physics-3d-DFqMtBUD.js").then((n) => n.r);
1536
+ const { enablePhysics3D } = await import("./physics-3d-CSoGjM8P.js").then((n) => n.r);
1490
1537
  await enablePhysics3D(engine);
1491
1538
  }
1492
1539
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -1524,4 +1571,4 @@ async function createPlaySession(json, opts = {}) {
1524
1571
  };
1525
1572
  }
1526
1573
  //#endregion
1527
- export { framingText as _, registerAllNodes as a, feelReport as c, facingText as d, failingReplays as f, describeFraming as g, playtestText as h, findFloatingProps as i, feelText as l, playtest as m, createPlaySession as n, runScript as o, findPlayer as p, describeCapture as r, validateScene as s, captureScene as t, facingReport as u };
1574
+ export { playtestText as _, registerAllNodes as a, ladderText as c, feelText as d, facingReport as f, playtest as g, findPlayer as h, findFloatingProps as i, ladderVerdict as l, failingReplays as m, createPlaySession as n, runScript as o, facingText as p, describeCapture as r, validateScene as s, captureScene as t, feelReport as u, describeFraming as v, framingText as y };
package/dist/test.d.ts CHANGED
@@ -165,6 +165,15 @@ interface PlaytestReport {
165
165
  actions: string[];
166
166
  /** Actions that never changed the world in any run. */
167
167
  inertActions: string[];
168
+ /**
169
+ * Does anything in this scene declare an END?
170
+ *
171
+ * "No run reached a win" has two causes that need opposite fixes: a goal
172
+ * that cannot be reached, and a scene with no goal in it. A walkabout
173
+ * template is not a broken game, and reporting one as a failure sends its
174
+ * author looking for a bug that was never there.
175
+ */
176
+ declaresWin: boolean;
168
177
  }
169
178
  /**
170
179
  * Who the driver is playing as.
@@ -190,6 +199,51 @@ declare function failingReplays(report: PlaytestReport): Array<{
190
199
  replay: JsonValue;
191
200
  }>;
192
201
  //#endregion
202
+ //#region src/test/verify-ladder.d.ts
203
+ /**
204
+ * The verification ladder, as one answer.
205
+ *
206
+ * The rungs already exist — `incanto-check` says the scene loads,
207
+ * `incanto-playtest` says it can be finished, `incanto-frame` says something
208
+ * was drawn. They are documented together and used apart, because using them
209
+ * together means knowing three things nobody writes down:
210
+ *
211
+ * the ORDER (a scene that does not load cannot be played or drawn, so the
212
+ * first red rung is the only one worth reading)
213
+ * that an unmeasured rung is not a failing one — "no dev server" means the
214
+ * question was never asked, and answering it "fail" sends an agent editing
215
+ * a scene that is fine
216
+ * what to do NEXT, which is one sentence and never the whole report
217
+ *
218
+ * Pure: the rungs are run by the CLI, this decides what they add up to.
219
+ */
220
+ type RungStatus = "pass" | "fail" | "unmeasured" | "skipped";
221
+ interface RungResult {
222
+ name: string;
223
+ status: RungStatus;
224
+ /** One line: what this rung found. */
225
+ summary: string;
226
+ /** What to do about it, when there is something to do. */
227
+ fix?: string;
228
+ }
229
+ interface LadderVerdict {
230
+ ok: boolean;
231
+ rungs: RungResult[];
232
+ /** Rungs that could not be measured — questions unasked, not answers. */
233
+ unmeasured: string[];
234
+ /** The single next action, or null when there is nothing to do. */
235
+ next: string | null;
236
+ /** Scenes found when none was named (empty once one is being verified). */
237
+ candidates: string[];
238
+ }
239
+ interface LadderContext {
240
+ /** Scenes found when none was named — none, or more than one to choose from. */
241
+ candidates?: string[];
242
+ }
243
+ declare function ladderVerdict(rungs: RungResult[], ctx?: LadderContext): LadderVerdict;
244
+ /** The ladder as a person or an agent reads it: every rung, then the one action. */
245
+ declare function ladderText(v: LadderVerdict): string;
246
+ //#endregion
193
247
  //#region src/test/index.d.ts
194
248
  /** One thing that is not standing where it should be. */
195
249
  interface GroundingIssue {
@@ -371,4 +425,4 @@ interface PlaySession {
371
425
  */
372
426
  declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
373
427
  //#endregion
374
- export { type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, NodeCapture, type Outcome, PlaySession, PlaySessionOptions, type PlaytestOptions, type PlaytestReport, type PlaytestRun, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, type Where, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, playtest, playtestText, registerAllNodes, runScript, validateScene };
428
+ export { type FacingOptions, type FacingReport, type FeelOptions, type FeelReport, type FramingEntry, type FramingOptions, type FramingReport, GroundingIssue, GroundingOptions, type LadderVerdict, 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, auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, playtest, playtestText, registerAllNodes, runScript, validateScene };
package/dist/test.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { r as auditScene } from "./replay-DilbZgQI.js";
2
- import { _ as framingText, a as registerAllNodes, c as feelReport, d as facingText, f as failingReplays, g as describeFraming, h as playtestText, i as findFloatingProps, l as feelText, m as playtest, n as createPlaySession, o as runScript, p as findPlayer, r as describeCapture, s as validateScene, t as captureScene, u as facingReport } from "./test-DD5scWfm.js";
3
- export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, playtest, playtestText, registerAllNodes, runScript, validateScene };
2
+ import { _ as playtestText, a as registerAllNodes, c as ladderText, d as feelText, f as facingReport, g as playtest, h as findPlayer, i as findFloatingProps, l as ladderVerdict, m as failingReplays, n as createPlaySession, o as runScript, p as facingText, r as describeCapture, s as validateScene, t as captureScene, u as feelReport, v as describeFraming, y as framingText } from "./test-E4-otKqK.js";
3
+ export { auditScene, captureScene, createPlaySession, describeCapture, describeFraming, facingReport, facingText, failingReplays, feelReport, feelText, findFloatingProps, findPlayer, framingText, ladderText, ladderVerdict, playtest, playtestText, registerAllNodes, runScript, validateScene };
package/dist/vite.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as FrameSignature } from "./frame-report-Ct8XgsmV.js";
1
+ import { n as FrameSignature } from "./frame-report-DZ70IY26.js";
2
2
 
3
3
  //#region src/vite/discover.d.ts
4
4
  /**
@@ -29,9 +29,20 @@ interface FrameHost {
29
29
  middlewares: {
30
30
  use(path: string, handler: (req: FrameReq, res: FrameRes) => void): void;
31
31
  };
32
- /** Vite's HMR channel. `ws` on v5, `hot` on v6+ — both are accepted. */
32
+ /**
33
+ * Vite's HMR channel, which has moved twice. `ws` on v5, `hot` on v6+, and
34
+ * since the Environment API the one that actually reaches the BROWSER is
35
+ * `environments.client.hot` — on v8 the top-level `hot` still exists and
36
+ * still accepts `send()` without error, it simply arrives nowhere. Every
37
+ * candidate is used, because a silent no-op is the worst of the three.
38
+ */
33
39
  ws?: FrameChannel;
34
40
  hot?: FrameChannel;
41
+ environments?: {
42
+ client?: {
43
+ hot?: FrameChannel;
44
+ };
45
+ };
35
46
  }
36
47
  interface FrameChannel {
37
48
  send(payload: {
@@ -205,6 +216,8 @@ interface IncantoLibraryOptions {
205
216
  token?: string;
206
217
  /** Upstream catalog (default: the agent8 backend). */
207
218
  url?: string;
219
+ /** Verse8 API root for the sign-in flow (default: the public one). */
220
+ authUrl?: string;
208
221
  }
209
222
  /**
210
223
  * Serve the agent8 asset catalog at `/api/library` on the dev server.
@@ -232,7 +245,25 @@ declare function incantoLibrary(opts?: IncantoLibraryOptions): {
232
245
  apply: "serve";
233
246
  configureServer(server: DevServer): void;
234
247
  };
248
+ /**
249
+ * A token, shown the way a wallet address is: enough to recognise WHICH token
250
+ * is in use, never enough to use it. Anything short enough that six and four
251
+ * characters would give most of it away is hidden entirely.
252
+ */
253
+ declare function maskToken(token: string): string;
254
+ /** Where the server's token came from, and what it looks like. Never the token. */
255
+ declare function serveTokenStatus(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions): Promise<void>;
256
+ /**
257
+ * The Verse8 device-authorization flow (RFC 8628), proxied.
258
+ *
259
+ * The same two calls agent8's own local-dev login makes: `authorize` returns a
260
+ * code and a page for the human to approve, `token` is polled until they have.
261
+ * It goes through the dev server for the same reason the catalog does — the
262
+ * page cannot reach that host itself — and it means a developer never has to
263
+ * find, copy or paste a token at all.
264
+ */
265
+ declare function serveDeviceAuth(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
235
266
  /** @internal Exported for tests — the whole request/response behaviour. */
236
267
  declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
237
268
  //#endregion
238
- export { type FrameHost, IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, listeningPorts, parseProcNetTcp, resolveSceneFile, sceneFacts, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList };
269
+ export { type FrameHost, IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, listeningPorts, maskToken, parseProcNetTcp, resolveSceneFile, sceneFacts, serveDeviceAuth, serveFrameEndpoints, serveLibrary, serveSceneFile, serveSceneList, serveTokenStatus };