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.
- package/bin/incanto-check.mjs +5 -1
- package/bin/incanto-feel.mjs +5 -1
- package/bin/incanto-frame.mjs +57 -6
- package/bin/incanto-model.mjs +79 -10
- package/bin/incanto-playtest.mjs +5 -1
- package/bin/incanto-verify.mjs +214 -0
- package/dist/2d.d.ts +1 -1
- package/dist/2d.js +1 -1
- package/dist/3d.d.ts +90 -5
- package/dist/3d.js +92 -5
- package/dist/{create-game-DFRbG0Bj.js → create-game-CniOiWzN.js} +1 -1
- package/dist/{create-game-Gifrjhnz.js → create-game-D16MVIPO.js} +24 -5
- package/dist/{editor-switch-DVwIGZdK.d.ts → editor-switch-DAvWQeld.d.ts} +1 -1
- package/dist/editor.js +4674 -4346
- package/dist/{environment-presets-DUsBqvtk.js → environment-presets-D1b0ydTS.js} +5 -4
- package/dist/{frame-report-Ct8XgsmV.d.ts → frame-report-DZ70IY26.d.ts} +25 -0
- package/dist/{frame-report-Lr3VO24R.js → frame-report-njybhZon.js} +130 -3
- package/dist/index.js +1 -1
- package/dist/{physics-3d-DFqMtBUD.js → physics-3d-CSoGjM8P.js} +1 -1
- package/dist/react.js +1 -1
- package/dist/{src-DUxGnvq3.js → src-Ca3oV1fe.js} +1 -1
- package/dist/{teardown-yePMOE1K.js → teardown-BKTCzLek.js} +3 -2
- package/dist/{test-DD5scWfm.js → test-E4-otKqK.js} +54 -7
- package/dist/test.d.ts +55 -1
- package/dist/test.js +2 -2
- package/dist/vite.d.ts +34 -3
- package/dist/vite.js +104 -12
- package/editor/assets/{agent8-B25qJ5Wo.js → agent8-_007gPF8.js} +1 -1
- package/editor/assets/{debug-CUsz-pT8.js → debug-0DI_MJaq.js} +1 -1
- package/editor/assets/{index-3u3GVteP.js → index-B-6eYZEi.js} +173 -96
- package/editor/index.html +1 -1
- package/package.json +3 -2
- package/skills/incanto-3d-models.md +30 -0
- package/skills/incanto-editor.md +6 -1
- package/skills/incanto-verifying-your-game.md +60 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/tps-3d/package.json +1 -1
- 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(
|
|
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(
|
|
6545
|
+
this.entry.retargeted.set(cacheKey, result.clip);
|
|
6545
6546
|
return result.clip;
|
|
6546
6547
|
}
|
|
6547
|
-
const cached = this.entry.retargeted.get(
|
|
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(
|
|
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
|
|
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 {
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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:
|
|
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-
|
|
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 —
|
|
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-
|
|
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-
|
|
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 {
|
|
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
|
|
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-
|
|
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
|
-
/**
|
|
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 };
|