incanto 0.65.0 → 0.67.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 +87 -0
- package/bin/incanto-editor.mjs +39 -2
- package/bin/incanto-feel.mjs +4 -1
- package/bin/incanto-frame.mjs +52 -3
- package/bin/incanto-logs.mjs +33 -3
- package/bin/incanto-multiplay.mjs +50 -1
- package/bin/incanto-play.mjs +3 -1
- package/bin/incanto-playtest.mjs +3 -25
- package/bin/incanto-verify.mjs +15 -2
- package/dist/2d.d.ts +76 -4
- package/dist/2d.js +3 -3
- package/dist/3d.d.ts +213 -14
- package/dist/3d.js +6 -6
- package/dist/{audio-player-BrEHwbK1.d.ts → audio-player-BNUHo0JF.d.ts} +1 -1
- package/dist/{behavior-CmMuQ6CL.d.ts → behavior-ZnfzKDo3.d.ts} +58 -1
- package/dist/{create-game-DIyh5Zy9.js → create-game-DMetd3vR.js} +14 -8
- package/dist/{create-game-W2xzTJhs.js → create-game-FAXdHta3.js} +138 -11
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +3 -2
- package/dist/{duplicate-CqSAtdrh.js → duplicate-B8QnX2-r.js} +1 -1
- package/dist/editor.js +7 -2
- package/dist/{environment-presets-BvHs6HEk.js → environment-presets-C-5g2pMh.js} +113 -9
- package/dist/{gameplay-D-H986A4.js → gameplay-CGSv-9XR.js} +295 -12
- package/dist/gameplay.d.ts +102 -6
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +50 -6
- package/dist/index.js +7 -6
- package/dist/{loader-CTB441z4.d.ts → loader-DyT30ArB.d.ts} +1 -1
- package/dist/{loader-lQDCwNag.js → loader-KZESOZmE.js} +148 -1
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-CmnunaR2.js → physics-2d-_B6zj3zU.js} +57 -4
- package/dist/{physics-3d-DULB1lD1.js → physics-3d-i1IeAY0F.js} +74 -5
- package/dist/{quiet-rapier-BAJ4K94N.js → quiet-rapier-C6fW4zcW.js} +14 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +1 -1
- package/dist/{register-CgeR4ckF.js → register-Cs53CY7s.js} +110 -16
- package/dist/{register-DO5_Qp0Q.js → register-vIxU3Xbv.js} +38 -4
- package/dist/rendering-options-ByAdi8PF.js +184 -0
- package/dist/{replay-CEw2xAhG.js → replay-BkpDV51O.js} +4 -3
- package/dist/{replay-CThvwkcQ.d.ts → replay-D0GcEHYU.d.ts} +1 -1
- package/dist/{split-screen-DxZzC1nm.d.ts → split-screen-DD5eYIgW.d.ts} +2 -2
- package/dist/{split-screen-gb0c3xdb.js → split-screen-DS8RaUV_.js} +2 -2
- package/dist/{src-DYffzHbd.js → src-BLlsGJW9.js} +1 -1
- package/dist/{teardown-BordHL9o.js → teardown-LB9cTnXJ.js} +2 -2
- package/dist/{test-CXZuXs8k.js → test-81uZ967a.js} +127 -24
- package/dist/test.d.ts +7 -5
- package/dist/test.js +2 -2
- package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
- package/dist/vite.d.ts +0 -7
- package/dist/vite.js +13 -3
- package/editor/assets/{agent8-DO5qzIOD.js → agent8-C2jtNKbx.js} +1 -1
- package/editor/assets/debug-DoU3KeqS.js +3 -0
- package/editor/assets/{index-CqEUVaPN.js → index-CNqHojcC.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +20 -1
- package/skills/incanto-building-3d-games.md +19 -0
- package/skills/incanto-editor.md +5 -1
- package/skills/incanto-environment.md +2 -2
- package/skills/incanto-gameplay-behaviors.md +86 -7
- package/skills/incanto-hud.md +14 -1
- package/skills/incanto-multiplayer.md +14 -0
- package/skills/incanto-node-reference.md +60 -2
- package/skills/incanto-performance.md +35 -0
- package/skills/incanto-physics-and-input.md +69 -0
- package/skills/incanto-playtesting.md +15 -1
- package/skills/incanto-save-slots.md +21 -1
- package/skills/incanto-verifying-your-game.md +19 -0
- package/skills/incanto-web-integration.md +24 -3
- package/skills/incanto-your-first-game.md +4 -4
- package/templates-app/beacon-isle-3d/package.json +1 -1
- package/templates-app/beacon-isle-3d/src/game.scene.json +38 -19
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +99 -53
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/game.scene.json +31 -17
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +14 -8
- package/templates-app/village-quest-3d/package.json +1 -1
- package/templates-app/village-quest-3d/src/grove.scene.json +20 -11
- package/templates-app/village-quest-3d/src/village.scene.json +10 -7
- package/editor/assets/debug-DntG9CVD.js +0 -3
- package/templates-app/beacon-isle-3d/coverage.json +0 -9
- package/templates-app/platformer-2d/coverage.json +0 -5
- package/templates-app/star-survivor/coverage.json +0 -5
- package/templates-app/tps-3d/coverage.json +0 -5
- package/templates-app/village-quest-3d/coverage.json +0 -9
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load a `--behaviors` file the way the author WROTE it, from any bin.
|
|
3
|
+
*
|
|
4
|
+
* Every template's `behaviors.ts` does `import gameJson from './game.scene.json'`
|
|
5
|
+
* — the documented way to read your own scene — and node's type stripping
|
|
6
|
+
* refuses a JSON import without `with { type: 'json' }`. Same for an
|
|
7
|
+
* extensionless relative import (`from './quest'`), which is what the tsconfig
|
|
8
|
+
* this package scaffolds asks for. Vite reads both. bun reads both. The bins'
|
|
9
|
+
* shebang is node.
|
|
10
|
+
*
|
|
11
|
+
* `incanto-playtest` grew a bun re-exec for exactly this and **the other four
|
|
12
|
+
* bins that take `--behaviors` never got it**: `feel`, `play`, `verify` and
|
|
13
|
+
* `multiplay` all died on the shipped `village-quest-3d`, and the only
|
|
14
|
+
* invocation the docs give (`incanto-verifying-your-game.md`) is the failing
|
|
15
|
+
* form. Worse, `verify:templates` — the gate step added specifically to run a
|
|
16
|
+
* template under node's resolver — runs `incanto-playtest`, the one bin whose
|
|
17
|
+
* fallback hides the failure. The step built to catch this class was
|
|
18
|
+
* neutralised by the thing it was built to catch.
|
|
19
|
+
*
|
|
20
|
+
* So the fallback lives here, once, and every bin uses it. Set
|
|
21
|
+
* `INCANTO_NO_BUN_REEXEC=1` to forbid it — that is how the gate exercises the
|
|
22
|
+
* resolver a user actually gets.
|
|
23
|
+
*/
|
|
24
|
+
import { spawnSync } from 'node:child_process';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Is this node refusing the file, rather than the file being wrong?
|
|
28
|
+
*
|
|
29
|
+
* Matched against the SPECIFIER where node gives us one. Node words the
|
|
30
|
+
* extensionless failure as `Cannot find module '<resolved path>' imported from
|
|
31
|
+
* '<importer>'`, and a check that reads the importer collapses it into
|
|
32
|
+
* "(file not found)" naming the file that plainly exists — which is what the
|
|
33
|
+
* author sees and cannot act on.
|
|
34
|
+
*/
|
|
35
|
+
export function nodeCannotReadIt(why) {
|
|
36
|
+
return (
|
|
37
|
+
/import attribute|Unknown file extension|Cannot find module|ERR_MODULE_NOT_FOUND|ERR_UNSUPPORTED_[A-Z_]+/i.test(
|
|
38
|
+
String(why),
|
|
39
|
+
) ||
|
|
40
|
+
// Node's type stripping ERASES types; it does not transform. Anything that
|
|
41
|
+
// needs a transform is refused by name, and the wording is nothing like the
|
|
42
|
+
// resolver's. Measured on the shipped `racing-3d` example:
|
|
43
|
+
//
|
|
44
|
+
// x TypeScript parameter property is not supported in strip-only mode
|
|
45
|
+
//
|
|
46
|
+
// `constructor(private root: Node3D)` is ordinary TypeScript that vite and
|
|
47
|
+
// bun both compile, and the bin gave up with "Pass the file that exports
|
|
48
|
+
// your Behavior subclasses" — telling the author their correct file was
|
|
49
|
+
// wrong. Enums and namespaces are refused the same way.
|
|
50
|
+
/strip-only mode|is not supported in strip|only mode|TypeScript parameter property|Unsupported TypeScript/i.test(
|
|
51
|
+
String(why),
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Re-run this bin under bun, once, if node is the reason it failed.
|
|
58
|
+
*
|
|
59
|
+
* Never returns when it re-execs. Returns false when it did not (already bun,
|
|
60
|
+
* already re-execed, forbidden, no bun on PATH, or a real error in the file).
|
|
61
|
+
*/
|
|
62
|
+
export function reexecUnderBun(entryUrl, why) {
|
|
63
|
+
if (!nodeCannotReadIt(why)) return false;
|
|
64
|
+
if (process.versions.bun) return false;
|
|
65
|
+
if (process.env.INCANTO_BUN_REEXEC || process.env.INCANTO_NO_BUN_REEXEC) return false;
|
|
66
|
+
if (spawnSync('bun', ['--version'], { stdio: 'ignore' }).status !== 0) return false;
|
|
67
|
+
const here = new URL(entryUrl).pathname;
|
|
68
|
+
const again = spawnSync('bun', [here, ...process.argv.slice(2)], {
|
|
69
|
+
stdio: 'inherit',
|
|
70
|
+
env: { ...process.env, INCANTO_BUN_REEXEC: '1' },
|
|
71
|
+
});
|
|
72
|
+
process.exit(again.status ?? 1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The sentence to print when the re-exec was not available. */
|
|
76
|
+
export function behaviorsHelp(file, why) {
|
|
77
|
+
return (
|
|
78
|
+
`could not load --behaviors '${file}' (${why}).` +
|
|
79
|
+
(nodeCannotReadIt(why)
|
|
80
|
+
? '\n\nThis is usually node refusing your file rather than a problem with it:' +
|
|
81
|
+
"\nnode cannot import JSON from a .ts module without `with { type: 'json' }`," +
|
|
82
|
+
'\nand it will not resolve an extensionless relative import. Both are what' +
|
|
83
|
+
'\nthe scaffolded tsconfig asks you to write. Run it with bun, which reads' +
|
|
84
|
+
'\nthe file as written — prefix any incanto command with `bunx --bun`.'
|
|
85
|
+
: '\nPass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.')
|
|
86
|
+
);
|
|
87
|
+
}
|
package/bin/incanto-editor.mjs
CHANGED
|
@@ -200,6 +200,29 @@ async function proxyLibrary(req, res, url) {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* What kind of project is this — read from the scenes already in it.
|
|
205
|
+
*
|
|
206
|
+
* A 3D game's new scene must not be born 2D, and the only thing that knows
|
|
207
|
+
* which is which is what is already on disk. Returns null for an empty project,
|
|
208
|
+
* where there is genuinely nothing to go on.
|
|
209
|
+
*/
|
|
210
|
+
function projectDimension() {
|
|
211
|
+
let d2 = 0;
|
|
212
|
+
let d3 = 0;
|
|
213
|
+
for (const entry of discoverScenes()) {
|
|
214
|
+
try {
|
|
215
|
+
const json = JSON.parse(readFileSync(join(ROOT, entry.rel ?? entry), 'utf8'));
|
|
216
|
+
if (json?.dimension === '3d') d3 += 1;
|
|
217
|
+
else if (json?.dimension === '2d') d2 += 1;
|
|
218
|
+
} catch {
|
|
219
|
+
// not a scene, or unreadable — the scene browser already says so
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (d3 === 0 && d2 === 0) return null;
|
|
223
|
+
return d3 > d2 ? '3d' : '2d';
|
|
224
|
+
}
|
|
225
|
+
|
|
203
226
|
function discoverScenes() {
|
|
204
227
|
const found = [];
|
|
205
228
|
const walk = (dir, depth) => {
|
|
@@ -418,12 +441,26 @@ const server = createServer((req, res) => {
|
|
|
418
441
|
return send(res, 409, JSON.stringify({ error: `already exists: ${wanted}` }));
|
|
419
442
|
}
|
|
420
443
|
const name = basename(abs).replace(/\.scene\.json$|\.json$/, '') || 'Scene';
|
|
444
|
+
/*
|
|
445
|
+
* A 3D project's new scene must not be born 2D.
|
|
446
|
+
*
|
|
447
|
+
* This hardcoded `dimension: '2d'` / `Node2D` and ignored `body`
|
|
448
|
+
* entirely, so every scene the editor created in a 3D game was the
|
|
449
|
+
* wrong kind — and node types are IMMUTABLE by design, so the halfway
|
|
450
|
+
* state (a 3D header over a Node2D root) is easy to reach through the
|
|
451
|
+
* header's dimension select and is written back with a 200.
|
|
452
|
+
*
|
|
453
|
+
* Asked for explicitly, or guessed from the project: whatever the
|
|
454
|
+
* sibling scenes are is overwhelmingly what the next one should be.
|
|
455
|
+
*/
|
|
456
|
+
const asked = body?.dimension === '3d' || body?.dimension === '2d' ? body.dimension : null;
|
|
457
|
+
const dimension = asked ?? projectDimension() ?? '2d';
|
|
421
458
|
const scene = {
|
|
422
459
|
format: 1,
|
|
423
460
|
type: 'scene',
|
|
424
|
-
dimension
|
|
461
|
+
dimension,
|
|
425
462
|
name,
|
|
426
|
-
root: { name, type: 'Node2D' },
|
|
463
|
+
root: { name, type: dimension === '3d' ? 'Node3D' : 'Node2D' },
|
|
427
464
|
};
|
|
428
465
|
try {
|
|
429
466
|
mkdirSync(dirname(abs), { recursive: true });
|
package/bin/incanto-feel.mjs
CHANGED
|
@@ -21,6 +21,7 @@ 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
25
|
|
|
25
26
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
|
|
@@ -63,7 +64,9 @@ if (args.behaviors) {
|
|
|
63
64
|
try {
|
|
64
65
|
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
65
66
|
} catch (e) {
|
|
66
|
-
|
|
67
|
+
const why = e?.message ?? String(e);
|
|
68
|
+
reexecUnderBun(import.meta.url, why);
|
|
69
|
+
console.error(behaviorsHelp(args.behaviors, why));
|
|
67
70
|
process.exit(1);
|
|
68
71
|
}
|
|
69
72
|
for (const [name, value] of Object.entries(mod)) {
|
package/bin/incanto-frame.mjs
CHANGED
|
@@ -150,11 +150,33 @@ const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
|
150
150
|
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
151
151
|
|
|
152
152
|
let found = null;
|
|
153
|
+
/**
|
|
154
|
+
* WHOSE dev server answered.
|
|
155
|
+
*
|
|
156
|
+
* This scans a port range, so before the ping carried identity, whoever
|
|
157
|
+
* answered first won — and a passing number from ANOTHER project silently
|
|
158
|
+
* overrode this one's failing one. Measured: project A on :5188 with a real
|
|
159
|
+
* page reported "the subject fills almost nothing (0.3%)", while `incanto
|
|
160
|
+
* verify` scanning from the same directory reported `✓ draws — 37.5%`,
|
|
161
|
+
* measured on project B. Corroboration from the other direction: four scenes
|
|
162
|
+
* across two unrelated projects all reported 22.6%.
|
|
163
|
+
*
|
|
164
|
+
* So: prefer the server rooted at THIS directory. If several answer and none is
|
|
165
|
+
* ours, refuse and list them — the same refusal `verify` already makes when a
|
|
166
|
+
* project has several scenes and guessing would verify the wrong one.
|
|
167
|
+
*/
|
|
168
|
+
const answered = [];
|
|
153
169
|
// BOTH hosts, deliberately: vite binds to ::1 on some machines and 127.0.0.1 on
|
|
154
170
|
// others, and a probe of the wrong one reports "no dev server" while the page
|
|
155
171
|
// is plainly on screen.
|
|
156
172
|
const HOSTS = ['localhost', '127.0.0.1'];
|
|
157
|
-
|
|
173
|
+
const CWD = process.cwd();
|
|
174
|
+
const sameProject = (b) => {
|
|
175
|
+
if (!b?.root) return false;
|
|
176
|
+
const r = String(b.root);
|
|
177
|
+
return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
|
|
178
|
+
};
|
|
179
|
+
for (const port of ports) {
|
|
158
180
|
for (const host of HOSTS) {
|
|
159
181
|
try {
|
|
160
182
|
const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
|
|
@@ -163,8 +185,8 @@ outer: for (const port of ports) {
|
|
|
163
185
|
if (!res.ok) continue;
|
|
164
186
|
const body = await res.json();
|
|
165
187
|
if (body?.incanto) {
|
|
166
|
-
|
|
167
|
-
break
|
|
188
|
+
answered.push({ host, port, ...body });
|
|
189
|
+
break; // one host per port is enough
|
|
168
190
|
}
|
|
169
191
|
} catch {
|
|
170
192
|
// not listening, not ours, or not answering — either way, next
|
|
@@ -172,6 +194,33 @@ outer: for (const port of ports) {
|
|
|
172
194
|
}
|
|
173
195
|
}
|
|
174
196
|
|
|
197
|
+
if (explicit) {
|
|
198
|
+
found = answered[0] ?? null;
|
|
199
|
+
} else {
|
|
200
|
+
const mine = answered.filter(sameProject);
|
|
201
|
+
if (mine.length > 0) {
|
|
202
|
+
found = mine[0];
|
|
203
|
+
} else if (answered.length === 1 && answered[0].root === undefined) {
|
|
204
|
+
// An older dev server that predates the identity field. Take it, but say so.
|
|
205
|
+
found = answered[0];
|
|
206
|
+
console.error(
|
|
207
|
+
`· the dev server on :${found.port} does not report which project it is ` +
|
|
208
|
+
'(older incanto) — if this reads the wrong game, pass --port.',
|
|
209
|
+
);
|
|
210
|
+
} else if (answered.length > 0) {
|
|
211
|
+
console.error(
|
|
212
|
+
`${answered.length} incanto dev server(s) are running and NONE of them is this ` +
|
|
213
|
+
`directory (${CWD}):\n` +
|
|
214
|
+
answered
|
|
215
|
+
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
|
|
216
|
+
.join('\n') +
|
|
217
|
+
"\n\nMeasuring one project on another project's page reports a number that has\n" +
|
|
218
|
+
"nothing to do with this game. Start this project's dev server, or pass --port.",
|
|
219
|
+
);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
175
224
|
if (!found) {
|
|
176
225
|
console.error(
|
|
177
226
|
'no incanto dev server found.\n' +
|
package/bin/incanto-logs.mjs
CHANGED
|
@@ -75,7 +75,17 @@ const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
|
75
75
|
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
76
76
|
|
|
77
77
|
let found = null;
|
|
78
|
-
|
|
78
|
+
// Whose dev server answered — see incanto-frame.mjs for the measurement that
|
|
79
|
+
// made this necessary: a scan takes whoever replies first, and a foreign
|
|
80
|
+
// project's page answered both the frame and the logs request.
|
|
81
|
+
const answered = [];
|
|
82
|
+
const CWD = process.cwd();
|
|
83
|
+
const sameProject = (b) => {
|
|
84
|
+
if (!b?.root) return false;
|
|
85
|
+
const r = String(b.root);
|
|
86
|
+
return r === CWD || CWD.startsWith(`${r}/`) || r.startsWith(`${CWD}/`);
|
|
87
|
+
};
|
|
88
|
+
for (const port of ports) {
|
|
79
89
|
for (const host of ['localhost', '127.0.0.1']) {
|
|
80
90
|
try {
|
|
81
91
|
const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
|
|
@@ -84,14 +94,34 @@ outer: for (const port of ports) {
|
|
|
84
94
|
if (!res.ok) continue;
|
|
85
95
|
const body = await res.json();
|
|
86
96
|
if (body?.incanto) {
|
|
87
|
-
|
|
88
|
-
break
|
|
97
|
+
answered.push({ host, port, ...body });
|
|
98
|
+
break;
|
|
89
99
|
}
|
|
90
100
|
} catch {
|
|
91
101
|
// not listening, not ours, or not answering — next
|
|
92
102
|
}
|
|
93
103
|
}
|
|
94
104
|
}
|
|
105
|
+
if (explicit) {
|
|
106
|
+
found = answered[0] ? { host: answered[0].host, port: answered[0].port } : null;
|
|
107
|
+
} else {
|
|
108
|
+
const mine = answered.filter(sameProject);
|
|
109
|
+
const pick =
|
|
110
|
+
mine[0] ?? (answered.length === 1 && answered[0].root === undefined ? answered[0] : null);
|
|
111
|
+
if (pick) found = { host: pick.host, port: pick.port };
|
|
112
|
+
else if (answered.length > 0) {
|
|
113
|
+
console.error(
|
|
114
|
+
`${answered.length} incanto dev server(s) are running and none is this directory ` +
|
|
115
|
+
`(${CWD}):\n` +
|
|
116
|
+
answered
|
|
117
|
+
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? '?'}`)
|
|
118
|
+
.join('\n') +
|
|
119
|
+
"\n\nReading another project's log buffer would report its errors as yours.\n" +
|
|
120
|
+
"Start this project's dev server, or pass --port.",
|
|
121
|
+
);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
95
125
|
|
|
96
126
|
if (!found) {
|
|
97
127
|
console.error(
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { existsSync, readFileSync } from 'node:fs';
|
|
21
21
|
import { dirname, join, resolve } from 'node:path';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
23
24
|
|
|
24
25
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
26
|
|
|
@@ -36,6 +37,7 @@ if (argv.includes('--help') || argv.includes('-h')) {
|
|
|
36
37
|
--seconds N simulated seconds (default 5)
|
|
37
38
|
--seed N make the match reproducible (default 1)
|
|
38
39
|
--behaviors FILE your Behavior subclasses (.ts works on bun / node >= 23.6)
|
|
40
|
+
--server FILE your Server class (default: server/src/server.ts if present)
|
|
39
41
|
--scenes a=b.json a scene a NetworkSpawner names, repeatable
|
|
40
42
|
--no-late-join skip the late-joining client
|
|
41
43
|
--json machine-readable report
|
|
@@ -83,7 +85,9 @@ if (behaviorsFile) {
|
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
} catch (e) {
|
|
86
|
-
|
|
88
|
+
const why = e?.message ?? String(e);
|
|
89
|
+
reexecUnderBun(import.meta.url, why);
|
|
90
|
+
console.error(behaviorsHelp(behaviorsFile, why));
|
|
87
91
|
process.exit(1);
|
|
88
92
|
}
|
|
89
93
|
}
|
|
@@ -122,11 +126,56 @@ for (const name of spawnerScenes) {
|
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Your REAL server class, so `agrees` measures the game you ship.
|
|
131
|
+
*
|
|
132
|
+
* `playMultiplayer` has always accepted a `server`, and this bin never passed
|
|
133
|
+
* one — so the rung ran every match against a bare loopback kernel with no
|
|
134
|
+
* remote functions. Measured: with no server `roomState` came back `{}`, with
|
|
135
|
+
* one it came back `{"matchMs": 5983.3}`; a `manager.call('claimRelic')` threw
|
|
136
|
+
* `BAD_FORMAT: Loopback kernel has no remote function 'claimRelic'` and the
|
|
137
|
+
* report STILL said `ok=true errors=0 problems=[]`. Moving `server/src/server.ts`
|
|
138
|
+
* out of the tree changed nothing, which is the whole finding: a ✓ that could
|
|
139
|
+
* not have been a ✗.
|
|
140
|
+
*
|
|
141
|
+
* Found where the templates put it, or named with `--server`.
|
|
142
|
+
*/
|
|
143
|
+
let server;
|
|
144
|
+
const serverFlag = flag('--server', null);
|
|
145
|
+
const serverGuesses = serverFlag
|
|
146
|
+
? [resolve(serverFlag)]
|
|
147
|
+
: ['server/src/server.ts', 'server/src/server.js', 'src/server.ts'].map((g) =>
|
|
148
|
+
resolve(sceneDir, '..', '..', g),
|
|
149
|
+
);
|
|
150
|
+
for (const at of serverGuesses) {
|
|
151
|
+
if (!existsSync(at)) continue;
|
|
152
|
+
try {
|
|
153
|
+
const mod = await import(pathToFileURL(at).href);
|
|
154
|
+
server = mod.Server ?? mod.default;
|
|
155
|
+
if (server) {
|
|
156
|
+
if (!argv.includes('--json')) console.log(`· server: ${at} (${server.name || 'default'})`);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
} catch (e) {
|
|
160
|
+
// A server that cannot be loaded must not read as a game that has none.
|
|
161
|
+
console.error(
|
|
162
|
+
`could not load the server at '${at}' (${e?.message ?? e}). ` +
|
|
163
|
+
'The agreement check would then measure a game without its own rules; refusing.',
|
|
164
|
+
);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (serverFlag && !server) {
|
|
169
|
+
console.error(`--server '${serverFlag}' exports neither \`Server\` nor a default.`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
125
173
|
let report;
|
|
126
174
|
try {
|
|
127
175
|
report = await playMultiplayer({
|
|
128
176
|
scene: sceneJson,
|
|
129
177
|
scenes,
|
|
178
|
+
...(server ? { server } : {}),
|
|
130
179
|
players: Number(flag('--players', 2)) || 2,
|
|
131
180
|
seconds: Number(flag('--seconds', 5)) || 5,
|
|
132
181
|
seed: Number(flag('--seed', 1)) || 1,
|
package/bin/incanto-play.mjs
CHANGED
|
@@ -26,6 +26,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
26
26
|
*/
|
|
27
27
|
import { createInterface } from 'node:readline';
|
|
28
28
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
29
30
|
|
|
30
31
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
31
32
|
|
|
@@ -79,6 +80,7 @@ if (args.behaviors) {
|
|
|
79
80
|
try {
|
|
80
81
|
mod = await import(pathToFileURL(resolve(args.behaviors)).href);
|
|
81
82
|
} catch (e) {
|
|
83
|
+
reexecUnderBun(import.meta.url, e?.message ?? String(e));
|
|
82
84
|
const reason =
|
|
83
85
|
e?.code === 'ERR_MODULE_NOT_FOUND' && String(e.message).includes(resolve(args.behaviors))
|
|
84
86
|
? 'file not found'
|
|
@@ -88,7 +90,7 @@ if (args.behaviors) {
|
|
|
88
90
|
cmd: 'behaviors',
|
|
89
91
|
error: {
|
|
90
92
|
code: e?.code ?? 'BEHAVIORS_LOAD_FAILED',
|
|
91
|
-
message:
|
|
93
|
+
message: behaviorsHelp(args.behaviors, reason),
|
|
92
94
|
},
|
|
93
95
|
});
|
|
94
96
|
process.exit(1);
|
package/bin/incanto-playtest.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
23
23
|
* Exit code 1 when no run reached a win, so CI can gate on it.
|
|
24
24
|
*/
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
+
import { behaviorsHelp, reexecUnderBun } from './_behaviors-loader.mjs';
|
|
26
27
|
|
|
27
28
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
29
|
|
|
@@ -92,31 +93,8 @@ if (args.behaviors) {
|
|
|
92
93
|
// and vite resolve it. It broke `incanto verify` on the shipped
|
|
93
94
|
// `village-quest-3d` starter, and it breaks for any author who splits their
|
|
94
95
|
// behaviours across files — which is most of them, eventually.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
);
|
|
98
|
-
if (nodeCannotReadIt && !process.versions.bun && !process.env.INCANTO_BUN_REEXEC) {
|
|
99
|
-
const { spawnSync } = await import('node:child_process');
|
|
100
|
-
const hasBun = spawnSync('bun', ['--version'], { stdio: 'ignore' }).status === 0;
|
|
101
|
-
if (hasBun) {
|
|
102
|
-
const here = fileURLToPath(import.meta.url);
|
|
103
|
-
const again = spawnSync('bun', [here, ...process.argv.slice(2)], {
|
|
104
|
-
stdio: 'inherit',
|
|
105
|
-
env: { ...process.env, INCANTO_BUN_REEXEC: '1' },
|
|
106
|
-
});
|
|
107
|
-
process.exit(again.status ?? 1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
console.error(
|
|
111
|
-
`could not load --behaviors '${args.behaviors}' (${why}).` +
|
|
112
|
-
(nodeCannotReadIt
|
|
113
|
-
? '\n\nThis is usually node refusing your file rather than a problem with it:' +
|
|
114
|
-
"\nnode cannot import JSON from a .ts module without `with { type: 'json' }`," +
|
|
115
|
-
'\nand it will not resolve an extensionless relative import. Both are what' +
|
|
116
|
-
'\nthe scaffolded tsconfig asks you to write. Run it with bun, which reads' +
|
|
117
|
-
'\nthe file as written: `bunx incanto playtest <scene> --behaviors <file>`.'
|
|
118
|
-
: '\nPass the file that exports your Behavior subclasses, e.g. src/behaviors.ts.'),
|
|
119
|
-
);
|
|
96
|
+
reexecUnderBun(import.meta.url, why);
|
|
97
|
+
console.error(behaviorsHelp(args.behaviors, why));
|
|
120
98
|
process.exit(1);
|
|
121
99
|
}
|
|
122
100
|
for (const [name, value] of Object.entries(mod)) {
|
package/bin/incanto-verify.mjs
CHANGED
|
@@ -84,6 +84,19 @@ function findScene() {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const bin = (name) => join(PKG, 'bin', `incanto-${name}.mjs`);
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `--port`, passed straight through to the page-reading rungs.
|
|
90
|
+
*
|
|
91
|
+
* `draws` and `says` ask a dev server for the browser's answer, and discovery
|
|
92
|
+
* scans a port range — so with two projects running, this one measured the
|
|
93
|
+
* OTHER one's page and reported its pass. `incanto-frame`/`incanto-logs` now
|
|
94
|
+
* refuse a foreign server, and this makes the escape hatch they document
|
|
95
|
+
* reachable from the rung that actually runs them.
|
|
96
|
+
*/
|
|
97
|
+
const PORT = process.argv.includes('--port')
|
|
98
|
+
? ['--port', String(process.argv[process.argv.indexOf('--port') + 1])]
|
|
99
|
+
: [];
|
|
87
100
|
const run = (name, args) =>
|
|
88
101
|
spawnSync(process.execPath, [bin(name), ...args], {
|
|
89
102
|
encoding: 'utf-8',
|
|
@@ -424,7 +437,7 @@ if (rungs[0].status === 'pass') {
|
|
|
424
437
|
|
|
425
438
|
// ---- draws ---------------------------------------------------------------
|
|
426
439
|
{
|
|
427
|
-
const r = run('frame', ['--json']);
|
|
440
|
+
const r = run('frame', ['--json', ...PORT]);
|
|
428
441
|
const out = safeJson(r.stdout);
|
|
429
442
|
const report = out?.report;
|
|
430
443
|
if (!report) {
|
|
@@ -466,7 +479,7 @@ if (rungs[0].status === 'pass') {
|
|
|
466
479
|
*/
|
|
467
480
|
// ---- says ---------------------------------------------------------------
|
|
468
481
|
{
|
|
469
|
-
const r = run('logs', ['--json']);
|
|
482
|
+
const r = run('logs', ['--json', ...PORT]);
|
|
470
483
|
const report = safeJson(r.stdout);
|
|
471
484
|
if (!report) {
|
|
472
485
|
rungs.push({
|
package/dist/2d.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats } from "./behavior-
|
|
1
|
+
import { P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-ZnfzKDo3.js";
|
|
2
2
|
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
|
|
3
3
|
import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
|
|
4
4
|
import { r as FrameStats } from "./frame-report-DCnHFmto.js";
|
|
5
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
5
|
+
import { t as LoadSceneOptions } from "./loader-DyT30ArB.js";
|
|
6
6
|
import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
|
|
7
|
-
import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-
|
|
7
|
+
import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-BNUHo0JF.js";
|
|
8
8
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
9
9
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
10
10
|
|
|
@@ -157,6 +157,11 @@ declare class StaticBody2D extends PhysicsBody2D {
|
|
|
157
157
|
*/
|
|
158
158
|
declare class Area2D extends PhysicsBody2D {
|
|
159
159
|
static override readonly typeName: string;
|
|
160
|
+
/**
|
|
161
|
+
* Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
|
|
162
|
+
* See `Area3D.overlapping`. Empty in a scene with no physics world.
|
|
163
|
+
*/
|
|
164
|
+
overlapping(group?: string): PhysicsBody2D[];
|
|
160
165
|
}
|
|
161
166
|
/** Dynamic simulated body. */
|
|
162
167
|
declare class RigidBody2D extends PhysicsBody2D {
|
|
@@ -271,6 +276,21 @@ declare class Physics2D {
|
|
|
271
276
|
private scopedLines;
|
|
272
277
|
/** Create newly-arrived joints; tear down departed ones. */
|
|
273
278
|
private syncJoints;
|
|
279
|
+
/** Who is currently inside each sensor/body, maintained from the event drain. */
|
|
280
|
+
private readonly overlaps;
|
|
281
|
+
private trackOverlap;
|
|
282
|
+
/**
|
|
283
|
+
* Forget every overlap whose node has left the tree.
|
|
284
|
+
*
|
|
285
|
+
* The lazy drop inside `overlapping()` only cleans the SETS of a key someone
|
|
286
|
+
* asks about, and only when they ask. This clears the KEYS, which is where the
|
|
287
|
+
* dead nodes were pinned.
|
|
288
|
+
*/
|
|
289
|
+
/** Drop one node from the overlap map, both as a key and as a member. */
|
|
290
|
+
private forgetOverlaps;
|
|
291
|
+
private pruneOverlaps;
|
|
292
|
+
/** The bodies currently overlapping `node`. Freed nodes are dropped on read. */
|
|
293
|
+
overlapping(node: PhysicsBody2D): PhysicsBody2D[];
|
|
274
294
|
/**
|
|
275
295
|
* World-space raycast in PIXELS (y-down). `exclude` skips that body — a
|
|
276
296
|
* shooter probing from inside its own collider needs it. Sensors (Area2D)
|
|
@@ -376,6 +396,18 @@ interface CreateGame2DOptions {
|
|
|
376
396
|
createElement(tag: string): HTMLElement;
|
|
377
397
|
};
|
|
378
398
|
seed?: number;
|
|
399
|
+
/**
|
|
400
|
+
* Namespace this game's persisted settings — volume, language, quality tier.
|
|
401
|
+
*
|
|
402
|
+
* They are keyed `incanto:<namespace>:<key>` in the browser's storage and
|
|
403
|
+
* defaulted to `settings` for everyone, so two Incanto games on one domain
|
|
404
|
+
* shared one options screen. Measured: game A's player sets `renderScale
|
|
405
|
+
* 0.25, muted, quality low`; game B, whose source never mentions settings,
|
|
406
|
+
* boots at a 480x232 drawing buffer and silent.
|
|
407
|
+
*
|
|
408
|
+
* `SaveSlots(namespace)` already takes one for exactly this reason.
|
|
409
|
+
*/
|
|
410
|
+
settings?: EngineOptions["settings"];
|
|
379
411
|
fixedHz?: number;
|
|
380
412
|
/** Renderer extras (pixelRatio, antialias, custom asset store). */
|
|
381
413
|
pixelRatio?: number;
|
|
@@ -415,7 +447,22 @@ interface Game2DRenderer extends GameRenderer {
|
|
|
415
447
|
}
|
|
416
448
|
interface Game2D {
|
|
417
449
|
engine: Engine;
|
|
418
|
-
|
|
450
|
+
/**
|
|
451
|
+
* The LIVE scene — a getter over `engine.scene`, not a snapshot.
|
|
452
|
+
*
|
|
453
|
+
* This used to be the scene captured at boot. `Engine.setScene` calls
|
|
454
|
+
* `this._scene?.root.free()`, so after any swap the old handle was not merely
|
|
455
|
+
* stale, it was EMPTIED — and a single-scene game reaches that through
|
|
456
|
+
* `flow.restart()`, which `incanto-hud.md` wires to a Start button. Measured:
|
|
457
|
+
*
|
|
458
|
+
* before swap game.scene.root.children = 2 engine.scene = 2
|
|
459
|
+
* after swap game.scene.root.children = 0 engine.scene = 2
|
|
460
|
+
* game.scene.root.getNode('Flow') -> No node at 'Flow'. Children here: [].
|
|
461
|
+
*
|
|
462
|
+
* `game.engine.scene` always worked and three skills use it, so there were
|
|
463
|
+
* two handles with one name and no note saying which was which.
|
|
464
|
+
*/
|
|
465
|
+
get scene(): Scene$1;
|
|
419
466
|
/**
|
|
420
467
|
* What the last drawn frame actually looks like, as numbers.
|
|
421
468
|
*
|
|
@@ -719,6 +766,31 @@ declare class Joint2D extends Node2D {
|
|
|
719
766
|
length: number;
|
|
720
767
|
stiffness: number;
|
|
721
768
|
damping: number;
|
|
769
|
+
/**
|
|
770
|
+
* Do the two linked bodies still collide with EACH OTHER?
|
|
771
|
+
*
|
|
772
|
+
* `null` (default) decides from the joint type, because the two families want
|
|
773
|
+
* opposite answers:
|
|
774
|
+
*
|
|
775
|
+
* - **pivots** (`fixed`, `spherical`, `revolute`) → **off**. A hinge's bodies
|
|
776
|
+
* overlap at the pivot by construction, so the contact solver fights the
|
|
777
|
+
* joint and flings them. A puzzle builder's first hinge produced a bar that
|
|
778
|
+
* spun chaotically forever and threw a 40 kg box thirty metres, and they
|
|
779
|
+
* nearly filed "spherical joints inject energy":
|
|
780
|
+
*
|
|
781
|
+
* ```
|
|
782
|
+
* t= 0 Plank r=[0,0,0] Box [ 0.00, 5.50, 2.40]
|
|
783
|
+
* t=3000 Plank r=[-66,24,64] Box [-29.70, 0.40, -4.91]
|
|
784
|
+
* ```
|
|
785
|
+
*
|
|
786
|
+
* - **tethers** (`rope`, `spring`) → **on**. These anchor a thing to the world,
|
|
787
|
+
* and the thing has to rest on what it is tethered to. Defaulting these off
|
|
788
|
+
* drops a barrel roped to the floor straight through it — measured, in this
|
|
789
|
+
* repo's own conformance room.
|
|
790
|
+
*
|
|
791
|
+
* Set `true`/`false` to say it outright.
|
|
792
|
+
*/
|
|
793
|
+
collide: boolean | null;
|
|
722
794
|
override onEnterTree(): void;
|
|
723
795
|
/** @internal The two bodies, resolved (throws on a bad target at step time). */
|
|
724
796
|
_resolveBodies(): {
|
package/dist/2d.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as IncantoError } from "./errors-BpWbnbb_.js";
|
|
2
|
-
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-
|
|
3
|
-
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-
|
|
4
|
-
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-
|
|
2
|
+
import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-DMetd3vR.js";
|
|
3
|
+
import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-vIxU3Xbv.js";
|
|
4
|
+
import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-_B6zj3zU.js";
|
|
5
5
|
//#region src/2d/library-sprite.ts
|
|
6
6
|
/**
|
|
7
7
|
* What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
|