incanto 0.68.0 → 0.70.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/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +19 -5
- package/bin/incanto-check.mjs +92 -15
- package/bin/incanto-editor.mjs +128 -5
- package/bin/incanto-env.mjs +3 -2
- package/bin/incanto-feel.mjs +24 -13
- package/bin/incanto-frame.mjs +8 -1
- package/bin/incanto-multiplay.mjs +11 -9
- package/bin/incanto-new.mjs +128 -5
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-serve.mjs +160 -0
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +165 -44
- package/bin/incanto.mjs +4 -2
- package/dist/2d.d.ts +285 -36
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +158 -10
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
- package/dist/{audio-player-DOrq7sP-.d.ts → audio-player-DaMxqfNE.d.ts} +33 -14
- package/dist/{behavior-DoFPYrgo.d.ts → behavior-DZExDn9o.d.ts} +809 -44
- package/dist/{create-game-IZIydDwI.js → create-game-Bwvh6q8A.js} +148 -61
- package/dist/{create-game-DbWtVTxD.js → create-game-C7ffQWW7.js} +103 -49
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +2 -3
- package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
- package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
- package/dist/editor.js +1066 -864
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-CybQXNqS.js → environment-presets-D6Q5BxeE.js} +299 -46
- 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-DM1eu_cV.js → gameplay-BfHkuzVb.js} +825 -221
- package/dist/gameplay.d.ts +187 -7
- package/dist/gameplay.js +1 -1
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +156 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-CcB533FR.d.ts → loader-Cff09LMm.d.ts} +2 -2
- package/dist/net.d.ts +27 -3
- package/dist/net.js +2 -2
- package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
- package/dist/{physics-2d-B7Y6dPZO.js → physics-2d-CE0Qvy3V.js} +136 -11
- package/dist/{physics-3d-bG3n70Ky.js → physics-3d-CpH-2gn5.js} +104 -23
- package/dist/{teardown-D2NEmxPB.js → picking-CQJ_PJKh.js} +106 -14
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-3ta-2Xig.js → register-6DYnKZcy.js} +652 -831
- package/dist/{register-ibCjm-wH.js → register-Bkk0wSDB.js} +348 -30
- package/dist/{replay-CYvhVHHN.js → replay-DjAkAzMq.js} +224 -14
- package/dist/{replay-Dvn8aeBd.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
- package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
- package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
- package/dist/{loader-BC4PNtJX.js → save-slots-BXVg148r.js} +4558 -2294
- package/dist/{sheet-grid-BT6N_Bjs.js → sheet-grid-Cea343VO.js} +6 -2
- package/dist/{split-screen-DhrSzZIB.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
- package/dist/{split-screen-CYwDkbLF.js → split-screen-PL78oVXP.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-D7RIqXYF.js → src-Cxfiv1Hg.js} +2 -17
- package/dist/test-iHYVUcDK.js +4036 -0
- package/dist/test.d.ts +542 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +54 -3
- package/dist/vite.js +349 -17
- package/dist/{webgl-unavailable-N9nQqesw.js → webgl-unavailable-C8aDbGmR.js} +56 -1
- package/editor/assets/agent8-D0MS174y.js +1 -0
- package/editor/assets/{debug-BBhKuBNV.js → debug-BnXkKuYu.js} +2 -2
- package/editor/assets/index-CIu3uc3l.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +7 -16
- package/schemas/scene.schema.json +30 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +25 -3
- package/skills/incanto-audio.md +27 -5
- package/skills/incanto-behaviors-and-scripts.md +83 -6
- package/skills/incanto-building-2d-games.md +106 -8
- package/skills/incanto-building-3d-games.md +118 -6
- package/skills/incanto-editor.md +46 -7
- package/skills/incanto-environment.md +19 -1
- package/skills/incanto-game-feel.md +70 -0
- package/skills/incanto-gameplay-behaviors.md +121 -14
- package/skills/incanto-hud.md +128 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +222 -58
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +78 -2
- package/skills/incanto-save-slots.md +188 -6
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +292 -12
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +5 -2
- package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
- package/templates-app/beacon-isle-3d/package.json +2 -2
- package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
- package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
- package/templates-app/beacon-isle-3d/src/main.ts +24 -4
- package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
- package/templates-app/beacon-isle-3d/verify.ts +3 -1
- package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
- package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
- package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
- package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
- package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
- package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
- package/templates-app/molehill-2d/index.html +73 -0
- package/templates-app/molehill-2d/package.json +23 -0
- package/templates-app/molehill-2d/src/behaviors.ts +198 -0
- package/templates-app/molehill-2d/src/game.scene.json +1255 -0
- package/templates-app/molehill-2d/src/main.ts +41 -0
- package/templates-app/molehill-2d/tsconfig.json +13 -0
- package/templates-app/molehill-2d/verify.ts +247 -0
- package/templates-app/molehill-2d/vite.config.ts +12 -0
- package/templates-app/platformer-2d/index.html +0 -23
- package/templates-app/platformer-2d/package.json +2 -2
- package/templates-app/platformer-2d/src/behaviors.ts +26 -16
- package/templates-app/platformer-2d/src/game.scene.json +143 -625
- package/templates-app/platformer-2d/src/main.ts +35 -13
- package/templates-app/platformer-2d/tsconfig.json +1 -1
- package/templates-app/star-survivor/package.json +2 -2
- package/templates-app/star-survivor/src/game.scene.json +41 -195
- package/templates-app/star-survivor/src/main.ts +28 -7
- package/templates-app/star-survivor/tsconfig.json +1 -1
- package/templates-app/tps-3d/PROJECT/Context.md +1 -1
- package/templates-app/tps-3d/package.json +2 -2
- package/templates-app/tps-3d/src/behaviors.ts +19 -1
- package/templates-app/tps-3d/src/game.scene.json +78 -217
- package/templates-app/tps-3d/src/main.ts +39 -17
- package/templates-app/tps-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
- package/templates-app/village-quest-3d/package.json +2 -2
- package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
- package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
- package/templates-app/village-quest-3d/src/main.ts +24 -4
- package/templates-app/village-quest-3d/src/village.scene.json +199 -838
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-IWIqk0HJ.js +0 -22
- package/dist/json-CwwhxQgb.js +0 -36
- package/dist/registry-CF70EArN.js +0 -212
- package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
- package/dist/test-8hoHeRmo.js +0 -2340
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BrrHOjMJ.js +0 -1
- package/editor/assets/index-eVd0BToA.js +0 -10958
package/bin/incanto-verify.mjs
CHANGED
|
@@ -19,23 +19,20 @@
|
|
|
19
19
|
* what to arrange, and says so in the summary.
|
|
20
20
|
*/
|
|
21
21
|
import { spawnSync } from 'node:child_process';
|
|
22
|
-
import { existsSync, readdirSync,
|
|
22
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
23
23
|
import { dirname, join, relative } from 'node:path';
|
|
24
24
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
25
|
+
import { readJson } from './_read-json.mjs';
|
|
25
26
|
|
|
26
27
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
-
const { ladderText, ladderVerdict } = await import(
|
|
28
|
+
const { firstLine, ladderText, ladderVerdict, saysRung } = await import(
|
|
28
29
|
pathToFileURL(join(PKG, 'dist', 'test.js')).href
|
|
29
30
|
);
|
|
30
31
|
|
|
31
32
|
const argv = process.argv.slice(2);
|
|
32
33
|
const asJson = argv.includes('--json');
|
|
33
|
-
const flag = (name) => {
|
|
34
|
-
const i = argv.indexOf(name);
|
|
35
|
-
return i >= 0 ? argv[i + 1] : null;
|
|
36
|
-
};
|
|
37
34
|
if (argv.includes('--help') || argv.includes('-h')) {
|
|
38
|
-
console.log(`Usage: incanto verify [scene.json] [--behaviors FILE] [--json]
|
|
35
|
+
console.log(`Usage: incanto verify [scene.json] [--behaviors FILE[,FILE...]] [--json]
|
|
39
36
|
|
|
40
37
|
Walks the whole ladder and tells you the ONE thing to do next:
|
|
41
38
|
|
|
@@ -111,7 +108,26 @@ const { scene, candidates } = findScene();
|
|
|
111
108
|
if (!scene) {
|
|
112
109
|
const report = ladderVerdict([], { candidates });
|
|
113
110
|
console.log(asJson ? JSON.stringify(report, null, 2) : ladderText(report));
|
|
114
|
-
|
|
111
|
+
/*
|
|
112
|
+
* Exit 2, not 0.
|
|
113
|
+
*
|
|
114
|
+
* "Unmeasured is not a failure" is the rule for a RUNG — the game is fine and
|
|
115
|
+
* a question could not be asked. This is different: nothing was measured at
|
|
116
|
+
* ALL, and a CI step whose whole job is `incanto-verify` went green having
|
|
117
|
+
* looked at nothing:
|
|
118
|
+
*
|
|
119
|
+
* 2 scenes here, and guessing between them would verify the wrong one:
|
|
120
|
+
* src/game.scene.json
|
|
121
|
+
* src/title.scene.json
|
|
122
|
+
* $ echo $?
|
|
123
|
+
* 0
|
|
124
|
+
*
|
|
125
|
+
* A distinct code rather than 1, so a script can still tell "your game is
|
|
126
|
+
* broken" from "tell me WHICH game" — and `verdict.measured` says the same
|
|
127
|
+
* thing to anything reading `--json`. Multi-scene is the shape of almost
|
|
128
|
+
* every real game, so this is not an edge case for long.
|
|
129
|
+
*/
|
|
130
|
+
process.exit(2);
|
|
115
131
|
}
|
|
116
132
|
if (!existsSync(scene) || !statSync(scene).isFile()) {
|
|
117
133
|
console.error(`no such scene: ${scene}`);
|
|
@@ -129,8 +145,20 @@ if (!existsSync(scene) || !statSync(scene).isFile()) {
|
|
|
129
145
|
* path to a file sitting right next to the scene it had already found.
|
|
130
146
|
*/
|
|
131
147
|
function findBehaviors(sceneFile) {
|
|
132
|
-
|
|
133
|
-
|
|
148
|
+
/*
|
|
149
|
+
* Every `--behaviors`, not the first one.
|
|
150
|
+
*
|
|
151
|
+
* The flag is repeatable in the bins this spawns, and reading only
|
|
152
|
+
* `argv.indexOf` would have silently dropped every file after the first —
|
|
153
|
+
* the CLI accepting an argument and then not using it, which is the shape
|
|
154
|
+
* this round keeps finding. Joined with commas, which is the other form the
|
|
155
|
+
* children take.
|
|
156
|
+
*/
|
|
157
|
+
const named = [];
|
|
158
|
+
for (let i = 0; i < argv.length; i++) {
|
|
159
|
+
if (argv[i] === '--behaviors' && argv[i + 1]) named.push(argv[++i]);
|
|
160
|
+
}
|
|
161
|
+
if (named.length > 0) return { file: named.join(','), guessed: false };
|
|
134
162
|
const near = dirname(sceneFile);
|
|
135
163
|
for (const dir of [near, join(near, '..'), process.cwd(), join(process.cwd(), 'src')]) {
|
|
136
164
|
for (const base of ['behaviors', 'behaviours']) {
|
|
@@ -275,31 +303,62 @@ if (rungs[0].status === 'pass') {
|
|
|
275
303
|
.join(', ') +
|
|
276
304
|
')' +
|
|
277
305
|
(won > 0 ? ` — ${won} did finish it` : ''),
|
|
306
|
+
// The throw itself, not a command to go and find it. This rung used to
|
|
307
|
+
// say "read the throw: incanto-playtest …" — sending its reader to
|
|
308
|
+
// another tool for a line this JSON already carries.
|
|
309
|
+
notes: (out?.errorMessages ?? []).slice(0, 3),
|
|
278
310
|
fix:
|
|
279
311
|
tally('error') > 0
|
|
280
|
-
? `
|
|
312
|
+
? `fix the script named above, then re-run \`incanto-verify ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\``
|
|
281
313
|
: tally('fell') > 0
|
|
282
314
|
? `the player left the world with nothing catching them — give the level a floor, walls, or a respawn (\`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\` prints where)`
|
|
283
315
|
: `nothing moved — check the input map and the controller: \`incanto-playtest ${scene}${behaviors ? ` --behaviors ${behaviors}` : ''}\``,
|
|
284
316
|
});
|
|
285
|
-
} else if (
|
|
317
|
+
} else if (won > 0) {
|
|
318
|
+
/*
|
|
319
|
+
* `r.status === 0` used to stand in for "it finished the game", and the
|
|
320
|
+
* exit code stopped meaning that: `incanto-playtest` now exits 0 for a
|
|
321
|
+
* scene with nobody to drive, because never being asked is not a failure.
|
|
322
|
+
* This rung read that 0 and reported
|
|
323
|
+
*
|
|
324
|
+
* ✓ plays — 0 of 8 seeded runs finished it
|
|
325
|
+
*
|
|
326
|
+
* on a scene with no character in it, right where it had been careful to
|
|
327
|
+
* say `? plays — nothing here is drivable`. The report has `runs`; ask it
|
|
328
|
+
* what happened rather than asking the child how it felt about it.
|
|
329
|
+
*/
|
|
286
330
|
rungs.push({
|
|
287
331
|
name: 'plays',
|
|
288
332
|
status: 'pass',
|
|
289
333
|
summary: `${won} of ${total} seeded runs finished it`,
|
|
290
334
|
});
|
|
291
335
|
} else if (undrivable) {
|
|
336
|
+
/*
|
|
337
|
+
* "Nobody to move" was said about games the bot had PLAYED.
|
|
338
|
+
*
|
|
339
|
+
* The driver has driven the mouse for a while, and this rung only ever
|
|
340
|
+
* asked whether a character could walk — so a flip puzzle whose every tile
|
|
341
|
+
* the bot clicked, whose `clicked` wires all fired and whose board changed
|
|
342
|
+
* state, was told to add a character controller. A whole class of games —
|
|
343
|
+
* match-3, tower defense, cards, point-and-click, RTS, board games — has no
|
|
344
|
+
* character to add, and the advice was to add one anyway.
|
|
345
|
+
*/
|
|
346
|
+
const clicks = (out?.runs ?? []).reduce((n, r) => n + (r.clicks ?? 0), 0);
|
|
292
347
|
rungs.push({
|
|
293
348
|
name: 'plays',
|
|
294
349
|
status: 'unmeasured',
|
|
295
350
|
summary:
|
|
296
|
-
|
|
297
|
-
? `
|
|
298
|
-
:
|
|
351
|
+
clicks > 0
|
|
352
|
+
? `played with the MOUSE — ${clicks} click(s) across ${total} runs, and no win to reach`
|
|
353
|
+
: out?.hasPlayer === false
|
|
354
|
+
? `nothing here is drivable — ${total} runs had nobody to move and nothing to click`
|
|
355
|
+
: `${total} runs went nowhere, and your behaviours were not loaded — that is most of what moves a character`,
|
|
299
356
|
fix:
|
|
300
|
-
|
|
301
|
-
? '
|
|
302
|
-
:
|
|
357
|
+
clicks > 0
|
|
358
|
+
? 'a mouse game is played, not walked — emit `won`/`lost` and this rung can pass'
|
|
359
|
+
: out?.hasPlayer === false
|
|
360
|
+
? 'give the player a character controller, the `player` group, or the name Player — or `Clickable` on what the cursor drives'
|
|
361
|
+
: `name them: \`incanto-verify ${scene} --behaviors src/behaviors.ts\``,
|
|
303
362
|
});
|
|
304
363
|
} else if (noGoal) {
|
|
305
364
|
rungs.push({
|
|
@@ -317,9 +376,22 @@ if (rungs[0].status === 'pass') {
|
|
|
317
376
|
rungs.push({
|
|
318
377
|
name: 'plays',
|
|
319
378
|
status: 'unmeasured',
|
|
379
|
+
// The parenthetical is only worth printing when it has something in it.
|
|
380
|
+
// It listed `lost` and `unfinished` and nothing else, so a game whose
|
|
381
|
+
// runs all ended at a SCENE SWAP — every tally zero — reported
|
|
382
|
+
// `8 runs played without reaching a win ()`, an empty bracket where the
|
|
383
|
+
// reason should be. `swapped` IS the reason, and it is the one outcome
|
|
384
|
+
// here that explains itself.
|
|
320
385
|
summary:
|
|
321
386
|
`${total} runs played without reaching a win` +
|
|
322
|
-
|
|
387
|
+
(() => {
|
|
388
|
+
const why = [
|
|
389
|
+
count(tally('lost'), 'lost'),
|
|
390
|
+
count(tally('unfinished'), 'ran out the clock'),
|
|
391
|
+
count(tally('swapped'), 'changed scene, where this bot stops measuring'),
|
|
392
|
+
].filter(Boolean);
|
|
393
|
+
return why.length > 0 ? ` (${why.join(', ')})` : '';
|
|
394
|
+
})(),
|
|
323
395
|
fix: 'nothing here is broken — a win that takes skill or a sequence is out of reach of random play. Judge it with a scripted run: `bun run verify`, or `runScript` from `incanto/test`',
|
|
324
396
|
});
|
|
325
397
|
}
|
|
@@ -363,13 +435,36 @@ if (rungs[0].status === 'pass') {
|
|
|
363
435
|
// arithmetic that cannot be true: `✓ feels — 12 of 9 fired`. The number has
|
|
364
436
|
// to add up with the `silent` list beside it.
|
|
365
437
|
const firedDeclared = declaredPaths.length - silent.length;
|
|
438
|
+
/*
|
|
439
|
+
* A rung that exists because "wired but never triggered plays perfectly and
|
|
440
|
+
* feels dead, and every other rung here calls it healthy" — and which then
|
|
441
|
+
* called it healthy itself. `fired.size === 0` failed and ANYTHING else
|
|
442
|
+
* passed, so the flagship starter reported
|
|
443
|
+
*
|
|
444
|
+
* ✓ feels — 13 of 26 fired — silent: /Game/Ward1/ChimeSfx …
|
|
445
|
+
*
|
|
446
|
+
* A tick next to thirteen emitters nobody heard is the same claim the rung
|
|
447
|
+
* was written to stop.
|
|
448
|
+
*
|
|
449
|
+
* It is not a FAILURE either: a random bot does not light three wards, so a
|
|
450
|
+
* win chime it never reached is not a broken wire. That is exactly what
|
|
451
|
+
* `unmeasured` means in this ladder — the tool did not get to look — and it
|
|
452
|
+
* is the one status that cannot be read as approval.
|
|
453
|
+
*/
|
|
366
454
|
rungs.push({
|
|
367
455
|
name: 'feels',
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
456
|
+
...(silent.length === 0
|
|
457
|
+
? { status: 'pass', summary: `every one of the ${declared} sound/effect nodes fired` }
|
|
458
|
+
: {
|
|
459
|
+
status: 'unmeasured',
|
|
460
|
+
summary:
|
|
461
|
+
`${firedDeclared} of ${declared} fired — silent: ${silent.slice(0, 3).join(', ')}` +
|
|
462
|
+
`${silent.length > 3 ? ` +${silent.length - 3}` : ''}`,
|
|
463
|
+
fix:
|
|
464
|
+
'a silent emitter is either a broken wire or a moment random play never ' +
|
|
465
|
+
'reached — the two look identical from here. Reach it in a scripted run ' +
|
|
466
|
+
'(`runScript` from `incanto/test`), and this rung can tell you which.',
|
|
467
|
+
}),
|
|
373
468
|
});
|
|
374
469
|
}
|
|
375
470
|
}
|
|
@@ -381,7 +476,7 @@ if (rungs[0].status === 'pass') {
|
|
|
381
476
|
{
|
|
382
477
|
const isMultiplayer = (() => {
|
|
383
478
|
try {
|
|
384
|
-
const doc =
|
|
479
|
+
const doc = readJson(scene);
|
|
385
480
|
return Boolean(doc?.multiplayer);
|
|
386
481
|
} catch {
|
|
387
482
|
return false;
|
|
@@ -449,6 +544,30 @@ if (rungs[0].status === 'pass') {
|
|
|
449
544
|
? 'bring the preview window to the front — a hidden tab does not render, and a frame is captured inside a render'
|
|
450
545
|
: 'start your dev server, open the page, and run this again — the pixels are in the browser',
|
|
451
546
|
});
|
|
547
|
+
} else if (report.hidden) {
|
|
548
|
+
/*
|
|
549
|
+
* A hidden page is UNMEASURED, not passed.
|
|
550
|
+
*
|
|
551
|
+
* A browser stops animating a page it considers hidden — a window merely
|
|
552
|
+
* covered by another counts — and the back buffer keeps whatever it last
|
|
553
|
+
* drew. Measured at one instant, same page:
|
|
554
|
+
*
|
|
555
|
+
* incanto-frame --do the page is hidden … a frozen game (exit 1)
|
|
556
|
+
* incanto-logs frame rate not measurable — hidden
|
|
557
|
+
* incanto-frame subject fills 17.5% of frame
|
|
558
|
+
* incanto-verify ✓ draws — subject fills 17.5%
|
|
559
|
+
*
|
|
560
|
+
* Two of six rungs reported ✓ off a stale buffer while the engine already
|
|
561
|
+
* knew, and said elsewhere, that the page was drawing nothing. The skills
|
|
562
|
+
* state the rule: an unmeasured rung is not a failure, and this is exactly
|
|
563
|
+
* the unmeasured case.
|
|
564
|
+
*/
|
|
565
|
+
rungs.push({
|
|
566
|
+
name: 'draws',
|
|
567
|
+
status: 'unmeasured',
|
|
568
|
+
summary: 'the page is HIDDEN — those pixels are whatever it last drew',
|
|
569
|
+
fix: 'bring the window to the front (one covered by another counts as hidden) and run this again',
|
|
570
|
+
});
|
|
452
571
|
} else if (report.black) {
|
|
453
572
|
rungs.push({
|
|
454
573
|
name: 'draws',
|
|
@@ -464,10 +583,26 @@ if (rungs[0].status === 'pass') {
|
|
|
464
583
|
fix: 'check the camera: `incanto-frame --out shot.png` and look at what it is aimed at',
|
|
465
584
|
});
|
|
466
585
|
} else {
|
|
586
|
+
/*
|
|
587
|
+
* A rung that PASSED can still have something to say, and this one was
|
|
588
|
+
* dropping it. `incanto-frame` prints `CLIPPED by the frame edge` and
|
|
589
|
+
* `the subject fills almost nothing` — real observations about a shot that
|
|
590
|
+
* is technically non-black — and the ladder replaced all of it with one
|
|
591
|
+
* coverage number. The `loads` rung has carried its tool's warnings since
|
|
592
|
+
* the day a scene described as "It will render black" came back green.
|
|
593
|
+
*/
|
|
594
|
+
const notes = [];
|
|
595
|
+
if (report.subject.clipped) {
|
|
596
|
+
notes.push('the subject is CLIPPED by the frame edge — part of it is off screen');
|
|
597
|
+
}
|
|
598
|
+
if (report.uniform) {
|
|
599
|
+
notes.push('one flat colour — the camera may be inside geometry, or only the sky is drawn');
|
|
600
|
+
}
|
|
467
601
|
rungs.push({
|
|
468
602
|
name: 'draws',
|
|
469
603
|
status: 'pass',
|
|
470
604
|
summary: `subject fills ${(report.subject.coverage * 100).toFixed(1)}% of the frame`,
|
|
605
|
+
...(notes.length > 0 ? { notes } : {}),
|
|
471
606
|
});
|
|
472
607
|
}
|
|
473
608
|
}
|
|
@@ -488,21 +623,11 @@ if (rungs[0].status === 'pass') {
|
|
|
488
623
|
summary: firstLine(r.stderr) ?? 'no page to ask',
|
|
489
624
|
fix: 'open the preview page and run this again — the log buffer is in the browser',
|
|
490
625
|
});
|
|
491
|
-
} else if (report.ok) {
|
|
492
|
-
rungs.push({ name: 'says', status: 'pass', summary: 'no errors, no failed assets' });
|
|
493
626
|
} else {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
// A game whose grass and water are fine still fails here when its player
|
|
499
|
-
// never loaded — which is the case this rung exists for, and the one
|
|
500
|
-
// `draws` calls healthy.
|
|
501
|
-
summary: asset
|
|
502
|
-
? `asset ${asset.ref} never loaded: ${asset.error}`
|
|
503
|
-
: (report.worst?.message ?? `${report.stats?.errors ?? 0} errors were swallowed`),
|
|
504
|
-
fix: 'read it in full: `incanto-logs`',
|
|
505
|
-
});
|
|
627
|
+
// What it MEANS lives in `verify-ladder` with the rest of the pure half —
|
|
628
|
+
// this rung was reading `ok` and dropping every warning the page reported,
|
|
629
|
+
// and there was nowhere to test that without a browser.
|
|
630
|
+
rungs.push(saysRung(report));
|
|
506
631
|
}
|
|
507
632
|
}
|
|
508
633
|
|
|
@@ -516,10 +641,6 @@ function safeJson(text) {
|
|
|
516
641
|
return null;
|
|
517
642
|
}
|
|
518
643
|
}
|
|
519
|
-
function firstLine(text) {
|
|
520
|
-
const line = (text ?? '').split('\n').find((l) => l.trim());
|
|
521
|
-
return line ? line.trim() : null;
|
|
522
|
-
}
|
|
523
644
|
|
|
524
645
|
const verdict = ladderVerdict(rungs);
|
|
525
646
|
if (asJson) {
|
|
@@ -536,4 +657,4 @@ if (asJson) {
|
|
|
536
657
|
// asynchronously, and exiting discards whatever has not flushed. A --json
|
|
537
658
|
// report read by another program came back truncated — silently, and only
|
|
538
659
|
// when piped, which is the only way a program reads it.
|
|
539
|
-
process.exitCode = verdict.ok ? 0 : 1;
|
|
660
|
+
process.exitCode = verdict.measured ? (verdict.ok ? 0 : 1) : 2;
|
package/bin/incanto.mjs
CHANGED
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
* the SAME runtime that started this one, which matters because `--behaviors`
|
|
24
24
|
* loads your TypeScript and bun and node disagree about what they can import.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
import { dirname, join } from 'node:path';
|
|
28
28
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
import { readJson } from './_read-json.mjs';
|
|
29
30
|
|
|
30
31
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
31
32
|
|
|
@@ -40,6 +41,7 @@ const COMMANDS = {
|
|
|
40
41
|
feel: ['incanto-feel.mjs', 'measure jump height, run speed, facing'],
|
|
41
42
|
frame: ['incanto-frame.mjs', 'what the running page is drawing right now'],
|
|
42
43
|
logs: ['incanto-logs.mjs', 'what the running page has been saying'],
|
|
44
|
+
serve: ['incanto-serve.mjs', 'serve a BUILT game so the tools can measure it'],
|
|
43
45
|
editor: ['incanto-editor.mjs', 'the visual scene composer'],
|
|
44
46
|
model: ['incanto-model.mjs', "a GLB's real bounds, animations and rig"],
|
|
45
47
|
assets: ['incanto-assets.mjs', 'the built-in art and audio catalog'],
|
|
@@ -49,7 +51,7 @@ const COMMANDS = {
|
|
|
49
51
|
|
|
50
52
|
function version() {
|
|
51
53
|
try {
|
|
52
|
-
const pkg =
|
|
54
|
+
const pkg = readJson(join(HERE, '..', 'package.json'));
|
|
53
55
|
return pkg.version ?? 'unknown';
|
|
54
56
|
} catch {
|
|
55
57
|
return 'unknown';
|