incanto 0.69.0 → 0.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/_behaviors-loader.mjs +22 -0
- package/bin/_read-json.mjs +28 -0
- package/bin/incanto-assets.mjs +5 -4
- package/bin/incanto-check.mjs +58 -31
- 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 +105 -10
- package/bin/incanto-play.mjs +158 -16
- package/bin/incanto-playtest.mjs +66 -23
- package/bin/incanto-skills.mjs +14 -2
- package/bin/incanto-verify.mjs +121 -102
- package/bin/incanto.mjs +3 -2
- package/dist/2d.d.ts +296 -34
- package/dist/2d.js +4 -4
- package/dist/3d.d.ts +157 -9
- package/dist/3d.js +8 -8
- package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
- package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
- package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
- package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
- package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
- 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 +1088 -882
- package/dist/env.d.ts +1 -1
- package/dist/env.js +5 -3
- package/dist/{environment-presets-BO-7XoFV.js → environment-presets-99BzP_L-.js} +298 -45
- package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
- package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
- package/dist/gameplay.d.ts +254 -10
- package/dist/gameplay.js +2 -2
- package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
- package/dist/index.d.ts +179 -13
- package/dist/index.js +10 -12
- package/dist/json-CfTjpvW8.js +67 -0
- package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.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-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
- package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
- package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
- package/dist/react.d.ts +2 -2
- package/dist/react.js +2 -2
- package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
- package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
- package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
- package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.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-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
- package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
- package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
- package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
- package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
- package/dist/test-CmN54kWv.js +4264 -0
- package/dist/test.d.ts +639 -30
- package/dist/test.js +3 -3
- package/dist/touch-BnCyPA0G.js +519 -0
- package/dist/vite.d.ts +20 -37
- package/dist/vite.js +177 -7
- package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
- package/editor/assets/agent8-yLIEYHbd.js +1 -0
- package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
- package/editor/assets/index-DK9xMGpW.js +11046 -0
- package/editor/index.html +1 -1
- package/package.json +4 -14
- package/schemas/scene.schema.json +34 -3
- package/skills/incanto-3d-character.md +14 -1
- package/skills/incanto-3d-models.md +12 -0
- package/skills/incanto-assets.md +9 -1
- package/skills/incanto-audio.md +8 -2
- package/skills/incanto-behaviors-and-scripts.md +105 -7
- package/skills/incanto-building-2d-games.md +97 -10
- package/skills/incanto-building-3d-games.md +58 -4
- 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 +172 -20
- package/skills/incanto-hud.md +142 -7
- package/skills/incanto-localization.md +13 -5
- package/skills/incanto-multiplayer.md +83 -3
- package/skills/incanto-node-reference.md +239 -59
- package/skills/incanto-performance.md +52 -0
- package/skills/incanto-physics-and-input.md +123 -24
- package/skills/incanto-playtesting.md +112 -2
- package/skills/incanto-save-slots.md +217 -7
- package/skills/incanto-scene-json-authoring.md +69 -12
- package/skills/incanto-verifying-your-game.md +207 -10
- package/skills/incanto-web-integration.md +28 -0
- package/skills/incanto-your-first-game.md +3 -1
- 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 +143 -600
- package/templates-app/beacon-isle-3d/src/main.ts +11 -1
- 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 +1205 -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/package.json +2 -2
- package/templates-app/platformer-2d/src/game.scene.json +178 -986
- package/templates-app/platformer-2d/src/main.ts +11 -1
- 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 +43 -216
- package/templates-app/star-survivor/src/main.ts +11 -1
- 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 +66 -263
- package/templates-app/tps-3d/src/main.ts +13 -2
- 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 +42 -4
- package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
- package/templates-app/village-quest-3d/src/main.ts +11 -1
- package/templates-app/village-quest-3d/src/village.scene.json +202 -863
- package/templates-app/village-quest-3d/tsconfig.json +1 -1
- package/templates-app/village-quest-3d/verify.ts +14 -1
- package/dist/duplicate-Dj4L-5AA.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-D6ISmuFE.js +0 -2383
- package/dist/touch-DEAmqGdf.js +0 -225
- package/editor/assets/agent8-BDvaZMQJ.js +0 -1
- package/editor/assets/index-CZIRr5EL.js +0 -10958
|
@@ -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);
|
|
@@ -76,7 +77,7 @@ if (command === 'list') {
|
|
|
76
77
|
if (command === 'info') {
|
|
77
78
|
console.log(JSON.stringify(entry, null, 2));
|
|
78
79
|
if (entry.animation) {
|
|
79
|
-
const meta =
|
|
80
|
+
const meta = readJson(join(ASSETS, entry.animation));
|
|
80
81
|
console.log('\nanimations:', Object.keys(meta.animations ?? {}).join(', '));
|
|
81
82
|
}
|
|
82
83
|
process.exitCode = 0; // see the list note — exit() truncates piped stdout
|
|
@@ -98,7 +99,7 @@ if (command === 'list') {
|
|
|
98
99
|
const { spriteFromLibraryMeta } = await import(
|
|
99
100
|
pathToFileURL(join(PKG, 'dist', '2d.js')).href
|
|
100
101
|
);
|
|
101
|
-
const meta =
|
|
102
|
+
const meta = readJson(join(ASSETS, entry.animation));
|
|
102
103
|
const { asset, props } = spriteFromLibraryMeta(meta, { url, assetKey: entry.name });
|
|
103
104
|
console.log(`\nscene assets entry:\n "${entry.name}": ${JSON.stringify(asset)}`);
|
|
104
105
|
console.log(
|
package/bin/incanto-check.mjs
CHANGED
|
@@ -14,11 +14,12 @@
|
|
|
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 { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
21
|
+
import { behaviorFiles, behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
22
|
+
import { readJson } from './_read-json.mjs';
|
|
22
23
|
|
|
23
24
|
const DIST = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist');
|
|
24
25
|
|
|
@@ -38,7 +39,8 @@ function parseArgs(argv) {
|
|
|
38
39
|
const a = argv[i];
|
|
39
40
|
if (a === '--json') args.json = true;
|
|
40
41
|
else if (a === '--strict-behaviors') args.strict = true;
|
|
41
|
-
|
|
42
|
+
// Repeatable: behaviours legitimately live in more than one file.
|
|
43
|
+
else if (a === '--behaviors') args.behaviors = [...(args.behaviors ?? []), argv[++i]];
|
|
42
44
|
else if (a === '--strict-warnings') args.strictWarnings = true;
|
|
43
45
|
else if (a === '--help' || a === '-h') args.help = true;
|
|
44
46
|
else if (a.startsWith('--')) {
|
|
@@ -51,7 +53,7 @@ function parseArgs(argv) {
|
|
|
51
53
|
|
|
52
54
|
const args = parseArgs(process.argv.slice(2));
|
|
53
55
|
if (args.help || args.invalid) {
|
|
54
|
-
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--behaviors FILE] [--strict-behaviors] [--strict-warnings]
|
|
56
|
+
console.log(`Usage: npx incanto check [files|dirs...] [--json] [--behaviors FILE[,FILE...]] [--strict-behaviors] [--strict-warnings]
|
|
55
57
|
|
|
56
58
|
Validates *.scene.json files headlessly with the installed engine.
|
|
57
59
|
With no arguments, scans the current directory recursively
|
|
@@ -86,12 +88,31 @@ function collectScenes(entry) {
|
|
|
86
88
|
*/
|
|
87
89
|
function missingArt(file, json) {
|
|
88
90
|
const assets = json && typeof json === 'object' ? json.assets : null;
|
|
89
|
-
if (!assets || typeof assets !== 'object') return [];
|
|
90
91
|
const sceneDir = dirname(file);
|
|
91
92
|
const root = projectRoot(sceneDir);
|
|
92
93
|
const out = [];
|
|
93
|
-
|
|
94
|
-
|
|
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) {
|
|
95
116
|
if (typeof url !== 'string' || url === '') continue;
|
|
96
117
|
// Remote and inline are not this command's business to guess about.
|
|
97
118
|
if (/^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//')) continue;
|
|
@@ -113,9 +134,9 @@ function missingArt(file, json) {
|
|
|
113
134
|
// warned about art that works.
|
|
114
135
|
if (resolvesAsPackageAsset(url, sceneDir)) continue;
|
|
115
136
|
out.push(
|
|
116
|
-
|
|
137
|
+
`${what} → ${url} is not in the project ` +
|
|
117
138
|
`(looked in ${relativeish(root, join(root, 'public'))}, ${relativeish(root, root)}` +
|
|
118
|
-
`, and beside the scene). It will draw nothing.`,
|
|
139
|
+
`, and beside the scene). It will ${kind === 'audio' ? 'be silent' : 'draw nothing'}.`,
|
|
119
140
|
);
|
|
120
141
|
}
|
|
121
142
|
return out;
|
|
@@ -168,31 +189,37 @@ if (files.length === 0) {
|
|
|
168
189
|
process.exit(1);
|
|
169
190
|
}
|
|
170
191
|
|
|
171
|
-
const { auditScene, instancedScenes, validateScene } =
|
|
172
|
-
pathToFileURL(join(DIST, 'test.js')).href
|
|
173
|
-
|
|
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();
|
|
174
196
|
|
|
175
197
|
/** Your Behavior subclasses, when you name a file — same contract as the other bins. */
|
|
176
198
|
const behaviors = {};
|
|
177
|
-
|
|
199
|
+
const behaviorSources = behaviorFiles(args.behaviors);
|
|
200
|
+
if (behaviorSources.length > 0) {
|
|
178
201
|
const { Behavior } = await import(pathToFileURL(join(DIST, 'index.js')).href);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
}
|
|
193
220
|
}
|
|
194
221
|
if (Object.keys(behaviors).length === 0) {
|
|
195
|
-
console.error(`warning: no Behavior subclasses exported from ${
|
|
222
|
+
console.error(`warning: no Behavior subclasses exported from ${behaviorSources.join(', ')}`);
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
225
|
|
|
@@ -223,7 +250,7 @@ const declaredAssets = {};
|
|
|
223
250
|
const corpus = [];
|
|
224
251
|
for (const file of files) {
|
|
225
252
|
try {
|
|
226
|
-
const doc =
|
|
253
|
+
const doc = readJson(file);
|
|
227
254
|
corpus.push({ path: resolve(file), json: doc });
|
|
228
255
|
for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
|
|
229
256
|
if (typeof entries !== 'object' || entries === null) continue;
|
|
@@ -242,7 +269,7 @@ const instanced = instancedScenes(corpus, (from, ref) => resolve(dirname(from),
|
|
|
242
269
|
const results = files.map((file) => {
|
|
243
270
|
let json;
|
|
244
271
|
try {
|
|
245
|
-
json =
|
|
272
|
+
json = readJson(file);
|
|
246
273
|
} catch (e) {
|
|
247
274
|
return { file, ok: false, code: 'BAD_JSON', message: String(e?.message ?? e) };
|
|
248
275
|
}
|
|
@@ -262,7 +289,7 @@ const results = files.map((file) => {
|
|
|
262
289
|
*/
|
|
263
290
|
behaviors,
|
|
264
291
|
declaredAssets,
|
|
265
|
-
resolveScene: (p) =>
|
|
292
|
+
resolveScene: (p) => readJson(resolve(dirname(file), p)),
|
|
266
293
|
});
|
|
267
294
|
if (res.ok) {
|
|
268
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 {
|
package/bin/incanto-frame.mjs
CHANGED
|
@@ -75,10 +75,17 @@ running game before capturing — the same words incanto-play takes:
|
|
|
75
75
|
|
|
76
76
|
incanto-frame --do "vector move 0 1; step 3000; vector move 0 0" --out bridge.png
|
|
77
77
|
incanto-frame --do "press jump; step 400" --out midair.png
|
|
78
|
+
incanto-frame --do "at %Coin; click; step 300" --out collected.png
|
|
78
79
|
|
|
79
80
|
press/release ACTION · vector ACTION X Y · key CODE down|up
|
|
81
|
+
at X Y | at NODE-PATH · click [left|middle|right] · mouse BUTTON down|up
|
|
80
82
|
pointer DX DY · step MS (semicolons or newlines separate)
|
|
81
83
|
|
|
84
|
+
'at' puts the CURSOR somewhere — a canvas pixel, or wherever a node is on
|
|
85
|
+
screen — and 'click' presses and releases there. That is the whole input of a
|
|
86
|
+
match-3, a tower defense, a card game or a point-and-click, and this language
|
|
87
|
+
could not say it: 'pointer' is a delta, which is the mouse-LOOK question.
|
|
88
|
+
|
|
82
89
|
step waits on the real clock: this is the live game, with its own loop.
|
|
83
90
|
|
|
84
91
|
|
|
@@ -140,7 +147,7 @@ if (driveScript) {
|
|
|
140
147
|
// Refuse here rather than in the page: a bad command that ran nothing would
|
|
141
148
|
// hand back the boot screen, and nothing about that frame would say so.
|
|
142
149
|
console.error(
|
|
143
|
-
`${parsed.error}\n commands: press/release ACTION · vector ACTION X Y · key CODE down|up · pointer DX DY · step MS`,
|
|
150
|
+
`${parsed.error}\n commands: press/release ACTION · vector ACTION X Y · key CODE down|up · at X Y | at NODE-PATH · click [left|middle|right] · mouse BUTTON down|up · pointer DX DY · step MS`,
|
|
144
151
|
);
|
|
145
152
|
process.exit(1);
|
|
146
153
|
}
|