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/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",
|
|
@@ -85,3 +85,25 @@ export function behaviorsHelp(file, why) {
|
|
|
85
85
|
: '\nPass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.')
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The files a `--behaviors` value names — one, or several.
|
|
91
|
+
*
|
|
92
|
+
* The flag took exactly one module and every bin's help said `FILE`, while the
|
|
93
|
+
* comment two functions up already knew that authors "split their behaviours
|
|
94
|
+
* across files — which is most of them, eventually". A game whose bosses live
|
|
95
|
+
* in `boss.ts` and whose scene table lives in `scenes.ts` had one way through:
|
|
96
|
+
* write a barrel that re-exports both, for the CLI's benefit and nothing
|
|
97
|
+
* else's. Comma-separate them instead, or repeat the flag.
|
|
98
|
+
*
|
|
99
|
+
* Splitting on the comma is safe on every platform this ships to: a comma is
|
|
100
|
+
* legal in a POSIX filename and effectively never used in one, and the repeated
|
|
101
|
+
* form is there for anyone who has managed it.
|
|
102
|
+
*/
|
|
103
|
+
export function behaviorFiles(value) {
|
|
104
|
+
const many = Array.isArray(value) ? value : [value];
|
|
105
|
+
return many
|
|
106
|
+
.flatMap((v) => String(v ?? '').split(','))
|
|
107
|
+
.map((f) => f.trim())
|
|
108
|
+
.filter(Boolean);
|
|
109
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a JSON file the way every other tool does: without the BOM.
|
|
3
|
+
*
|
|
4
|
+
* A UTF-8 byte order mark is legal in a UTF-8 file and meaningless in it, and
|
|
5
|
+
* `JSON.parse` refuses it. Windows PowerShell 5.1's `Set-Content` writes one by
|
|
6
|
+
* default, Notepad offers it, and several editors add it on "save as UTF-8" —
|
|
7
|
+
* so a scene written by a perfectly ordinary tool came back as
|
|
8
|
+
*
|
|
9
|
+
* [BAD_JSON] Unexpected token '', "{ "format"... is not valid JSON
|
|
10
|
+
*
|
|
11
|
+
* about a character that is ZERO WIDTH. The author sees valid JSON, an error
|
|
12
|
+
* pointing at nothing, and no way forward. Node's own `require()` of a .json
|
|
13
|
+
* file has stripped it for a decade; `JSON.parse` is the one door that does not.
|
|
14
|
+
*
|
|
15
|
+
* Every bin reads project JSON through here now, so the answer is the same
|
|
16
|
+
* wherever a file arrives from.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
|
|
20
|
+
/** Strip a leading UTF-8 BOM. Everything else is left exactly as written. */
|
|
21
|
+
export function stripBom(text) {
|
|
22
|
+
return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** `JSON.parse` of a file, BOM and all. Throws the parser's own error. */
|
|
26
|
+
export function readJson(file) {
|
|
27
|
+
return JSON.parse(stripBom(readFileSync(file, 'utf-8')));
|
|
28
|
+
}
|
package/bin/incanto-assets.mjs
CHANGED
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
* External art still works the usual way (any URL in scene assets{}) — this
|
|
13
13
|
* is the zero-setup starting set.
|
|
14
14
|
*/
|
|
15
|
-
import { copyFileSync, mkdirSync
|
|
15
|
+
import { copyFileSync, mkdirSync } from 'node:fs';
|
|
16
16
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
18
|
+
import { readJson } from './_read-json.mjs';
|
|
18
19
|
|
|
19
20
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
21
|
const ASSETS = join(PKG, 'assets');
|
|
21
22
|
|
|
22
23
|
function catalog() {
|
|
23
|
-
return
|
|
24
|
+
return readJson(join(ASSETS, 'catalog.json'));
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
const [command, ...rest] = process.argv.slice(2);
|
|
@@ -40,8 +41,21 @@ if (command === 'list') {
|
|
|
40
41
|
} else {
|
|
41
42
|
for (const e of entries) {
|
|
42
43
|
const anim = e.animation ? ' [animated]' : '';
|
|
44
|
+
/*
|
|
45
|
+
* The GRID, from the fields rather than the prose.
|
|
46
|
+
*
|
|
47
|
+
* `list` printed only the description, and a description carrying a frame
|
|
48
|
+
* size is a second source of truth that drifts: `2dbasic` said
|
|
49
|
+
* "(frame size 192x192)" against a real grid of 111x83 — 666x664 does not
|
|
50
|
+
* divide by 192 — and the wrong number was the first one an agent read.
|
|
51
|
+
* The sizes live in `frameWidth`/`frameHeight` now, and the prose says
|
|
52
|
+
* what the art IS.
|
|
53
|
+
*/
|
|
54
|
+
const grid = e.frameWidth
|
|
55
|
+
? ` ${e.frameWidth}×${e.frameHeight}${e.tiles ? ` (${e.tiles} tiles)` : ''}`
|
|
56
|
+
: '';
|
|
43
57
|
console.log(
|
|
44
|
-
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)} ${e.description.slice(0,
|
|
58
|
+
`${e.name.padEnd(20)} ${e.kind.padEnd(10)}${anim.padEnd(11)}${grid.padEnd(16)} ${e.description.slice(0, 64)}`,
|
|
45
59
|
);
|
|
46
60
|
}
|
|
47
61
|
console.log(`\n${entries.length} built-in asset(s) — incanto-assets info <name> for details`);
|
|
@@ -63,7 +77,7 @@ if (command === 'list') {
|
|
|
63
77
|
if (command === 'info') {
|
|
64
78
|
console.log(JSON.stringify(entry, null, 2));
|
|
65
79
|
if (entry.animation) {
|
|
66
|
-
const meta =
|
|
80
|
+
const meta = readJson(join(ASSETS, entry.animation));
|
|
67
81
|
console.log('\nanimations:', Object.keys(meta.animations ?? {}).join(', '));
|
|
68
82
|
}
|
|
69
83
|
process.exitCode = 0; // see the list note — exit() truncates piped stdout
|
|
@@ -85,7 +99,7 @@ if (command === 'list') {
|
|
|
85
99
|
const { spriteFromLibraryMeta } = await import(
|
|
86
100
|
pathToFileURL(join(PKG, 'dist', '2d.js')).href
|
|
87
101
|
);
|
|
88
|
-
const meta =
|
|
102
|
+
const meta = readJson(join(ASSETS, entry.animation));
|
|
89
103
|
const { asset, props } = spriteFromLibraryMeta(meta, { url, assetKey: entry.name });
|
|
90
104
|
console.log(`\nscene assets entry:\n "${entry.name}": ${JSON.stringify(asset)}`);
|
|
91
105
|
console.log(
|
package/bin/incanto-check.mjs
CHANGED
|
@@ -14,18 +14,33 @@
|
|
|
14
14
|
* that needs no TypeScript. Sub-scene `instance` paths resolve relative to
|
|
15
15
|
* each scene file.
|
|
16
16
|
*/
|
|
17
|
-
import { existsSync, lstatSync, readdirSync,
|
|
17
|
+
import { existsSync, lstatSync, readdirSync, statSync } from 'node:fs';
|
|
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 { behaviorFiles, behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
22
|
+
import { readJson } from './_read-json.mjs';
|
|
21
23
|
|
|
22
24
|
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
23
25
|
|
|
24
26
|
function parseArgs(argv) {
|
|
25
|
-
const args = {
|
|
26
|
-
|
|
27
|
+
const args = {
|
|
28
|
+
files: [],
|
|
29
|
+
json: false,
|
|
30
|
+
strict: false,
|
|
31
|
+
help: false,
|
|
32
|
+
invalid: false,
|
|
33
|
+
behaviors: null,
|
|
34
|
+
};
|
|
35
|
+
// Indexed, because `--behaviors` takes the NEXT argument. A `for…of` with a
|
|
36
|
+
// hand-rolled `++i` reads fine and throws `i is not defined` at runtime,
|
|
37
|
+
// which is a flag that exists in the help and cannot be used.
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const a = argv[i];
|
|
27
40
|
if (a === '--json') args.json = true;
|
|
28
41
|
else if (a === '--strict-behaviors') args.strict = true;
|
|
42
|
+
// Repeatable: behaviours legitimately live in more than one file.
|
|
43
|
+
else if (a === '--behaviors') args.behaviors = [...(args.behaviors ?? []), argv[++i]];
|
|
29
44
|
else if (a === '--strict-warnings') args.strictWarnings = true;
|
|
30
45
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
31
46
|
else if (a.startsWith('--')) {
|
|
@@ -38,7 +53,7 @@ function parseArgs(argv) {
|
|
|
38
53
|
|
|
39
54
|
const args = parseArgs(process.argv.slice(2));
|
|
40
55
|
if (args.help || args.invalid) {
|
|
41
|
-
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--strict-behaviors] [--strict-warnings]
|
|
56
|
+
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--behaviors FILE[,FILE...]] [--strict-behaviors] [--strict-warnings]
|
|
42
57
|
|
|
43
58
|
Validates *.scene.json files headlessly with the installed engine.
|
|
44
59
|
With no arguments, scans the current directory recursively
|
|
@@ -73,12 +88,31 @@ function collectScenes(entry) {
|
|
|
73
88
|
*/
|
|
74
89
|
function missingArt(file, json) {
|
|
75
90
|
const assets = json && typeof json === 'object' ? json.assets : null;
|
|
76
|
-
if (!assets || typeof assets !== 'object') return [];
|
|
77
91
|
const sceneDir = dirname(file);
|
|
78
92
|
const root = projectRoot(sceneDir);
|
|
79
93
|
const out = [];
|
|
80
|
-
|
|
81
|
-
|
|
94
|
+
/*
|
|
95
|
+
* A url is named in TWO places, and this only ever looked at one.
|
|
96
|
+
*
|
|
97
|
+
* `assets{}` is where the 2D sprite props and `ModelInstance3D` point; the
|
|
98
|
+
* 3D texture props, `AudioPlayer.src`, `material.map` and a raw-url
|
|
99
|
+
* `UiImage.src` take the url DIRECTLY, and for three of them a url is the
|
|
100
|
+
* only spelling accepted. None of those was ever checked, while
|
|
101
|
+
* `incanto-verify` reported "its assets resolve" over the top of them.
|
|
102
|
+
*/
|
|
103
|
+
const named = [
|
|
104
|
+
...Object.entries(assets && typeof assets === 'object' ? assets : {}).map(([key, decl]) => ({
|
|
105
|
+
what: `$${key}`,
|
|
106
|
+
kind: decl && typeof decl === 'object' ? decl.type : '',
|
|
107
|
+
url: decl && typeof decl === 'object' ? decl.url : null,
|
|
108
|
+
})),
|
|
109
|
+
...sceneAssetUrls(json).map((u) => ({
|
|
110
|
+
what: `${u.path}.${u.prop}`,
|
|
111
|
+
kind: u.kind,
|
|
112
|
+
url: u.url,
|
|
113
|
+
})),
|
|
114
|
+
];
|
|
115
|
+
for (const { what, kind, url } of named) {
|
|
82
116
|
if (typeof url !== 'string' || url === '') continue;
|
|
83
117
|
// Remote and inline are not this command's business to guess about.
|
|
84
118
|
if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')) continue;
|
|
@@ -100,9 +134,9 @@ function missingArt(file, json) {
|
|
|
100
134
|
// warned about art that works.
|
|
101
135
|
if (resolvesAsPackageAsset(url, sceneDir)) continue;
|
|
102
136
|
out.push(
|
|
103
|
-
|
|
137
|
+
`${what} → ${url} is not in the project ` +
|
|
104
138
|
`(looked in ${relativeish(root, join(root, 'public'))}, ${relativeish(root, root)}` +
|
|
105
|
-
`, and beside the scene). It will draw nothing.`,
|
|
139
|
+
`, and beside the scene). It will ${kind === 'audio' ? 'be silent' : 'draw nothing'}.`,
|
|
106
140
|
);
|
|
107
141
|
}
|
|
108
142
|
return out;
|
|
@@ -155,9 +189,39 @@ if (files.length === 0) {
|
|
|
155
189
|
process.exit(1);
|
|
156
190
|
}
|
|
157
191
|
|
|
158
|
-
const { auditScene, instancedScenes, validateScene } =
|
|
159
|
-
pathToFileURL(join(DIST, 'test.js')).href
|
|
160
|
-
|
|
192
|
+
const { auditScene, instancedScenes, registerAllNodes, sceneAssetUrls, validateScene } =
|
|
193
|
+
await import(pathToFileURL(join(DIST, 'test.js')).href);
|
|
194
|
+
// `sceneAssetUrls` reads the node registry to know which props name a resource.
|
|
195
|
+
registerAllNodes();
|
|
196
|
+
|
|
197
|
+
/** Your Behavior subclasses, when you name a file — same contract as the other bins. */
|
|
198
|
+
const behaviors = {};
|
|
199
|
+
const behaviorSources = behaviorFiles(args.behaviors);
|
|
200
|
+
if (behaviorSources.length > 0) {
|
|
201
|
+
const { Behavior } = await import(pathToFileURL(join(DIST, 'index.js')).href);
|
|
202
|
+
for (const file of behaviorSources) {
|
|
203
|
+
let mod;
|
|
204
|
+
try {
|
|
205
|
+
mod = await import(pathToFileURL(resolve(file)).href);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
const why = e?.message ?? String(e);
|
|
208
|
+
// node cannot import the pattern this package teaches (a JSON import
|
|
209
|
+
// without an attribute, an extensionless relative path); bun can. Same
|
|
210
|
+
// hand-off as `incanto-playtest`.
|
|
211
|
+
reexecUnderBun(import.meta.url, why);
|
|
212
|
+
console.error(behaviorsHelp(file, why));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
for (const [name, value] of Object.entries(mod)) {
|
|
216
|
+
if (typeof value === 'function' && value.prototype instanceof Behavior) {
|
|
217
|
+
behaviors[name] = value;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (Object.keys(behaviors).length === 0) {
|
|
222
|
+
console.error(`warning: no Behavior subclasses exported from ${behaviorSources.join(', ')}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
161
225
|
|
|
162
226
|
/**
|
|
163
227
|
* Every `strings` table in the corpus, unioned.
|
|
@@ -186,7 +250,7 @@ const declaredAssets = {};
|
|
|
186
250
|
const corpus = [];
|
|
187
251
|
for (const file of files) {
|
|
188
252
|
try {
|
|
189
|
-
const doc =
|
|
253
|
+
const doc = readJson(file);
|
|
190
254
|
corpus.push({ path: resolve(file), json: doc });
|
|
191
255
|
for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
|
|
192
256
|
if (typeof entries !== 'object' || entries === null) continue;
|
|
@@ -205,14 +269,27 @@ const instanced = instancedScenes(corpus, (from, ref) => resolve(dirname(from),
|
|
|
205
269
|
const results = files.map((file) => {
|
|
206
270
|
let json;
|
|
207
271
|
try {
|
|
208
|
-
json =
|
|
272
|
+
json = readJson(file);
|
|
209
273
|
} catch (e) {
|
|
210
274
|
return { file, ok: false, code: 'BAD_JSON', message: String(e?.message ?? e) };
|
|
211
275
|
}
|
|
212
276
|
const res = validateScene(json, {
|
|
213
277
|
strictBehaviors: args.strict,
|
|
278
|
+
/*
|
|
279
|
+
* `validateScene` has always taken `behaviors`; this bin had no way to pass
|
|
280
|
+
* any. So `--strict-behaviors` could only ever FAIL on a project with a
|
|
281
|
+
* behaviour of its own — which is every project past its first hour:
|
|
282
|
+
*
|
|
283
|
+
* [UNKNOWN_BEHAVIOR] Unknown behavior 'Hunter'. … Did you forget
|
|
284
|
+
* registerBehavior('Hunter', Hunter)?
|
|
285
|
+
* 0/1 scene(s) valid EXIT=1
|
|
286
|
+
*
|
|
287
|
+
* A flag whose only outcome is failure is a flag nobody uses, and the check
|
|
288
|
+
* it gates — a script name that resolves nowhere — is worth having.
|
|
289
|
+
*/
|
|
290
|
+
behaviors,
|
|
214
291
|
declaredAssets,
|
|
215
|
-
resolveScene: (p) =>
|
|
292
|
+
resolveScene: (p) => readJson(resolve(dirname(file), p)),
|
|
216
293
|
});
|
|
217
294
|
if (res.ok) {
|
|
218
295
|
return {
|
package/bin/incanto-editor.mjs
CHANGED
|
@@ -31,9 +31,11 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
|
|
|
31
31
|
import { createServer } from 'node:http';
|
|
32
32
|
import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
|
|
33
33
|
import { fileURLToPath } from 'node:url';
|
|
34
|
+
import { readJson } from './_read-json.mjs';
|
|
34
35
|
|
|
35
36
|
const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
36
37
|
const EDITOR_DIR = join(PKG_ROOT, 'editor');
|
|
38
|
+
const ASSETS_DIR = join(PKG_ROOT, 'assets');
|
|
37
39
|
|
|
38
40
|
// Biome-compatible JSON layout — same algorithm as the editor's
|
|
39
41
|
// src/format-json.ts (inline when the one-line form fits 80 cols).
|
|
@@ -100,10 +102,39 @@ const MODE = args.input ? 'single' : 'project';
|
|
|
100
102
|
const ROOT = resolve('.');
|
|
101
103
|
const INPUT = args.input ? resolve(args.input) : null;
|
|
102
104
|
const OUTPUT = args.input ? resolve(args.output ?? args.input) : null;
|
|
103
|
-
const VERSION =
|
|
105
|
+
const VERSION = readJson(join(PKG_ROOT, 'package.json')).version;
|
|
104
106
|
|
|
105
107
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'out', 'coverage']);
|
|
106
108
|
|
|
109
|
+
/*
|
|
110
|
+
* A named scene that is not there is a typo, and the banner used to congratulate
|
|
111
|
+
* you on it:
|
|
112
|
+
*
|
|
113
|
+
* $ incanto-editor typo.scene.json
|
|
114
|
+
* incanto-editor v0.69.0
|
|
115
|
+
* scene: /private/tmp/ed/typo.scene.json
|
|
116
|
+
* url: http://127.0.0.1:5399/
|
|
117
|
+
*
|
|
118
|
+
* The server started, the terminal reported success, and the failure waited in
|
|
119
|
+
* the browser — where it reads as the editor being broken rather than as the
|
|
120
|
+
* path being wrong. Project mode is the mode that CREATES scenes; naming one is
|
|
121
|
+
* a request to edit a file that exists.
|
|
122
|
+
*/
|
|
123
|
+
if (MODE === 'single' && !existsSync(INPUT)) {
|
|
124
|
+
console.error(`no such scene: ${INPUT}`);
|
|
125
|
+
const near = discoverScenes()
|
|
126
|
+
.filter((f) => !f.notScene)
|
|
127
|
+
.slice(0, 5);
|
|
128
|
+
if (near.length > 0) {
|
|
129
|
+
console.error('\nscenes in this project:');
|
|
130
|
+
for (const f of near) console.error(` ${f.rel}`);
|
|
131
|
+
console.error('\nor run `incanto-editor` with no file to browse them all.');
|
|
132
|
+
} else {
|
|
133
|
+
console.error('\nrun `incanto-editor` with no file to browse this project.');
|
|
134
|
+
}
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
|
|
107
138
|
// --- agent8 asset library -------------------------------------------------
|
|
108
139
|
const LIBRARY_URL =
|
|
109
140
|
process.env.INCANTO_LIBRARY_URL ?? 'https://agent8-backend.verse8.io/api/resources';
|
|
@@ -212,7 +243,7 @@ function projectDimension() {
|
|
|
212
243
|
let d3 = 0;
|
|
213
244
|
for (const entry of discoverScenes()) {
|
|
214
245
|
try {
|
|
215
|
-
const json =
|
|
246
|
+
const json = readJson(join(ROOT, entry.rel ?? entry));
|
|
216
247
|
if (json?.dimension === '3d') d3 += 1;
|
|
217
248
|
else if (json?.dimension === '2d') d2 += 1;
|
|
218
249
|
} catch {
|
|
@@ -294,7 +325,7 @@ function inspectJson(abs, size) {
|
|
|
294
325
|
if (size > 16_000_000) return { notScene: 'too large to check without opening it' };
|
|
295
326
|
let parsed;
|
|
296
327
|
try {
|
|
297
|
-
parsed =
|
|
328
|
+
parsed = readJson(abs);
|
|
298
329
|
} catch (error) {
|
|
299
330
|
return { notScene: `not valid JSON: ${String(error.message ?? error)}` };
|
|
300
331
|
}
|
|
@@ -334,6 +365,15 @@ const MIME = {
|
|
|
334
365
|
'.json': 'application/json; charset=utf-8',
|
|
335
366
|
'.svg': 'image/svg+xml',
|
|
336
367
|
'.png': 'image/png',
|
|
368
|
+
'.jpg': 'image/jpeg',
|
|
369
|
+
'.jpeg': 'image/jpeg',
|
|
370
|
+
'.webp': 'image/webp',
|
|
371
|
+
'.gif': 'image/gif',
|
|
372
|
+
'.mp3': 'audio/mpeg',
|
|
373
|
+
'.ogg': 'audio/ogg',
|
|
374
|
+
'.wav': 'audio/wav',
|
|
375
|
+
'.glb': 'model/gltf-binary',
|
|
376
|
+
'.gltf': 'model/gltf+json',
|
|
337
377
|
'.wasm': 'application/wasm',
|
|
338
378
|
};
|
|
339
379
|
|
|
@@ -521,6 +561,46 @@ const server = createServer((req, res) => {
|
|
|
521
561
|
}),
|
|
522
562
|
);
|
|
523
563
|
}
|
|
564
|
+
/*
|
|
565
|
+
* REFUSE to overwrite a file that is not already a scene.
|
|
566
|
+
*
|
|
567
|
+
* The body check above asks "is this a scene"; nothing asked what it
|
|
568
|
+
* was about to land on. `?file=` accepts any `.json` under the project
|
|
569
|
+
* on purpose — the browser lists every JSON and the LOAD decides, which
|
|
570
|
+
* is how a scene saved as `level.json` still opens — so the write path
|
|
571
|
+
* was one query parameter away from destroying a manifest:
|
|
572
|
+
*
|
|
573
|
+
* $ curl -X PUT -d '{…a valid scene…}' '…/api/scene?file=package.json'
|
|
574
|
+
* {"ok":true,"output":"/private/tmp/repro/package.json"} [200]
|
|
575
|
+
* $ cat package.json
|
|
576
|
+
* {"format":1,"type":"scene",…}
|
|
577
|
+
*
|
|
578
|
+
* And the GET on the same server already refuses a non-scene WITH the
|
|
579
|
+
* reason, so the two halves of one endpoint disagreed about the rule.
|
|
580
|
+
*
|
|
581
|
+
* A file that does not exist yet is fine: that is `create`.
|
|
582
|
+
*/
|
|
583
|
+
if (existsSync(target.output)) {
|
|
584
|
+
let existing = null;
|
|
585
|
+
try {
|
|
586
|
+
existing = readJson(target.output);
|
|
587
|
+
} catch {
|
|
588
|
+
// Unparseable: not a scene, and not ours to replace either.
|
|
589
|
+
}
|
|
590
|
+
const already =
|
|
591
|
+
existing === null ? { ok: false, why: 'it is not JSON' } : sceneFacts(existing);
|
|
592
|
+
if (!already.ok) {
|
|
593
|
+
return send(
|
|
594
|
+
res,
|
|
595
|
+
409,
|
|
596
|
+
JSON.stringify({
|
|
597
|
+
error:
|
|
598
|
+
`refusing to overwrite ${target.output} — ${already.why}. ` +
|
|
599
|
+
'The editor only replaces scenes, and only creates new files.',
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
524
604
|
try {
|
|
525
605
|
// Write the client's bytes verbatim — the editor formats scenes
|
|
526
606
|
// biome-compatibly; re-stringifying here would undo that.
|
|
@@ -535,6 +615,39 @@ const server = createServer((req, res) => {
|
|
|
535
615
|
return send(res, 405, JSON.stringify({ error: 'method not allowed' }));
|
|
536
616
|
}
|
|
537
617
|
|
|
618
|
+
/*
|
|
619
|
+
* The BUILT-IN art, at the path the scenes already name.
|
|
620
|
+
*
|
|
621
|
+
* `incanto-assets` prints `incanto/assets/items/coin.png` as an asset's url
|
|
622
|
+
* and three shipped 2D templates carry exactly that — a BUNDLER specifier,
|
|
623
|
+
* which the game's vite resolves and a browser cannot. In the game the
|
|
624
|
+
* template's `main.ts` overwrites each url with the import; the editor loads
|
|
625
|
+
* the scene JSON as written, so it asked this server for
|
|
626
|
+
* `/incanto/assets/characters/medieval-knight.png` and got 404. Every sprite
|
|
627
|
+
* in `platformer-2d`, `star-survivor` and `topdown-rpg` was a blank quad in
|
|
628
|
+
* the composer they ship with.
|
|
629
|
+
*
|
|
630
|
+
* Serving the package's own `assets/` here makes the written url resolve, so
|
|
631
|
+
* the art is live in the editor without any scene change and without the
|
|
632
|
+
* runtime injection having to know about the editor.
|
|
633
|
+
*/
|
|
634
|
+
if (url.pathname.startsWith('/incanto/assets/')) {
|
|
635
|
+
const asset = normalize(join(ASSETS_DIR, url.pathname.slice('/incanto/assets/'.length)));
|
|
636
|
+
if (!asset.startsWith(ASSETS_DIR + sep)) {
|
|
637
|
+
return send(res, 403, JSON.stringify({ error: 'forbidden' }));
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
return send(
|
|
641
|
+
res,
|
|
642
|
+
200,
|
|
643
|
+
readFileSync(asset),
|
|
644
|
+
MIME[extname(asset)] ?? 'application/octet-stream',
|
|
645
|
+
);
|
|
646
|
+
} catch {
|
|
647
|
+
return send(res, 404, JSON.stringify({ error: `no built-in asset: ${url.pathname}` }));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
538
651
|
// static editor assets (path-traversal guarded)
|
|
539
652
|
const rel = url.pathname === '/' ? '/index.html' : url.pathname;
|
|
540
653
|
const file = normalize(join(EDITOR_DIR, rel));
|
|
@@ -559,9 +672,19 @@ server.listen(args.port, args.host, () => {
|
|
|
559
672
|
if (MODE === 'single') {
|
|
560
673
|
console.log(` scene: ${INPUT}${OUTPUT === INPUT ? '' : `\n output: ${OUTPUT}`}`);
|
|
561
674
|
} else {
|
|
562
|
-
|
|
675
|
+
/*
|
|
676
|
+
* `discoverScenes()` returns every `.json` in the project — that is the
|
|
677
|
+
* point, because the BROWSER lists them all and marks which ones are
|
|
678
|
+
* scenes. Counting the list and calling the total "scene files" made the
|
|
679
|
+
* banner say `4 scene files` about a project holding one scene,
|
|
680
|
+
* `package.json`, `tsconfig.json` and `biome.json`.
|
|
681
|
+
*/
|
|
682
|
+
const found = discoverScenes();
|
|
683
|
+
const scenes = found.filter((f) => !f.notScene);
|
|
684
|
+
const other = found.length - scenes.length;
|
|
563
685
|
console.log(
|
|
564
|
-
` project: ${ROOT} (${scenes.length} scene
|
|
686
|
+
` project: ${ROOT} (${scenes.length} scene${scenes.length === 1 ? '' : 's'}` +
|
|
687
|
+
`${other > 0 ? `, ${other} other .json file${other === 1 ? '' : 's'} listed` : ''})`,
|
|
565
688
|
);
|
|
566
689
|
}
|
|
567
690
|
console.log(
|
package/bin/incanto-env.mjs
CHANGED
|
@@ -15,9 +15,10 @@
|
|
|
15
15
|
* generateScatter is library-only (`import {generateScatter} from
|
|
16
16
|
* 'incanto/env'`) — its item templates don't fit CLI flags.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { writeFileSync } from 'node:fs';
|
|
19
19
|
import { dirname, join, resolve } from 'node:path';
|
|
20
20
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
21
|
+
import { readJson } from './_read-json.mjs';
|
|
21
22
|
|
|
22
23
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
24
|
|
|
@@ -128,7 +129,7 @@ const countNodes = (n) => 1 + (n.children ?? []).reduce((sum, c) => sum + countN
|
|
|
128
129
|
if (flags.has('--into')) {
|
|
129
130
|
const file = resolve(flags.get('--into'));
|
|
130
131
|
const at = flags.get('--at');
|
|
131
|
-
const scene =
|
|
132
|
+
const scene = readJson(file);
|
|
132
133
|
const out = env.insertIntoScene(scene, node, at);
|
|
133
134
|
writeFileSync(file, `${JSON.stringify(out, null, 2)}\n`);
|
|
134
135
|
console.log(
|
package/bin/incanto-feel.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import { dirname, join, resolve } from 'node:path';
|
|
4
4
|
/**
|
|
5
5
|
* incanto-feel — what your controls actually FEEL like, as numbers.
|
|
@@ -21,7 +21,8 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
21
21
|
* wrong for most games.
|
|
22
22
|
*/
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
-
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
24
|
+
import { behaviorFiles, behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
25
|
+
import { readJson } from './_read-json.mjs';
|
|
25
26
|
|
|
26
27
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
28
|
|
|
@@ -29,13 +30,19 @@ function parseArgs(argv) {
|
|
|
29
30
|
const args = {};
|
|
30
31
|
for (let i = 0; i < argv.length; i++) {
|
|
31
32
|
const a = argv[i];
|
|
32
|
-
|
|
33
|
+
// Repeatable: behaviours legitimately live in more than one file.
|
|
34
|
+
if (a === '--behaviors') args.behaviors = [...(args.behaviors ?? []), argv[++i]];
|
|
33
35
|
else if (a === '--move') args.move = argv[++i];
|
|
34
36
|
else if (a === '--jump') args.jump = argv[++i];
|
|
35
37
|
else if (a === '--json') args.json = true;
|
|
36
38
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
37
39
|
else if (a.startsWith('--')) {
|
|
40
|
+
// `invalid`, not `help`: printing usage and exiting 0 told an agent its
|
|
41
|
+
// typo was a request. `--runs=4`, `--behaviours`, `--no-replay` all landed
|
|
42
|
+
// here and looked like success — and with `--json` the machine path was
|
|
43
|
+
// literally silent (0 bytes on stdout).
|
|
38
44
|
console.error(`unknown flag: ${a}`);
|
|
45
|
+
args.invalid = true;
|
|
39
46
|
args.help = true;
|
|
40
47
|
} else if (!args.scene) args.scene = a;
|
|
41
48
|
}
|
|
@@ -47,10 +54,13 @@ if (args.help || !args.scene) {
|
|
|
47
54
|
console.error(`Usage: incanto-feel <scene.json> [options]
|
|
48
55
|
|
|
49
56
|
--behaviors FILE your Behavior subclasses (.ts works on node >= 23.6 / bun)
|
|
50
|
-
|
|
57
|
+
comma-separate or repeat for several files
|
|
58
|
+
--move ACTION the vector action to move with, and ONLY that one
|
|
59
|
+
(default: the first vector2, then every other one, each
|
|
60
|
+
pushed +x/-y/+y/-x until the player moves)
|
|
51
61
|
--jump ACTION the button to jump with (default: "jump")
|
|
52
62
|
--json the report as JSON instead of prose`);
|
|
53
|
-
process.exit(args.help && args.scene !== undefined ? 0 : 1);
|
|
63
|
+
process.exit(!args.invalid && args.help && args.scene !== undefined ? 0 : 1);
|
|
54
64
|
}
|
|
55
65
|
|
|
56
66
|
const { facingReport, facingText, feelReport, feelText } = await import(
|
|
@@ -59,14 +69,15 @@ const { facingReport, facingText, feelReport, feelText } = await import(
|
|
|
59
69
|
const incanto = await import(pathToFileURL(join(PKG, 'dist', 'index.js')).href);
|
|
60
70
|
|
|
61
71
|
const behaviors = {};
|
|
62
|
-
|
|
72
|
+
const behaviorSources = behaviorFiles(args.behaviors);
|
|
73
|
+
for (const file of behaviorSources) {
|
|
63
74
|
let mod;
|
|
64
75
|
try {
|
|
65
|
-
mod = await import(pathToFileURL(resolve(
|
|
76
|
+
mod = await import(pathToFileURL(resolve(file)).href);
|
|
66
77
|
} catch (e) {
|
|
67
78
|
const why = e?.message ?? String(e);
|
|
68
79
|
reexecUnderBun(import.meta.url, why);
|
|
69
|
-
console.error(behaviorsHelp(
|
|
80
|
+
console.error(behaviorsHelp(file, why));
|
|
70
81
|
process.exit(1);
|
|
71
82
|
}
|
|
72
83
|
for (const [name, value] of Object.entries(mod)) {
|
|
@@ -78,16 +89,16 @@ if (args.behaviors) {
|
|
|
78
89
|
|
|
79
90
|
const scenePath = resolve(args.scene);
|
|
80
91
|
const sceneDir = dirname(scenePath);
|
|
81
|
-
const sceneJson =
|
|
92
|
+
const sceneJson = readJson(scenePath);
|
|
82
93
|
|
|
83
94
|
let report;
|
|
84
95
|
try {
|
|
85
96
|
report = await feelReport(sceneJson, {
|
|
86
97
|
behaviors,
|
|
87
|
-
stubMissingBehaviors:
|
|
98
|
+
stubMissingBehaviors: behaviorSources.length === 0,
|
|
88
99
|
...(args.move ? { moveAction: args.move } : {}),
|
|
89
100
|
...(args.jump ? { jumpAction: args.jump } : {}),
|
|
90
|
-
resolveScene: (p) =>
|
|
101
|
+
resolveScene: (p) => readJson(resolve(sceneDir, p)),
|
|
91
102
|
});
|
|
92
103
|
} catch (e) {
|
|
93
104
|
console.error(`feel failed: ${e?.message ?? e}`);
|
|
@@ -103,8 +114,8 @@ if (!args.json) {
|
|
|
103
114
|
try {
|
|
104
115
|
const facing = await facingReport(sceneJson, {
|
|
105
116
|
behaviors,
|
|
106
|
-
stubMissingBehaviors:
|
|
107
|
-
resolveScene: (p) =>
|
|
117
|
+
stubMissingBehaviors: behaviorSources.length === 0,
|
|
118
|
+
resolveScene: (p) => readJson(resolve(sceneDir, p)),
|
|
108
119
|
});
|
|
109
120
|
if (facing.skin) process.stdout.write(`${facingText(facing)}\n`);
|
|
110
121
|
} catch {
|