incanto 0.67.0 → 0.69.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/assets/catalog.json +9 -5
- package/bin/_pick-server.mjs +48 -0
- package/bin/incanto-assets.mjs +14 -1
- package/bin/incanto-check.mjs +69 -5
- package/bin/incanto-frame.mjs +10 -25
- package/bin/incanto-logs.mjs +8 -18
- package/bin/incanto-new.mjs +29 -1
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-verify.mjs +40 -0
- package/bin/incanto.mjs +1 -0
- package/dist/2d.d.ts +10 -8
- package/dist/2d.js +6 -4
- package/dist/3d.d.ts +31 -7
- package/dist/3d.js +9 -7
- package/dist/{audio-player-BNUHo0JF.d.ts → audio-player-C0j2k-yB.d.ts} +80 -3
- package/dist/{behavior-ZnfzKDo3.d.ts → behavior-rm8vVzu_.d.ts} +145 -1
- package/dist/boot-failure-CKYrEcGF.js +47 -0
- package/dist/{create-game-FAXdHta3.js → create-game-CvJ1wSRP.js} +27 -27
- package/dist/{create-game-DMetd3vR.js → create-game-CwxEDlCm.js} +15 -11
- package/dist/debug.d.ts +1 -1
- package/dist/{duplicate-B8QnX2-r.js → duplicate-Dj4L-5AA.js} +1 -1
- package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
- package/dist/editor.js +12 -6
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-C-5g2pMh.js → environment-presets-BO-7XoFV.js} +58 -9
- package/dist/{frame-report-DCnHFmto.d.ts → frame-report-DNxDAb1w.d.ts} +8 -0
- package/dist/{frame-report-BSMny7oe.js → frame-report-Dlq13Gyj.js} +1 -0
- package/dist/{gameplay-CGSv-9XR.js → gameplay-D1KixWK8.js} +175 -151
- package/dist/gameplay.d.ts +51 -3
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +6 -6
- package/dist/index.js +8 -7
- package/dist/{loader-KZESOZmE.js → loader-DZE7B07H.js} +48 -7
- package/dist/{loader-DyT30ArB.d.ts → loader-DadV2QCT.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-_B6zj3zU.js → physics-2d-CF5yyjbj.js} +2 -2
- package/dist/{physics-3d-i1IeAY0F.js → physics-3d-DbYaEcZy.js} +3 -3
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-vIxU3Xbv.js → register-DbVArsns.js} +19 -10
- package/dist/{register-Cs53CY7s.js → register-mmfugpaQ.js} +159 -6
- package/dist/{replay-BkpDV51O.js → replay-BEHLDcQV.js} +28 -5
- package/dist/{replay-D0GcEHYU.d.ts → replay-BQfkX4x7.d.ts} +38 -4
- package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DS8RaUV_.js → split-screen-CauMwu6y.js} +2 -2
- package/dist/{split-screen-DD5eYIgW.d.ts → split-screen-Cg76MCV5.d.ts} +3 -3
- package/dist/{src-BLlsGJW9.js → src-CYEu-_S9.js} +1 -1
- package/dist/{teardown-LB9cTnXJ.js → teardown-CMqFVxO9.js} +5 -4
- package/dist/{test-81uZ967a.js → test-D6ISmuFE.js} +73 -25
- package/dist/test.d.ts +17 -6
- package/dist/test.js +3 -3
- package/dist/vite.d.ts +70 -2
- package/dist/vite.js +176 -14
- package/dist/{rendering-options-ByAdi8PF.js → webgl-unavailable-Z5_g8x9H.js} +57 -2
- package/editor/assets/{agent8-C2jtNKbx.js → agent8-BDvaZMQJ.js} +1 -1
- package/editor/assets/{debug-DoU3KeqS.js → debug-CMULinBW.js} +1 -1
- package/editor/assets/{index-CNqHojcC.js → index-CZIRr5EL.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +4 -3
- package/schemas/scene.schema.json +4 -0
- package/skills/incanto-3d-character.md +1 -0
- package/skills/incanto-assets.md +16 -2
- package/skills/incanto-audio.md +65 -0
- package/skills/incanto-building-2d-games.md +66 -0
- package/skills/incanto-building-3d-games.md +97 -2
- package/skills/incanto-gameplay-behaviors.md +46 -2
- package/skills/incanto-node-reference.md +1 -0
- package/skills/incanto-physics-and-input.md +23 -0
- package/skills/incanto-save-slots.md +27 -0
- package/skills/incanto-scene-json-authoring.md +12 -1
- package/skills/incanto-verifying-your-game.md +96 -2
- package/skills/incanto-your-first-game.md +2 -1
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/main.ts +21 -4
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +17 -27
- package/templates-app/platformer-2d/src/main.ts +32 -13
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/main.ts +25 -7
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/main.ts +34 -16
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/main.ts +21 -4
package/assets/catalog.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"url": "incanto/assets/characters/2dbasic.png",
|
|
6
6
|
"kind": "character",
|
|
7
7
|
"bytes": 30499,
|
|
8
|
-
"description": "2dbasic sprite sheet image.anything,base character.
|
|
8
|
+
"description": "2dbasic sprite sheet image.anything,base character.",
|
|
9
9
|
"animation": "characters/2dbasic.json",
|
|
10
10
|
"frameWidth": 111,
|
|
11
11
|
"frameHeight": 83
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"url": "incanto/assets/characters/ghost.png",
|
|
145
145
|
"kind": "character",
|
|
146
146
|
"bytes": 22933,
|
|
147
|
-
"description": "Ghost character with translucent appearance sprite sheet image
|
|
147
|
+
"description": "Ghost character with translucent appearance sprite sheet image",
|
|
148
148
|
"animation": "characters/ghost.json",
|
|
149
149
|
"frameWidth": 112,
|
|
150
150
|
"frameHeight": 128
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
"url": "incanto/assets/characters/goblin.png",
|
|
156
156
|
"kind": "character",
|
|
157
157
|
"bytes": 57994,
|
|
158
|
-
"description": "Medieval goblin with torch sprite sheet image
|
|
158
|
+
"description": "Medieval goblin with torch sprite sheet image",
|
|
159
159
|
"animation": "characters/goblin.json",
|
|
160
160
|
"frameWidth": 192,
|
|
161
161
|
"frameHeight": 192
|
|
@@ -286,7 +286,7 @@
|
|
|
286
286
|
"url": "incanto/assets/characters/medieval-knight.png",
|
|
287
287
|
"kind": "character",
|
|
288
288
|
"bytes": 84367,
|
|
289
|
-
"description": "
|
|
289
|
+
"description": "Using a medieval-themed SD(Super Deformed) knight sprite sheet image, you can apply idle, move, and attack animations, among others.",
|
|
290
290
|
"animation": "characters/medieval-knight.json",
|
|
291
291
|
"frameWidth": 192,
|
|
292
292
|
"frameHeight": 192
|
|
@@ -297,7 +297,11 @@
|
|
|
297
297
|
"url": "incanto/assets/tiles/minecraft-tiles.png",
|
|
298
298
|
"kind": "tile",
|
|
299
299
|
"bytes": 10511,
|
|
300
|
-
"description": "Minecraft-themed tiles sprite sheet image
|
|
300
|
+
"description": "Minecraft-themed tiles sprite sheet image",
|
|
301
|
+
"frameWidth": 16,
|
|
302
|
+
"frameHeight": 16,
|
|
303
|
+
"columns": 5,
|
|
304
|
+
"tiles": 25
|
|
301
305
|
},
|
|
302
306
|
{
|
|
303
307
|
"name": "monster-died",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of the dev servers that answered is THIS project's — the one decision
|
|
3
|
+
* `incanto-frame` and `incanto-logs` must never get wrong, in one place.
|
|
4
|
+
*
|
|
5
|
+
* They each had their own copy, and each copy had the same hatch: when exactly
|
|
6
|
+
* one server answered and it did not say whose it was, take it anyway ("an
|
|
7
|
+
* older incanto that predates the identity field"). A server that will not
|
|
8
|
+
* identify itself is the ONE kind the check cannot clear, so the hatch let
|
|
9
|
+
* through precisely what it was written to stop. Measured in an empty temp
|
|
10
|
+
* directory with one foreign page listening, on a scene with two nodes and no
|
|
11
|
+
* assets:
|
|
12
|
+
*
|
|
13
|
+
* ✓ draws — subject fills 37.5% of the frame
|
|
14
|
+
* ✗ says — asset …/NO-SUCH-TEXTURE.jpg never loaded: 404
|
|
15
|
+
*
|
|
16
|
+
* A ✓ measured off another game, and a ✗ naming a texture that appears nowhere
|
|
17
|
+
* in this project. It surfaced in this repo as an intermittent failure of
|
|
18
|
+
* `incanto-verify.test.ts`, which is a gate that lies in both directions.
|
|
19
|
+
*
|
|
20
|
+
* The real dev server has sent `root` since 0.41 (`frame-endpoint.test.ts`
|
|
21
|
+
* pins it), so nothing is lost; `--port` still names a server on purpose.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Is `body.root` this directory, an ancestor of it, or inside it? */
|
|
25
|
+
export function sameProject(body, cwd) {
|
|
26
|
+
if (!body?.root) return false;
|
|
27
|
+
const root = String(body.root);
|
|
28
|
+
return root === cwd || cwd.startsWith(`${root}/`) || root.startsWith(`${cwd}/`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `{ pick }` when one of them is ours, `{ refusal }` when some answered and
|
|
33
|
+
* none is, `{}` when nothing answered at all (the caller says "no dev server").
|
|
34
|
+
*/
|
|
35
|
+
export function pickServer(answered, cwd, what) {
|
|
36
|
+
const mine = answered.filter((a) => sameProject(a, cwd));
|
|
37
|
+
if (mine.length > 0) return { pick: mine[0] };
|
|
38
|
+
if (answered.length === 0) return {};
|
|
39
|
+
const listed = answered
|
|
40
|
+
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? "doesn't say"}`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
return {
|
|
43
|
+
refusal:
|
|
44
|
+
`${answered.length} incanto dev server(s) are running and none of them is this ` +
|
|
45
|
+
`directory (${cwd}):\n${listed}\n\n${what}\n` +
|
|
46
|
+
"Start this project's dev server, or pass --port.",
|
|
47
|
+
};
|
|
48
|
+
}
|
package/bin/incanto-assets.mjs
CHANGED
|
@@ -40,8 +40,21 @@ if (command === 'list') {
|
|
|
40
40
|
} else {
|
|
41
41
|
for (const e of entries) {
|
|
42
42
|
const anim = e.animation ? ' [animated]' : '';
|
|
43
|
+
/*
|
|
44
|
+
* The GRID, from the fields rather than the prose.
|
|
45
|
+
*
|
|
46
|
+
* `list` printed only the description, and a description carrying a frame
|
|
47
|
+
* size is a second source of truth that drifts: `2dbasic` said
|
|
48
|
+
* "(frame size 192x192)" against a real grid of 111x83 — 666x664 does not
|
|
49
|
+
* divide by 192 — and the wrong number was the first one an agent read.
|
|
50
|
+
* The sizes live in `frameWidth`/`frameHeight` now, and the prose says
|
|
51
|
+
* what the art IS.
|
|
52
|
+
*/
|
|
53
|
+
const grid = e.frameWidth
|
|
54
|
+
? ` ${e.frameWidth}×${e.frameHeight}${e.tiles ? ` (${e.tiles} tiles)` : ''}`
|
|
55
|
+
: '';
|
|
43
56
|
console.log(
|
|
44
|
-
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)} ${e.description.slice(0,
|
|
57
|
+
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)}${grid.padEnd(16)} ${e.description.slice(0, 64)}`,
|
|
45
58
|
);
|
|
46
59
|
}
|
|
47
60
|
console.log(`\n${entries.length} built-in asset(s) — incanto-assets info <name> for details`);
|
package/bin/incanto-check.mjs
CHANGED
|
@@ -18,14 +18,27 @@ import { existsSync, lstatSync, readdirSync, readFileSync, statSync } from 'node
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
21
22
|
|
|
22
23
|
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
23
24
|
|
|
24
25
|
function parseArgs(argv) {
|
|
25
|
-
const args = {
|
|
26
|
-
|
|
26
|
+
const args = {
|
|
27
|
+
files: [],
|
|
28
|
+
json: false,
|
|
29
|
+
strict: false,
|
|
30
|
+
help: false,
|
|
31
|
+
invalid: false,
|
|
32
|
+
behaviors: null,
|
|
33
|
+
};
|
|
34
|
+
// Indexed, because `--behaviors` takes the NEXT argument. A `for…of` with a
|
|
35
|
+
// hand-rolled `++i` reads fine and throws `i is not defined` at runtime,
|
|
36
|
+
// which is a flag that exists in the help and cannot be used.
|
|
37
|
+
for (let i = 0; i < argv.length; i++) {
|
|
38
|
+
const a = argv[i];
|
|
27
39
|
if (a === '--json') args.json = true;
|
|
28
40
|
else if (a === '--strict-behaviors') args.strict = true;
|
|
41
|
+
else if (a === '--behaviors') args.behaviors = argv[++i];
|
|
29
42
|
else if (a === '--strict-warnings') args.strictWarnings = true;
|
|
30
43
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
31
44
|
else if (a.startsWith('--')) {
|
|
@@ -38,7 +51,7 @@ function parseArgs(argv) {
|
|
|
38
51
|
|
|
39
52
|
const args = parseArgs(process.argv.slice(2));
|
|
40
53
|
if (args.help || args.invalid) {
|
|
41
|
-
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--strict-behaviors] [--strict-warnings]
|
|
54
|
+
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--behaviors FILE] [--strict-behaviors] [--strict-warnings]
|
|
42
55
|
|
|
43
56
|
Validates *.scene.json files headlessly with the installed engine.
|
|
44
57
|
With no arguments, scans the current directory recursively
|
|
@@ -155,7 +168,33 @@ if (files.length === 0) {
|
|
|
155
168
|
process.exit(1);
|
|
156
169
|
}
|
|
157
170
|
|
|
158
|
-
const { auditScene, validateScene } = await import(
|
|
171
|
+
const { auditScene, instancedScenes, validateScene } = await import(
|
|
172
|
+
pathToFileURL(join(DIST, 'test.js')).href
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/** Your Behavior subclasses, when you name a file — same contract as the other bins. */
|
|
176
|
+
const behaviors = {};
|
|
177
|
+
if (args.behaviors) {
|
|
178
|
+
const { Behavior } = await import(pathToFileURL(join(DIST, 'index.js')).href);
|
|
179
|
+
let mod;
|
|
180
|
+
try {
|
|
181
|
+
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
const why = e?.message ?? String(e);
|
|
184
|
+
// node cannot import the pattern this package teaches (a JSON import
|
|
185
|
+
// without an attribute, an extensionless relative path); bun can. Same
|
|
186
|
+
// hand-off as `incanto-playtest`.
|
|
187
|
+
reexecUnderBun(import.meta.url, why);
|
|
188
|
+
console.error(behaviorsHelp(args.behaviors, why));
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
192
|
+
if (typeof value === 'function' && value.prototype instanceof Behavior) behaviors[name] = value;
|
|
193
|
+
}
|
|
194
|
+
if (Object.keys(behaviors).length === 0) {
|
|
195
|
+
console.error(`warning: no Behavior subclasses exported from ${args.behaviors}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
159
198
|
|
|
160
199
|
/**
|
|
161
200
|
* Every `strings` table in the corpus, unioned.
|
|
@@ -176,9 +215,16 @@ const declaredElsewhere = {};
|
|
|
176
215
|
* part of its project it can.
|
|
177
216
|
*/
|
|
178
217
|
const declaredAssets = {};
|
|
218
|
+
/**
|
|
219
|
+
* Every scene some OTHER scene embeds — the only honest way to tell a fragment
|
|
220
|
+
* from a first draft. Without it a 3D scene with no camera and no light checked
|
|
221
|
+
* CLEAN, because "has no camera" was doing double duty as "is a fragment".
|
|
222
|
+
*/
|
|
223
|
+
const corpus = [];
|
|
179
224
|
for (const file of files) {
|
|
180
225
|
try {
|
|
181
226
|
const doc = JSON.parse(readFileSync(file, 'utf-8'));
|
|
227
|
+
corpus.push({ path: resolve(file), json: doc });
|
|
182
228
|
for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
|
|
183
229
|
if (typeof entries !== 'object' || entries === null) continue;
|
|
184
230
|
declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
|
|
@@ -191,6 +237,8 @@ for (const file of files) {
|
|
|
191
237
|
}
|
|
192
238
|
}
|
|
193
239
|
|
|
240
|
+
const instanced = instancedScenes(corpus, (from, ref) => resolve(dirname(from), ref));
|
|
241
|
+
|
|
194
242
|
const results = files.map((file) => {
|
|
195
243
|
let json;
|
|
196
244
|
try {
|
|
@@ -200,6 +248,19 @@ const results = files.map((file) => {
|
|
|
200
248
|
}
|
|
201
249
|
const res = validateScene(json, {
|
|
202
250
|
strictBehaviors: args.strict,
|
|
251
|
+
/*
|
|
252
|
+
* `validateScene` has always taken `behaviors`; this bin had no way to pass
|
|
253
|
+
* any. So `--strict-behaviors` could only ever FAIL on a project with a
|
|
254
|
+
* behaviour of its own — which is every project past its first hour:
|
|
255
|
+
*
|
|
256
|
+
* [UNKNOWN_BEHAVIOR] Unknown behavior 'Hunter'. … Did you forget
|
|
257
|
+
* registerBehavior('Hunter', Hunter)?
|
|
258
|
+
* 0/1 scene(s) valid EXIT=1
|
|
259
|
+
*
|
|
260
|
+
* A flag whose only outcome is failure is a flag nobody uses, and the check
|
|
261
|
+
* it gates — a script name that resolves nowhere — is worth having.
|
|
262
|
+
*/
|
|
263
|
+
behaviors,
|
|
203
264
|
declaredAssets,
|
|
204
265
|
resolveScene: (p) => JSON.parse(readFileSync(resolve(dirname(file), p), 'utf-8')),
|
|
205
266
|
});
|
|
@@ -207,7 +268,10 @@ const results = files.map((file) => {
|
|
|
207
268
|
return {
|
|
208
269
|
file,
|
|
209
270
|
ok: true,
|
|
210
|
-
warnings: [
|
|
271
|
+
warnings: [
|
|
272
|
+
...auditScene(json, { declaredElsewhere, instanced: instanced.has(resolve(file)) }),
|
|
273
|
+
...missingArt(file, json),
|
|
274
|
+
],
|
|
211
275
|
};
|
|
212
276
|
}
|
|
213
277
|
return {
|
package/bin/incanto-frame.mjs
CHANGED
|
@@ -20,6 +20,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { pickServer } from './_pick-server.mjs';
|
|
23
24
|
|
|
24
25
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
26
|
const { listeningPorts, parseProcNetTcp } = await import(
|
|
@@ -171,11 +172,6 @@ const answered = [];
|
|
|
171
172
|
// is plainly on screen.
|
|
172
173
|
const HOSTS = ['localhost', '127.0.0.1'];
|
|
173
174
|
const CWD = process.cwd();
|
|
174
|
-
const sameProject = (b) => {
|
|
175
|
-
if (!b?.root) return false;
|
|
176
|
-
const r = String(b.root);
|
|
177
|
-
return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
|
|
178
|
-
};
|
|
179
175
|
for (const port of ports) {
|
|
180
176
|
for (const host of HOSTS) {
|
|
181
177
|
try {
|
|
@@ -197,26 +193,15 @@ for (const port of ports) {
|
|
|
197
193
|
if (explicit) {
|
|
198
194
|
found = answered[0] ?? null;
|
|
199
195
|
} else {
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
} else if (answered.length > 0) {
|
|
211
|
-
console.error(
|
|
212
|
-
`${answered.length} incanto dev server(s) are running and NONE of them is this ` +
|
|
213
|
-
`directory (${CWD}):\n` +
|
|
214
|
-
answered
|
|
215
|
-
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
|
|
216
|
-
.join('\n') +
|
|
217
|
-
"\n\nMeasuring one project on another project's page reports a number that has\n" +
|
|
218
|
-
"nothing to do with this game. Start this project's dev server, or pass --port.",
|
|
219
|
-
);
|
|
196
|
+
const { pick, refusal } = pickServer(
|
|
197
|
+
answered,
|
|
198
|
+
CWD,
|
|
199
|
+
"Measuring one project on another project's page reports a number that has\n" +
|
|
200
|
+
'nothing to do with this game.',
|
|
201
|
+
);
|
|
202
|
+
if (pick) found = pick;
|
|
203
|
+
else if (refusal) {
|
|
204
|
+
console.error(refusal);
|
|
220
205
|
process.exit(1);
|
|
221
206
|
}
|
|
222
207
|
}
|
package/bin/incanto-logs.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { readFileSync } from 'node:fs';
|
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
import { pickServer } from './_pick-server.mjs';
|
|
24
25
|
|
|
25
26
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
const { listeningPorts } = await import(pathToFileURL(join(PKG, 'dist', 'vite.js')).href);
|
|
@@ -80,11 +81,6 @@ let found = null;
|
|
|
80
81
|
// project's page answered both the frame and the logs request.
|
|
81
82
|
const answered = [];
|
|
82
83
|
const CWD = process.cwd();
|
|
83
|
-
const sameProject = (b) => {
|
|
84
|
-
if (!b?.root) return false;
|
|
85
|
-
const r = String(b.root);
|
|
86
|
-
return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
|
|
87
|
-
};
|
|
88
84
|
for (const port of ports) {
|
|
89
85
|
for (const host of ['localhost', '127.0.0.1']) {
|
|
90
86
|
try {
|
|
@@ -105,20 +101,14 @@ for (const port of ports) {
|
|
|
105
101
|
if (explicit) {
|
|
106
102
|
found = answered[0] ? { host: answered[0].host, port: answered[0].port } : null;
|
|
107
103
|
} else {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
const { pick, refusal } = pickServer(
|
|
105
|
+
answered,
|
|
106
|
+
CWD,
|
|
107
|
+
"Reading another project's log buffer would report its errors as yours.",
|
|
108
|
+
);
|
|
111
109
|
if (pick) found = { host: pick.host, port: pick.port };
|
|
112
|
-
else if (
|
|
113
|
-
console.error(
|
|
114
|
-
`${answered.length} incanto dev server(s) are running and none is this directory ` +
|
|
115
|
-
`(${CWD}):\n` +
|
|
116
|
-
answered
|
|
117
|
-
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
|
|
118
|
-
.join('\n') +
|
|
119
|
-
"\n\nReading another project's log buffer would report its errors as yours.\n" +
|
|
120
|
-
"Start this project's dev server, or pass --port.",
|
|
121
|
-
);
|
|
110
|
+
else if (refusal) {
|
|
111
|
+
console.error(refusal);
|
|
122
112
|
process.exit(1);
|
|
123
113
|
}
|
|
124
114
|
}
|
package/bin/incanto-new.mjs
CHANGED
|
@@ -70,7 +70,11 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (!target) {
|
|
73
|
-
console.error(
|
|
73
|
+
console.error(
|
|
74
|
+
'usage: incanto-new <dir> [--template <name>] [--local] | --list\n' +
|
|
75
|
+
' --local pin `file:` to the incanto this command is running from,\n' +
|
|
76
|
+
' instead of the registry — for verifying a release candidate.',
|
|
77
|
+
);
|
|
74
78
|
process.exit(1);
|
|
75
79
|
}
|
|
76
80
|
|
|
@@ -93,10 +97,30 @@ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
|
93
97
|
pkg.name = basename(dest)
|
|
94
98
|
.toLowerCase()
|
|
95
99
|
.replace(/[^a-z0-9-]/g, '-');
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* WHICH incanto this scaffold will install.
|
|
103
|
+
*
|
|
104
|
+
* The template pins `^<version>`, so `bun install` resolves the REGISTRY build
|
|
105
|
+
* — even when the scaffolder itself came from a local tarball. Someone
|
|
106
|
+
* verifying a release candidate that shares its version with a published one
|
|
107
|
+
* then measures the published artifact for an hour and never sees a difference:
|
|
108
|
+
* the version matches, the install succeeds, and a `main.ts` that typechecks
|
|
109
|
+
* against the candidate does not typecheck against the registry copy.
|
|
110
|
+
*
|
|
111
|
+
* `--local` pins the package this bin is running from, and either way the pin
|
|
112
|
+
* is printed. A silent resolution is the part that cost the hour.
|
|
113
|
+
*/
|
|
114
|
+
const ENGINE_ROOT = PKG;
|
|
115
|
+
if (argv.includes('--local')) {
|
|
116
|
+
pkg.dependencies = { ...pkg.dependencies, incanto: `file:${ENGINE_ROOT}` };
|
|
117
|
+
}
|
|
118
|
+
const pin = pkg.dependencies?.incanto ?? '(none)';
|
|
96
119
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
97
120
|
|
|
98
121
|
console.log(`
|
|
99
122
|
${template} → ${target}
|
|
123
|
+
incanto ${pin}
|
|
100
124
|
|
|
101
125
|
cd ${target}
|
|
102
126
|
bun install # or: npm install
|
|
@@ -104,6 +128,10 @@ console.log(`
|
|
|
104
128
|
bun run verify # prove it plays, headlessly
|
|
105
129
|
bun run check # validate the scene JSON
|
|
106
130
|
|
|
131
|
+
When it plays, ship it:
|
|
132
|
+
bun run build # the artifact a player gets
|
|
133
|
+
bunx incanto-serve # serve dist/ so verify/frame/logs can see the BUILT game
|
|
134
|
+
|
|
107
135
|
Vibe-coding? Install the agent skills too:
|
|
108
136
|
bunx incanto-skills claude # or: opencode
|
|
109
137
|
`);
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
/**
|
|
4
|
+
* Serve a BUILT game, and let the inspect tools see it.
|
|
5
|
+
*
|
|
6
|
+
* `incanto-frame` and `incanto-logs` ask a dev server, which asks the page over
|
|
7
|
+
* vite's HMR channel. A production build has no HMR, so nothing could look at
|
|
8
|
+
* the artifact an author actually deploys:
|
|
9
|
+
*
|
|
10
|
+
* $ node …/incanto-logs.mjs --port 8791 # dist/ on a static server
|
|
11
|
+
* no incanto dev server found.
|
|
12
|
+
* $ incanto-verify src/game.scene.json
|
|
13
|
+
* ? draws — no incanto dev server found.
|
|
14
|
+
* ? says — no incanto dev server found.
|
|
15
|
+
*
|
|
16
|
+
* The boot path is the only code a BUILD adds over a SCENE — `main.ts`, the
|
|
17
|
+
* loading overlay, the asset base path, everything after `createGame*` resolves
|
|
18
|
+
* — and it was the one thing no rung could reach.
|
|
19
|
+
*
|
|
20
|
+
* This serves `dist/` as plain static files (no bundler, no transform) and adds
|
|
21
|
+
* exactly two things: the `/__incanto/*` endpoints the CLIs already speak, and
|
|
22
|
+
* one inline script injected into `index.html` that relays them to the page.
|
|
23
|
+
* Nothing is added to the files on disk, so what you measure is what you ship.
|
|
24
|
+
*
|
|
25
|
+
* bunx incanto-serve # ./dist on :4180
|
|
26
|
+
* bunx incanto-serve build --port 5050
|
|
27
|
+
* bunx incanto-verify # now measures the BUILT game
|
|
28
|
+
*/
|
|
29
|
+
import { createServer } from 'node:http';
|
|
30
|
+
import { createRequire } from 'node:module';
|
|
31
|
+
import { extname, join, normalize, resolve, sep } from 'node:path';
|
|
32
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
33
|
+
|
|
34
|
+
const PKG = resolve(fileURLToPath(import.meta.url), '../..');
|
|
35
|
+
const DIST = join(PKG, 'dist');
|
|
36
|
+
if (!existsSync(join(DIST, 'vite.js'))) {
|
|
37
|
+
console.error('incanto: the package is not built (dist/vite.js missing).');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
const { createStaticInspectHost, serveFrameEndpoints } = await import(
|
|
41
|
+
pathToFileURL(join(DIST, 'vite.js')).href
|
|
42
|
+
);
|
|
43
|
+
const VERSION = createRequire(import.meta.url)('../package.json').version;
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
47
|
+
console.error(`Usage: incanto-serve [dir] [--port N] [--host H]
|
|
48
|
+
|
|
49
|
+
Serve a BUILT game (default ./dist) so \`incanto-frame\`, \`incanto-logs\` and
|
|
50
|
+
\`incanto-verify\` can measure the artifact you actually deploy. Files are served
|
|
51
|
+
byte-for-byte; the inspect client is injected into index.html at serve time and
|
|
52
|
+
never written to disk.`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
const flag = (name, fallback) => {
|
|
56
|
+
const at = args.indexOf(name);
|
|
57
|
+
return at >= 0 && args[at + 1] ? args[at + 1] : fallback;
|
|
58
|
+
};
|
|
59
|
+
const port = Number(flag('--port', '4180'));
|
|
60
|
+
const host = flag('--host', '127.0.0.1');
|
|
61
|
+
const dir = resolve(
|
|
62
|
+
args.find(
|
|
63
|
+
(a) =>
|
|
64
|
+
!a.startsWith('--') &&
|
|
65
|
+
args[args.indexOf(a) - 1] !== '--port' &&
|
|
66
|
+
args[args.indexOf(a) - 1] !== '--host',
|
|
67
|
+
) ?? 'dist',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
71
|
+
console.error(
|
|
72
|
+
`incanto-serve: no such directory: ${dir}\n` +
|
|
73
|
+
' Build first (`bun run build`), or name the output directory.',
|
|
74
|
+
);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
if (!existsSync(join(dir, 'index.html'))) {
|
|
78
|
+
console.error(
|
|
79
|
+
`incanto-serve: ${dir} has no index.html — that is not a built game.\n` +
|
|
80
|
+
' Point this at the directory your bundler wrote.',
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const inspect = createStaticInspectHost(process.cwd());
|
|
86
|
+
serveFrameEndpoints(inspect, VERSION);
|
|
87
|
+
|
|
88
|
+
const TYPES = {
|
|
89
|
+
'.html': 'text/html; charset=utf-8',
|
|
90
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
91
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
92
|
+
'.css': 'text/css; charset=utf-8',
|
|
93
|
+
'.json': 'application/json; charset=utf-8',
|
|
94
|
+
'.png': 'image/png',
|
|
95
|
+
'.jpg': 'image/jpeg',
|
|
96
|
+
'.jpeg': 'image/jpeg',
|
|
97
|
+
'.webp': 'image/webp',
|
|
98
|
+
'.gif': 'image/gif',
|
|
99
|
+
'.svg': 'image/svg+xml',
|
|
100
|
+
'.ico': 'image/x-icon',
|
|
101
|
+
'.glb': 'model/gltf-binary',
|
|
102
|
+
'.gltf': 'model/gltf+json',
|
|
103
|
+
'.mp3': 'audio/mpeg',
|
|
104
|
+
'.ogg': 'audio/ogg',
|
|
105
|
+
'.wav': 'audio/wav',
|
|
106
|
+
'.wasm': 'application/wasm',
|
|
107
|
+
'.woff2': 'font/woff2',
|
|
108
|
+
'.ttf': 'font/ttf',
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/** Inject the relay ahead of everything, so it exists before the game boots. */
|
|
112
|
+
function withInspectClient(html) {
|
|
113
|
+
const tag = `<script>${inspect.clientScript()}</script>`;
|
|
114
|
+
if (html.includes('</head>')) return html.replace('</head>', `${tag}</head>`);
|
|
115
|
+
return tag + html;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const server = createServer((req, res) => {
|
|
119
|
+
const url = (req.url ?? '/').split('?')[0] ?? '/';
|
|
120
|
+
if (url.startsWith('/__incanto/')) {
|
|
121
|
+
// `originalUrl` is what the endpoints read for the query string; connect
|
|
122
|
+
// strips the mount path off `url` and these were written against that.
|
|
123
|
+
req.originalUrl = req.url;
|
|
124
|
+
if (inspect.handleInspect(req, res)) return;
|
|
125
|
+
res.statusCode = 404;
|
|
126
|
+
res.end('{}');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// A path outside the served directory is a traversal attempt, not a typo.
|
|
130
|
+
const rel = normalize(decodeURIComponent(url)).replace(/^(\.\.[/\\])+/, '');
|
|
131
|
+
let file = join(dir, rel);
|
|
132
|
+
if (!file.startsWith(dir + sep) && file !== dir) file = dir;
|
|
133
|
+
if (!existsSync(file) || statSync(file).isDirectory()) file = join(dir, 'index.html');
|
|
134
|
+
|
|
135
|
+
const type = TYPES[extname(file)] ?? 'application/octet-stream';
|
|
136
|
+
if (file.endsWith('index.html')) {
|
|
137
|
+
const html = withInspectClient(readFileSync(file, 'utf8'));
|
|
138
|
+
res.statusCode = 200;
|
|
139
|
+
res.setHeader('content-type', TYPES['.html']);
|
|
140
|
+
res.end(html);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
res.statusCode = 200;
|
|
144
|
+
res.setHeader('content-type', type);
|
|
145
|
+
res.end(readFileSync(file));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
server.on('error', (e) => {
|
|
149
|
+
if (e?.code === 'EADDRINUSE') {
|
|
150
|
+
console.error(`incanto-serve: port ${port} is already in use — pass --port N.`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
throw e;
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
server.listen(port, host, () => {
|
|
157
|
+
console.log(`incanto-serve ${dir}`);
|
|
158
|
+
console.log(` http://${host}:${port}/`);
|
|
159
|
+
console.log(' open it, then run `incanto-verify` / `incanto-frame` / `incanto-logs`.');
|
|
160
|
+
});
|
package/bin/incanto-verify.mjs
CHANGED
|
@@ -449,6 +449,30 @@ if (rungs[0].status === 'pass') {
|
|
|
449
449
|
? 'bring the preview window to the front — a hidden tab does not render, and a frame is captured inside a render'
|
|
450
450
|
: 'start your dev server, open the page, and run this again — the pixels are in the browser',
|
|
451
451
|
});
|
|
452
|
+
} else if (report.hidden) {
|
|
453
|
+
/*
|
|
454
|
+
* A hidden page is UNMEASURED, not passed.
|
|
455
|
+
*
|
|
456
|
+
* A browser stops animating a page it considers hidden — a window merely
|
|
457
|
+
* covered by another counts — and the back buffer keeps whatever it last
|
|
458
|
+
* drew. Measured at one instant, same page:
|
|
459
|
+
*
|
|
460
|
+
* incanto-frame --do the page is hidden … a frozen game (exit 1)
|
|
461
|
+
* incanto-logs frame rate not measurable — hidden
|
|
462
|
+
* incanto-frame subject fills 17.5% of frame
|
|
463
|
+
* incanto-verify ✓ draws — subject fills 17.5%
|
|
464
|
+
*
|
|
465
|
+
* Two of six rungs reported ✓ off a stale buffer while the engine already
|
|
466
|
+
* knew, and said elsewhere, that the page was drawing nothing. The skills
|
|
467
|
+
* state the rule: an unmeasured rung is not a failure, and this is exactly
|
|
468
|
+
* the unmeasured case.
|
|
469
|
+
*/
|
|
470
|
+
rungs.push({
|
|
471
|
+
name: 'draws',
|
|
472
|
+
status: 'unmeasured',
|
|
473
|
+
summary: 'the page is HIDDEN — those pixels are whatever it last drew',
|
|
474
|
+
fix: 'bring the window to the front (one covered by another counts as hidden) and run this again',
|
|
475
|
+
});
|
|
452
476
|
} else if (report.black) {
|
|
453
477
|
rungs.push({
|
|
454
478
|
name: 'draws',
|
|
@@ -464,10 +488,26 @@ if (rungs[0].status === 'pass') {
|
|
|
464
488
|
fix: 'check the camera: `incanto-frame --out shot.png` and look at what it is aimed at',
|
|
465
489
|
});
|
|
466
490
|
} else {
|
|
491
|
+
/*
|
|
492
|
+
* A rung that PASSED can still have something to say, and this one was
|
|
493
|
+
* dropping it. `incanto-frame` prints `CLIPPED by the frame edge` and
|
|
494
|
+
* `the subject fills almost nothing` — real observations about a shot that
|
|
495
|
+
* is technically non-black — and the ladder replaced all of it with one
|
|
496
|
+
* coverage number. The `loads` rung has carried its tool's warnings since
|
|
497
|
+
* the day a scene described as "It will render black" came back green.
|
|
498
|
+
*/
|
|
499
|
+
const notes = [];
|
|
500
|
+
if (report.subject.clipped) {
|
|
501
|
+
notes.push('the subject is CLIPPED by the frame edge — part of it is off screen');
|
|
502
|
+
}
|
|
503
|
+
if (report.uniform) {
|
|
504
|
+
notes.push('one flat colour — the camera may be inside geometry, or only the sky is drawn');
|
|
505
|
+
}
|
|
467
506
|
rungs.push({
|
|
468
507
|
name: 'draws',
|
|
469
508
|
status: 'pass',
|
|
470
509
|
summary: `subject fills ${(report.subject.coverage * 100).toFixed(1)}% of the frame`,
|
|
510
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
471
511
|
});
|
|
472
512
|
}
|
|
473
513
|
}
|
package/bin/incanto.mjs
CHANGED
|
@@ -40,6 +40,7 @@ const COMMANDS = {
|
|
|
40
40
|
feel: ['incanto-feel.mjs', 'measure jump height, run speed, facing'],
|
|
41
41
|
frame: ['incanto-frame.mjs', 'what the running page is drawing right now'],
|
|
42
42
|
logs: ['incanto-logs.mjs', 'what the running page has been saying'],
|
|
43
|
+
serve: ['incanto-serve.mjs', 'serve a BUILT game so the tools can measure it'],
|
|
43
44
|
editor: ['incanto-editor.mjs', 'the visual scene composer'],
|
|
44
45
|
model: ['incanto-model.mjs', "a GLB's real bounds, animations and rig"],
|
|
45
46
|
assets: ['incanto-assets.mjs', 'the built-in art and audio catalog'],
|