incanto 0.47.0 → 0.49.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-frame.mjs +58 -1
- package/bin/incanto-logs.mjs +127 -0
- package/bin/incanto-model.mjs +13 -1
- package/bin/incanto-playtest.mjs +7 -0
- package/bin/incanto-verify.mjs +41 -2
- package/dist/2d.d.ts +21 -12
- package/dist/2d.js +57 -3
- package/dist/3d.d.ts +38 -1
- package/dist/3d.js +114 -4
- package/dist/{create-game-CniOiWzN.js → create-game-BgV6UbVA.js} +5 -5
- package/dist/{create-game-D16MVIPO.js → create-game-DFBjMetZ.js} +83 -6
- package/dist/{duplicate-CRtihGmC.js → duplicate-CI9WF_bg.js} +1 -1
- package/dist/{environment-presets-D1b0ydTS.js → environment-presets-CZOH5TY5.js} +15 -20
- package/dist/{gameplay-BQOeAid6.js → gameplay-02Btmmjn.js} +80 -9
- package/dist/gameplay.d.ts +17 -0
- package/dist/gameplay.js +1 -1
- package/dist/index.d.ts +139 -1
- package/dist/index.js +8 -7
- package/dist/{loader-r49nDwB4.js → loader-DwazzlQb.js} +36 -6
- package/dist/log-report-lxrQY9cH.js +0 -0
- package/dist/net.js +3 -3
- package/dist/{physics-2d-BmgXBNDB.js → physics-2d-vyCBfACH.js} +3 -3
- package/dist/{physics-3d-CSoGjM8P.js → physics-3d-DpRqw8Mz.js} +4 -4
- package/dist/react.js +1 -1
- package/dist/{register-R2JTnIMw.js → register-BNPZYJmd.js} +23 -22
- package/dist/{register-D651it1J.js → register-CB11yp21.js} +2 -2
- package/dist/{register-BSXV8T9F.js → register-uvaZj1KX.js} +62 -2
- package/dist/{replay-DilbZgQI.js → replay-C0XJIsO7.js} +1 -1
- package/dist/sprite-animation-CMr6f1K2.d.ts +44 -0
- package/dist/{particle-sim-Bw7hB93B.js → sprite-animation-D_p28jwU.js} +63 -1
- package/dist/{src-Ca3oV1fe.js → src-DF4gCsqO.js} +1 -1
- package/dist/{test-E4-otKqK.js → test-DRna_BQU.js} +210 -30
- package/dist/test.d.ts +38 -6
- package/dist/test.js +2 -2
- package/dist/vite.js +41 -4
- package/editor/assets/{agent8-_007gPF8.js → agent8-DCW4TgDt.js} +1 -1
- package/editor/assets/{debug-0DI_MJaq.js → debug-RC6qts6S.js} +1 -1
- package/editor/assets/{index-B-6eYZEi.js → index-5dEIhvsf.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +3 -2
- package/schemas/scene.schema.json +10 -3
- package/skills/incanto-3d-character.md +13 -2
- package/skills/incanto-3d-models.md +40 -0
- package/skills/incanto-assets.md +15 -0
- package/skills/incanto-building-2d-games.md +57 -5
- package/skills/incanto-building-3d-games.md +7 -0
- package/skills/incanto-gameplay-behaviors.md +18 -1
- package/skills/incanto-hud.md +39 -0
- package/skills/incanto-node-reference.md +5 -3
- package/skills/incanto-physics-and-input.md +1 -1
- package/skills/incanto-playtesting.md +9 -2
- package/skills/incanto-verifying-your-game.md +67 -0
- 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
package/bin/incanto-frame.mjs
CHANGED
|
@@ -26,6 +26,7 @@ const { listeningPorts, parseProcNetTcp } = await import(
|
|
|
26
26
|
pathToFileURL(join(PKG, 'dist', 'vite.js')).href
|
|
27
27
|
);
|
|
28
28
|
const { frameText, diffText } = await import(pathToFileURL(join(PKG, 'dist', '3d.js')).href);
|
|
29
|
+
const { parseDrive } = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
29
30
|
|
|
30
31
|
const args = process.argv.slice(2);
|
|
31
32
|
const asJson = args.includes('--json');
|
|
@@ -39,6 +40,8 @@ const flag = (name) => {
|
|
|
39
40
|
const gridArg = flag('--grid');
|
|
40
41
|
const outArg = flag('--out');
|
|
41
42
|
const sizeArg = flag('--size');
|
|
43
|
+
const driveArg = flag('--do');
|
|
44
|
+
const driveFile = flag('--do-file');
|
|
42
45
|
const remember = flag('--remember');
|
|
43
46
|
const diff = flag('--diff');
|
|
44
47
|
const threshold = flag('--threshold');
|
|
@@ -49,6 +52,8 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
49
52
|
--grid WxH cells across and down (default 16x9)
|
|
50
53
|
--out FILE also write the frame as a PNG you can look at
|
|
51
54
|
--size N longest side of that PNG (default 512)
|
|
55
|
+
--do SCRIPT drive the game first, THEN capture (see below)
|
|
56
|
+
--do-file FILE the same script, from a file
|
|
52
57
|
--port N skip discovery and use this port
|
|
53
58
|
|
|
54
59
|
--remember [L] keep this frame as the baseline named L (default "last")
|
|
@@ -64,6 +69,17 @@ the frame it fills. When that is not enough, look:
|
|
|
64
69
|
|
|
65
70
|
incanto-frame --out shot.png # then open it, or read it
|
|
66
71
|
|
|
72
|
+
Every interesting state in a game is downstream of input, so --do drives the
|
|
73
|
+
running game before capturing — the same words incanto-play takes:
|
|
74
|
+
|
|
75
|
+
incanto-frame --do "vector move 0 1; step 3000; vector move 0 0" --out bridge.png
|
|
76
|
+
incanto-frame --do "press jump; step 400" --out midair.png
|
|
77
|
+
|
|
78
|
+
press/release ACTION · vector ACTION X Y · key CODE down|up
|
|
79
|
+
pointer DX DY · step MS (semicolons or newlines separate)
|
|
80
|
+
|
|
81
|
+
step waits on the real clock: this is the live game, with its own loop.
|
|
82
|
+
|
|
67
83
|
|
|
68
84
|
The report of ONE frame has a floor: a one-pixel seam is a fraction of a level
|
|
69
85
|
once a cell is averaged, and nothing can tell it apart from a thin rope in the
|
|
@@ -111,6 +127,24 @@ function bsdListeningPorts() {
|
|
|
111
127
|
}
|
|
112
128
|
}
|
|
113
129
|
|
|
130
|
+
/** The script to run before capturing, from `--do` or `--do-file`. */
|
|
131
|
+
const driveScript = driveFile ? read(driveFile) : driveArg;
|
|
132
|
+
if (driveFile && driveScript === null) {
|
|
133
|
+
console.error(`cannot read ${driveFile}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
if (driveScript) {
|
|
137
|
+
const parsed = parseDrive(driveScript);
|
|
138
|
+
if (parsed.error) {
|
|
139
|
+
// Refuse here rather than in the page: a bad command that ran nothing would
|
|
140
|
+
// hand back the boot screen, and nothing about that frame would say so.
|
|
141
|
+
console.error(
|
|
142
|
+
`${parsed.error}\n commands: press/release ACTION · vector ACTION X Y · key CODE down|up · pointer DX DY · step MS`,
|
|
143
|
+
);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
const explicit = args.includes('--port') ? Number(args[args.indexOf('--port') + 1]) : null;
|
|
115
149
|
const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
116
150
|
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
@@ -150,12 +184,20 @@ if (!found) {
|
|
|
150
184
|
const params = new URLSearchParams();
|
|
151
185
|
if (gridArg !== null) params.set('grid', gridArg);
|
|
152
186
|
if (outArg) params.set('image', sizeArg || '512');
|
|
187
|
+
if (driveScript) {
|
|
188
|
+
params.set('drive', driveScript);
|
|
189
|
+
// Tell the server how long to wait: the script runs in real time.
|
|
190
|
+
const totalMs = parseDrive(driveScript).totalMs;
|
|
191
|
+
params.set('driveMs', String(totalMs));
|
|
192
|
+
}
|
|
153
193
|
if (remember !== null) params.set('remember', remember);
|
|
154
194
|
if (diff !== null) params.set('diff', diff);
|
|
155
195
|
if (threshold) params.set('threshold', threshold);
|
|
156
196
|
const q = params.toString();
|
|
157
197
|
const res = await fetch(`http://${found.host}:${found.port}/__incanto/frame${q ? `?${q}` : ''}`, {
|
|
158
|
-
|
|
198
|
+
// Outlast the server's own budget, or the CLI gives up on an answer that
|
|
199
|
+
// was on its way.
|
|
200
|
+
signal: AbortSignal.timeout(15_000 + (driveScript ? parseDrive(driveScript).totalMs * 2 : 0)),
|
|
159
201
|
});
|
|
160
202
|
const body = await res.json().catch(() => null);
|
|
161
203
|
|
|
@@ -184,6 +226,18 @@ if (!res.ok || !body?.report) {
|
|
|
184
226
|
}
|
|
185
227
|
|
|
186
228
|
const report = body.report;
|
|
229
|
+
// A drive that advanced no frames moved nothing: the inputs landed on a game
|
|
230
|
+
// the browser had frozen, and the frame that came back is the one from before.
|
|
231
|
+
// Saying nothing here would be handing over a picture that quietly lies.
|
|
232
|
+
if (driveScript && report.droveFrames === 0) {
|
|
233
|
+
console.error(
|
|
234
|
+
'the drive ran but the game never advanced — 0 frames drawn.\n' +
|
|
235
|
+
' The page is hidden (a window covered by another counts) and the browser\n' +
|
|
236
|
+
' stops animating it, so the inputs landed on a frozen game and this frame\n' +
|
|
237
|
+
' is unchanged. Bring the preview to the front and try again.',
|
|
238
|
+
);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
187
241
|
if (report.error) {
|
|
188
242
|
console.error(`the page could not capture a frame: ${report.error}`);
|
|
189
243
|
process.exit(1);
|
|
@@ -215,6 +269,9 @@ if (asJson) {
|
|
|
215
269
|
);
|
|
216
270
|
} else {
|
|
217
271
|
const lines = [frameText(report)];
|
|
272
|
+
if (typeof report.droveFrames === 'number') {
|
|
273
|
+
lines.push(`drove ${report.droveFrames} frames before capturing`);
|
|
274
|
+
}
|
|
218
275
|
if (wroteImage) lines.push(`wrote ${wroteImage} — open it, or read it`);
|
|
219
276
|
if (body.diffError === 'no-baseline') {
|
|
220
277
|
lines.push(
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* incanto-logs — what the RUNNING game is saying.
|
|
4
|
+
*
|
|
5
|
+
* bunx incanto-logs
|
|
6
|
+
* bunx incanto-logs --json
|
|
7
|
+
*
|
|
8
|
+
* `incanto-frame` gets the pixels out of the browser; this gets the WORDS. The
|
|
9
|
+
* engine writes diagnostics for exactly this purpose — a model whose fit came
|
|
10
|
+
* out at 934x, an asset that 404'd, a node that quarantined itself — and until
|
|
11
|
+
* now every one of them reached a human only, in the debug overlay's logs
|
|
12
|
+
* panel. An agent could see a game and not hear it.
|
|
13
|
+
*
|
|
14
|
+
* Unlike a frame this needs no render, so it answers from a tab the browser has
|
|
15
|
+
* stopped drawing.
|
|
16
|
+
*
|
|
17
|
+
* Exit 1 when the game reports something wrong: an error logged, an error
|
|
18
|
+
* swallowed to keep it alive, or an asset that never loaded.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
|
+
const { listeningPorts } = await import(pathToFileURL(join(PKG, 'dist', 'vite.js')).href);
|
|
27
|
+
const { logText } = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const asJson = args.includes('--json');
|
|
31
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
32
|
+
console.error(`Usage: incanto-logs [--json] [--port N]
|
|
33
|
+
|
|
34
|
+
What the running game is saying: its warnings and errors, the errors it
|
|
35
|
+
swallowed to keep going, the assets that never loaded, and its frame rate.
|
|
36
|
+
|
|
37
|
+
Run your game's dev server and open the page first — the log buffer is in the
|
|
38
|
+
browser, not in the dev server. This needs no render, so a page the browser has
|
|
39
|
+
stopped drawing still answers.
|
|
40
|
+
|
|
41
|
+
Exit 1 when something is wrong.`);
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const read = (path) => {
|
|
46
|
+
try {
|
|
47
|
+
return readFileSync(path, 'utf-8');
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Ports listening on a machine with no `/proc` (macOS, BSD) — see incanto-frame. */
|
|
54
|
+
function bsdListeningPorts() {
|
|
55
|
+
try {
|
|
56
|
+
const { execFileSync } = createRequire(import.meta.url)('node:child_process');
|
|
57
|
+
const out = execFileSync('lsof', ['-nP', '-iTCP', '-sTCP:LISTEN'], {
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
timeout: 1500,
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
});
|
|
62
|
+
const ports = new Set();
|
|
63
|
+
for (const line of out.split('\n')) {
|
|
64
|
+
const m = line.match(/:(\d+)\s*\(LISTEN\)/);
|
|
65
|
+
if (m) ports.add(Number(m[1]));
|
|
66
|
+
}
|
|
67
|
+
return [...ports];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const explicit = args.includes('--port') ? Number(args[args.indexOf('--port') + 1]) : null;
|
|
74
|
+
const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
75
|
+
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
76
|
+
|
|
77
|
+
let found = null;
|
|
78
|
+
outer: for (const port of ports) {
|
|
79
|
+
for (const host of ['localhost', '127.0.0.1']) {
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
|
|
82
|
+
signal: AbortSignal.timeout(300),
|
|
83
|
+
});
|
|
84
|
+
if (!res.ok) continue;
|
|
85
|
+
const body = await res.json();
|
|
86
|
+
if (body?.incanto) {
|
|
87
|
+
found = { host, port };
|
|
88
|
+
break outer;
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
// not listening, not ours, or not answering — next
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!found) {
|
|
97
|
+
console.error(
|
|
98
|
+
'no incanto dev server found.\n' +
|
|
99
|
+
' Start your game (the dev server) and try again — the log buffer lives in\n' +
|
|
100
|
+
' the running page, so there has to be one.',
|
|
101
|
+
);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const res = await fetch(`http://${found.host}:${found.port}/__incanto/logs`, {
|
|
106
|
+
signal: AbortSignal.timeout(10_000),
|
|
107
|
+
});
|
|
108
|
+
const body = await res.json().catch(() => null);
|
|
109
|
+
|
|
110
|
+
if (res.status === 504 || body?.error === 'no-page-connected') {
|
|
111
|
+
console.error(
|
|
112
|
+
`the dev server is running on :${found.port}, but no page answered.\n` +
|
|
113
|
+
' Open the preview in a browser — the log buffer is there, not in the server.',
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok || !body?.report || body.report.error) {
|
|
118
|
+
console.error(
|
|
119
|
+
`could not read the game's logs: ${body?.report?.error ?? body?.error ?? res.status}`,
|
|
120
|
+
);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(asJson ? JSON.stringify(body.report, null, 2) : logText(body.report));
|
|
125
|
+
// `exitCode`, never `process.exit()` — stdout to a pipe is written
|
|
126
|
+
// asynchronously and exiting discards what has not flushed.
|
|
127
|
+
process.exitCode = body.report.ok ? 0 : 1;
|
package/bin/incanto-model.mjs
CHANGED
|
@@ -353,7 +353,19 @@ if (args.json) {
|
|
|
353
353
|
// The verdict is the part you act on, so it goes last — closest to the
|
|
354
354
|
// prompt, where a reader stops.
|
|
355
355
|
out.push('', verdictText(verdict));
|
|
356
|
-
|
|
356
|
+
// A rig that takes the library clips gets the WHOLE character: the model on
|
|
357
|
+
// its own is a statue, and the steps from there (a dynamic body under the
|
|
358
|
+
// controller, the skin fitted to the capsule and dropped to its feet, the
|
|
359
|
+
// clips declared, the input actions that make anything read the keyboard)
|
|
360
|
+
// are documented in four different places and all needed at once.
|
|
361
|
+
if (verdict.characterJson) {
|
|
362
|
+
const { assets, input, node } = verdict.characterJson;
|
|
363
|
+
out.push(
|
|
364
|
+
'',
|
|
365
|
+
'paste a playable character into your scene:',
|
|
366
|
+
JSON.stringify({ assets, input, node }, null, 2),
|
|
367
|
+
);
|
|
368
|
+
} else if (verdict.sceneJson) {
|
|
357
369
|
const { assets, node } = verdict.sceneJson;
|
|
358
370
|
out.push(
|
|
359
371
|
'',
|
package/bin/incanto-playtest.mjs
CHANGED
|
@@ -34,6 +34,8 @@ function parseArgs(argv) {
|
|
|
34
34
|
else if (a === '--runs') args.runs = Number(argv[++i]);
|
|
35
35
|
else if (a === '--seconds') args.seconds = Number(argv[++i]);
|
|
36
36
|
else if (a === '--seed') args.seed = Number(argv[++i]);
|
|
37
|
+
else if (a === '--fall-below') args.fallBelow = Number(argv[++i]);
|
|
38
|
+
else if (a === '--reach-radius') args.reachRadius = Number(argv[++i]);
|
|
37
39
|
else if (a === '--out') args.out = argv[++i];
|
|
38
40
|
else if (a === '--no-replays') args.noReplays = true;
|
|
39
41
|
else if (a === '--json') args.json = true;
|
|
@@ -53,6 +55,9 @@ if (args.help || !args.scene) {
|
|
|
53
55
|
--runs N independent seeded runs (default 20)
|
|
54
56
|
--seconds N simulated seconds per run before calling it stuck (default 60)
|
|
55
57
|
--seed N first seed; run i uses seed+i (default 1)
|
|
58
|
+
--fall-below Y left the world past this y (default: 50 under the spawn in
|
|
59
|
+
3D, 1000 px under it in 2D — 2D counts DOWN the screen)
|
|
60
|
+
--reach-radius R how close counts as "reached" (default 2 in 3D, 32 px in 2D)
|
|
56
61
|
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
57
62
|
--out DIR where failing replays go (default .incanto/playtest)
|
|
58
63
|
--no-replays report only, write nothing
|
|
@@ -101,6 +106,8 @@ try {
|
|
|
101
106
|
seed: args.seed,
|
|
102
107
|
behaviors,
|
|
103
108
|
stubMissingBehaviors: !args.behaviors,
|
|
109
|
+
...(Number.isFinite(args.fallBelow) ? { fallBelow: args.fallBelow } : {}),
|
|
110
|
+
...(Number.isFinite(args.reachRadius) ? { reachRadius: args.reachRadius } : {}),
|
|
104
111
|
// sub-scenes resolve relative to the scene file, as the loader does
|
|
105
112
|
resolveScene: (p) => JSON.parse(readFileSync(resolve(sceneDir, p), 'utf-8')),
|
|
106
113
|
});
|
package/bin/incanto-verify.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* loads incanto-check the scene is legal and every asset resolves
|
|
14
14
|
* plays incanto-playtest a seeded run can actually finish it
|
|
15
15
|
* draws incanto-frame the GPU drew something, and the subject is in shot
|
|
16
|
+
* says incanto-logs what the running game is complaining about
|
|
16
17
|
*
|
|
17
18
|
* Exit 1 when a rung FAILED. An unmeasured rung is not a failure — it prints
|
|
18
19
|
* what to arrange, and says so in the summary.
|
|
@@ -41,6 +42,7 @@ Walks the whole ladder and tells you the ONE thing to do next:
|
|
|
41
42
|
loads the scene is legal and every asset resolves (incanto-check)
|
|
42
43
|
plays a seeded run can actually finish it (incanto-playtest)
|
|
43
44
|
draws the GPU drew something and the subject is in shot (incanto-frame)
|
|
45
|
+
says the running game logs no errors and lost no assets (incanto-logs)
|
|
44
46
|
|
|
45
47
|
Without a scene it looks for one *.scene.json under the current directory.
|
|
46
48
|
"draws" needs a dev server with the page open; without one it is reported as
|
|
@@ -48,10 +50,18 @@ NOT MEASURED rather than failed — a missing measurement is not a broken game.`
|
|
|
48
50
|
process.exit(0);
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The scene to verify: the one named, or the only one that can be found.
|
|
55
|
+
*
|
|
56
|
+
* ALWAYS the same shape. It used to return a bare string for a named scene and
|
|
57
|
+
* an object for a search, and the caller destructures — so `{ scene } = "a.json"`
|
|
58
|
+
* was `undefined` and naming your scene was the one way to make the whole
|
|
59
|
+
* ladder do nothing. It only ever worked when you named nothing, which is how
|
|
60
|
+
* it was run while being written.
|
|
61
|
+
*/
|
|
52
62
|
function findScene() {
|
|
53
63
|
const named = argv.find((a) => !a.startsWith('-') && a.endsWith('.json'));
|
|
54
|
-
if (named) return named;
|
|
64
|
+
if (named) return { scene: named, candidates: [named] };
|
|
55
65
|
const found = [];
|
|
56
66
|
const walk = (dir, depth) => {
|
|
57
67
|
if (depth > 4 || found.length > 8) return;
|
|
@@ -190,6 +200,35 @@ if (rungs[0].status === 'pass') {
|
|
|
190
200
|
* deprecation notice lands on stdout ahead of the report, and treating that as
|
|
191
201
|
* "the tool could not run" is how this reported a healthy scene as broken.
|
|
192
202
|
*/
|
|
203
|
+
// ---- says ---------------------------------------------------------------
|
|
204
|
+
{
|
|
205
|
+
const r = run('logs', ['--json']);
|
|
206
|
+
const report = safeJson(r.stdout);
|
|
207
|
+
if (!report) {
|
|
208
|
+
rungs.push({
|
|
209
|
+
name: 'says',
|
|
210
|
+
status: 'unmeasured',
|
|
211
|
+
summary: firstLine(r.stderr) ?? 'no page to ask',
|
|
212
|
+
fix: 'open the preview page and run this again — the log buffer is in the browser',
|
|
213
|
+
});
|
|
214
|
+
} else if (report.ok) {
|
|
215
|
+
rungs.push({ name: 'says', status: 'pass', summary: 'no errors, no failed assets' });
|
|
216
|
+
} else {
|
|
217
|
+
const asset = report.assetErrors?.[0];
|
|
218
|
+
rungs.push({
|
|
219
|
+
name: 'says',
|
|
220
|
+
status: 'fail',
|
|
221
|
+
// A game whose grass and water are fine still fails here when its player
|
|
222
|
+
// never loaded — which is the case this rung exists for, and the one
|
|
223
|
+
// `draws` calls healthy.
|
|
224
|
+
summary: asset
|
|
225
|
+
? `asset ${asset.ref} never loaded: ${asset.error}`
|
|
226
|
+
: (report.worst?.message ?? `${report.stats?.errors ?? 0} errors were swallowed`),
|
|
227
|
+
fix: 'read it in full: `incanto-logs`',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
193
232
|
function safeJson(text) {
|
|
194
233
|
const raw = text ?? '';
|
|
195
234
|
const start = raw.indexOf('{');
|
package/dist/2d.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Et as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engi
|
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
|
|
3
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
4
|
import { t as LoadSceneOptions } from "./loader-CeyU_bm1.js";
|
|
5
|
+
import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
|
|
5
6
|
import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
|
|
6
7
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
7
8
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
@@ -135,10 +136,16 @@ declare class RigidBody2D extends PhysicsBody2D {
|
|
|
135
136
|
declare class CharacterBody2D extends PhysicsBody2D {
|
|
136
137
|
static override readonly typeName: string;
|
|
137
138
|
static override readonly props: PropSchema;
|
|
139
|
+
/**
|
|
140
|
+
* Renamed to match the 3D body, where `snapToGround` had to give the name
|
|
141
|
+
* back to `Node3D`'s PLACEMENT prop. 2D has no placement prop, so nothing is
|
|
142
|
+
* ambiguous here and old scenes keep loading unchanged.
|
|
143
|
+
*/
|
|
144
|
+
static readonly propAliases: Record<string, string>;
|
|
138
145
|
/** px/s, y-down (up = -y). */
|
|
139
146
|
velocity: number[];
|
|
140
147
|
/** Keep contact on slopes/steps while not moving upward. */
|
|
141
|
-
|
|
148
|
+
stickToGround: boolean;
|
|
142
149
|
/** Max climbable slope angle. */
|
|
143
150
|
slopeLimitDeg: number;
|
|
144
151
|
/** @internal Updated by Physics2D.moveAndSlide. */
|
|
@@ -373,6 +380,11 @@ interface SpriteFromLibraryResult {
|
|
|
373
380
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
|
374
381
|
* declaration + AnimatedSprite2D props. `autoplay` defaults to the first
|
|
375
382
|
* LOOPING animation (idle-like), falling back to the first one.
|
|
383
|
+
*
|
|
384
|
+
* The animation map also gets ALIASES for the movement states a character
|
|
385
|
+
* controller emits but the sheet does not name (`run` → `move`, `fall` →
|
|
386
|
+
* `idle`), so the converted props and the documented
|
|
387
|
+
* `movementStateChanged → play` wiring work together out of the box.
|
|
376
388
|
*/
|
|
377
389
|
declare function spriteFromLibraryMeta(meta: unknown, opts: {
|
|
378
390
|
url: string;
|
|
@@ -411,15 +423,6 @@ declare class Sprite2D extends Node2D {
|
|
|
411
423
|
}
|
|
412
424
|
//#endregion
|
|
413
425
|
//#region src/2d/nodes/animated-sprite-2d.d.ts
|
|
414
|
-
interface AnimationDef {
|
|
415
|
-
/**
|
|
416
|
-
* Two numbers = inclusive [start, end] frame RANGE.
|
|
417
|
-
* Any other length = explicit frame list.
|
|
418
|
-
*/
|
|
419
|
-
frames: number[];
|
|
420
|
-
fps: number;
|
|
421
|
-
loop?: boolean;
|
|
422
|
-
}
|
|
423
426
|
/**
|
|
424
427
|
* Spritesheet animation: pure-JSON animation map, frame selection via a UV
|
|
425
428
|
* window on the node's own texture clone. Frames advance in `update(dt)` —
|
|
@@ -431,7 +434,7 @@ declare class AnimatedSprite2D extends Sprite2D {
|
|
|
431
434
|
static override readonly props: PropSchema;
|
|
432
435
|
/** `'$assetKey'` of a spritesheet asset. */
|
|
433
436
|
sheet: string;
|
|
434
|
-
animations: Record<string,
|
|
437
|
+
animations: Record<string, AnimationEntry>;
|
|
435
438
|
autoplay: string;
|
|
436
439
|
playing: boolean;
|
|
437
440
|
/** Absolute frame index within the sheet. */
|
|
@@ -440,8 +443,14 @@ declare class AnimatedSprite2D extends Sprite2D {
|
|
|
440
443
|
private frameList;
|
|
441
444
|
private frameIndex;
|
|
442
445
|
private frameTime;
|
|
446
|
+
private currentDef;
|
|
443
447
|
private ownTexture;
|
|
444
448
|
private loadedSheetRef;
|
|
449
|
+
/**
|
|
450
|
+
* `currentAnimation` reports the clip that is RUNNING, not the name asked
|
|
451
|
+
* for: an alias is a rename, and a state machine that reads back `fall` while
|
|
452
|
+
* the idle frames play is describing something that is not happening.
|
|
453
|
+
*/
|
|
445
454
|
play(name: string): void;
|
|
446
455
|
stop(): void;
|
|
447
456
|
override onReady(): void;
|
|
@@ -920,4 +929,4 @@ declare function parseCells(cells: readonly (string | readonly number[])[], lege
|
|
|
920
929
|
*/
|
|
921
930
|
declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set<number>): TileRect[];
|
|
922
931
|
//#endregion
|
|
923
|
-
export { AnimatedSprite2D,
|
|
932
|
+
export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
|
package/dist/2d.js
CHANGED
|
@@ -1,12 +1,65 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-
|
|
3
|
-
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-
|
|
4
|
-
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-
|
|
2
|
+
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-BgV6UbVA.js";
|
|
3
|
+
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-BNPZYJmd.js";
|
|
4
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-vyCBfACH.js";
|
|
5
5
|
//#region src/2d/library-sprite.ts
|
|
6
6
|
/**
|
|
7
|
+
* What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
|
|
8
|
+
* a library sheet that answers for it.
|
|
9
|
+
*
|
|
10
|
+
* The two vocabularies never met: the controller emits `run`, every library
|
|
11
|
+
* sheet calls it `move`, and no sheet anywhere ships a `fall` — so the wiring
|
|
12
|
+
* the 2D skill documents in one line (`movementStateChanged → play`) asked for
|
|
13
|
+
* animations that do not exist, on artwork from the engine's own catalog.
|
|
14
|
+
*
|
|
15
|
+
* These are ALIASES, so they read as what they are, and the author can point
|
|
16
|
+
* any of them somewhere better.
|
|
17
|
+
*/
|
|
18
|
+
const CONTROLLER_STATES = [
|
|
19
|
+
["run", [
|
|
20
|
+
"run",
|
|
21
|
+
"move",
|
|
22
|
+
"walk"
|
|
23
|
+
]],
|
|
24
|
+
["jump", ["jump", "idle"]],
|
|
25
|
+
["fall", [
|
|
26
|
+
"fall",
|
|
27
|
+
"jump",
|
|
28
|
+
"idle"
|
|
29
|
+
]],
|
|
30
|
+
["wallSlide", ["wallSlide", "idle"]],
|
|
31
|
+
["dash", [
|
|
32
|
+
"dash",
|
|
33
|
+
"run",
|
|
34
|
+
"move",
|
|
35
|
+
"idle"
|
|
36
|
+
]]
|
|
37
|
+
];
|
|
38
|
+
/**
|
|
39
|
+
* Fill in the states the sheet does not name, and touch nothing it does.
|
|
40
|
+
*
|
|
41
|
+
* Silent when there is no `idle` to fall back on: a spinning-coin sheet is not
|
|
42
|
+
* a character, and inventing movement states for it would put clips in the map
|
|
43
|
+
* that mean nothing.
|
|
44
|
+
*/
|
|
45
|
+
function addControllerStates(animations) {
|
|
46
|
+
const real = new Set(Object.keys(animations));
|
|
47
|
+
if (!real.has("idle")) return;
|
|
48
|
+
for (const [state, candidates] of CONTROLLER_STATES) {
|
|
49
|
+
if (real.has(state)) continue;
|
|
50
|
+
const target = candidates.find((c) => c !== state && real.has(c));
|
|
51
|
+
if (target) animations[state] = target;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
7
55
|
* Turn a library sprite-animation JSON into a scene-ready spritesheet asset
|
|
8
56
|
* declaration + AnimatedSprite2D props. `autoplay` defaults to the first
|
|
9
57
|
* LOOPING animation (idle-like), falling back to the first one.
|
|
58
|
+
*
|
|
59
|
+
* The animation map also gets ALIASES for the movement states a character
|
|
60
|
+
* controller emits but the sheet does not name (`run` → `move`, `fall` →
|
|
61
|
+
* `idle`), so the converted props and the documented
|
|
62
|
+
* `movementStateChanged → play` wiring work together out of the box.
|
|
10
63
|
*/
|
|
11
64
|
function spriteFromLibraryMeta(meta, opts) {
|
|
12
65
|
const m = meta;
|
|
@@ -25,6 +78,7 @@ function spriteFromLibraryMeta(meta, opts) {
|
|
|
25
78
|
firstName ??= name;
|
|
26
79
|
if (loop && firstLooping === null) firstLooping = name;
|
|
27
80
|
}
|
|
81
|
+
addControllerStates(animations);
|
|
28
82
|
const autoplay = opts.autoplay ?? firstLooping ?? firstName;
|
|
29
83
|
return {
|
|
30
84
|
asset: {
|
package/dist/3d.d.ts
CHANGED
|
@@ -370,9 +370,27 @@ declare class CharacterBody3D extends PhysicsBody3D {
|
|
|
370
370
|
static override readonly props: PropSchema;
|
|
371
371
|
/** m/s, y-up (up = +y). */
|
|
372
372
|
velocity: number[];
|
|
373
|
-
|
|
373
|
+
/**
|
|
374
|
+
* Hold the capsule against the surface while it moves, so walking down a
|
|
375
|
+
* slope does not become a series of small falls.
|
|
376
|
+
*
|
|
377
|
+
* Named apart from `Node3D.snapToGround` (which PLACES a node on the ground
|
|
378
|
+
* at load) because sharing the name silently broke both: the boolean shadowed
|
|
379
|
+
* the placement prop, its `true` default made the placement pass teleport
|
|
380
|
+
* every character body in a terrain scene, and a lift value was rejected
|
|
381
|
+
* outright — so the only escape from the teleport also turned this off.
|
|
382
|
+
*/
|
|
383
|
+
stickToGround: boolean;
|
|
374
384
|
/** Max climbable slope angle. */
|
|
375
385
|
slopeLimitDeg: number;
|
|
386
|
+
/**
|
|
387
|
+
* `snapToGround` on a character body is ambiguous, so it is refused.
|
|
388
|
+
*
|
|
389
|
+
* A boolean here used to mean the ground-stick, and as placement it means
|
|
390
|
+
* "leave y alone" — which is already the default. Rather than pick one and
|
|
391
|
+
* silently do the other thing to an existing scene, say which prop was meant.
|
|
392
|
+
*/
|
|
393
|
+
override onReady(): void;
|
|
376
394
|
/** @internal Updated by Physics3D.moveAndSlide. */
|
|
377
395
|
_grounded: boolean;
|
|
378
396
|
moveAndSlide(): void;
|
|
@@ -1006,6 +1024,25 @@ interface ModelVerdict {
|
|
|
1006
1024
|
/** Clip names baked into the file itself (playable by name, no asset entry). */
|
|
1007
1025
|
embeddedClips: string[];
|
|
1008
1026
|
sceneJson: SceneJson | null;
|
|
1027
|
+
/**
|
|
1028
|
+
* The whole playable character — body, controller, skin, clips and input —
|
|
1029
|
+
* for a rig that can take the library's locomotion clips. Null for a prop.
|
|
1030
|
+
*
|
|
1031
|
+
* The model on its own is a statue, and every step from there is documented
|
|
1032
|
+
* somewhere different: the controller must sit under a dynamic body, the skin
|
|
1033
|
+
* must be sized to the capsule and dropped to its feet, the clips must be
|
|
1034
|
+
* declared, and the input actions must exist or nothing reads the keyboard.
|
|
1035
|
+
*/
|
|
1036
|
+
characterJson: CharacterJson | null;
|
|
1037
|
+
}
|
|
1038
|
+
interface CharacterJson {
|
|
1039
|
+
assets: Record<string, {
|
|
1040
|
+
type: string;
|
|
1041
|
+
url: string;
|
|
1042
|
+
}>;
|
|
1043
|
+
/** The actions `CharacterController3D` reads — without them it never moves. */
|
|
1044
|
+
input: Record<string, unknown>;
|
|
1045
|
+
node: Record<string, unknown>;
|
|
1009
1046
|
}
|
|
1010
1047
|
interface VerdictTarget {
|
|
1011
1048
|
url: string;
|