incanto 0.66.0 → 0.68.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/_pick-server.mjs +48 -0
- package/bin/incanto-check.mjs +16 -2
- package/bin/incanto-editor.mjs +39 -2
- package/bin/incanto-feel.mjs +4 -1
- package/bin/incanto-frame.mjs +37 -3
- package/bin/incanto-logs.mjs +23 -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 +44 -7
- package/dist/2d.js +6 -4
- package/dist/3d.d.ts +112 -16
- package/dist/3d.js +8 -6
- package/dist/{audio-player-_UAcHxnC.d.ts → audio-player-DOrq7sP-.d.ts} +62 -4
- package/dist/{behavior-BXNLfIJk.d.ts → behavior-DoFPYrgo.d.ts} +117 -2
- package/dist/boot-failure-CKYrEcGF.js +47 -0
- package/dist/{create-game-DYJCIzO0.js → create-game-DbWtVTxD.js} +91 -29
- package/dist/{create-game-Dd7H4bJV.js → create-game-IZIydDwI.js} +16 -10
- package/dist/debug.d.ts +1 -1
- package/dist/debug.js +3 -2
- package/dist/{duplicate-CqSAtdrh.js → duplicate-IWIqk0HJ.js} +1 -1
- package/dist/{editor-switch-DAvWQeld.d.ts → editor-switch-CnIOiyNJ.d.ts} +1 -1
- package/dist/editor.js +18 -7
- package/dist/env.d.ts +1 -1
- package/dist/{environment-presets-BlPsEmq6.js → environment-presets-CybQXNqS.js} +100 -11
- package/dist/{gameplay-D1RADWu3.js → gameplay-DM1eu_cV.js} +46 -5
- package/dist/gameplay.d.ts +75 -6
- package/dist/gameplay.js +2 -2
- package/dist/index.d.ts +52 -8
- package/dist/index.js +9 -7
- package/dist/{loader-lQDCwNag.js → loader-BC4PNtJX.js} +173 -1
- package/dist/{loader-Cga7FVP4.d.ts → loader-CcB533FR.d.ts} +2 -2
- package/dist/net.d.ts +2 -2
- package/dist/net.js +1 -1
- package/dist/{physics-2d-Cji5A6sX.js → physics-2d-B7Y6dPZO.js} +29 -2
- package/dist/{physics-3d-BkHjwJgI.js → physics-3d-bG3n70Ky.js} +30 -3
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/{register-BmuqYTiY.js → register-3ta-2Xig.js} +148 -22
- package/dist/{register-D0CxCveZ.js → register-ibCjm-wH.js} +3 -3
- package/dist/{replay-IsZbNu6d.js → replay-CYvhVHHN.js} +28 -4
- package/dist/{replay-BHoB6fCU.d.ts → replay-Dvn8aeBd.d.ts} +38 -4
- package/dist/{schema-CFeioQRE.d.ts → schema-B6ugCV1Q.d.ts} +11 -0
- package/dist/{split-screen-D_i7GRcY.js → split-screen-CYwDkbLF.js} +2 -2
- package/dist/{split-screen-eJUFjhi1.d.ts → split-screen-DhrSzZIB.d.ts} +3 -3
- package/dist/{src-BTLbXFPZ.js → src-D7RIqXYF.js} +1 -1
- package/dist/{teardown-byR9USax.js → teardown-D2NEmxPB.js} +2 -2
- package/dist/{test-DgrD0jHD.js → test-8hoHeRmo.js} +140 -32
- package/dist/test.d.ts +9 -7
- package/dist/test.js +3 -3
- package/dist/{touch-DESwnpOc.js → touch-DEAmqGdf.js} +30 -186
- package/dist/vite.d.ts +0 -7
- package/dist/vite.js +13 -3
- package/dist/webgl-unavailable-N9nQqesw.js +228 -0
- package/editor/assets/{agent8-9N-Pd_YS.js → agent8-BrrHOjMJ.js} +1 -1
- package/editor/assets/debug-BBhKuBNV.js +3 -0
- package/editor/assets/{index-CeDhIPTC.js → index-eVd0BToA.js} +92 -92
- package/editor/index.html +1 -1
- package/package.json +1 -1
- package/schemas/scene.schema.json +5 -1
- package/skills/incanto-3d-character.md +1 -0
- package/skills/incanto-audio.md +49 -0
- package/skills/incanto-building-2d-games.md +37 -0
- package/skills/incanto-building-3d-games.md +56 -0
- package/skills/incanto-editor.md +5 -1
- package/skills/incanto-environment.md +2 -2
- package/skills/incanto-gameplay-behaviors.md +29 -2
- package/skills/incanto-hud.md +14 -1
- package/skills/incanto-multiplayer.md +14 -0
- package/skills/incanto-node-reference.md +2 -1
- package/skills/incanto-performance.md +35 -0
- package/skills/incanto-physics-and-input.md +23 -0
- package/skills/incanto-playtesting.md +15 -1
- package/skills/incanto-save-slots.md +21 -1
- package/skills/incanto-scene-json-authoring.md +12 -1
- package/skills/incanto-verifying-your-game.md +19 -0
- 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/beacon-isle-3d/src/main.ts +8 -1
- package/templates-app/platformer-2d/package.json +1 -1
- package/templates-app/platformer-2d/src/game.scene.json +99 -53
- package/templates-app/platformer-2d/src/main.ts +8 -1
- package/templates-app/star-survivor/package.json +1 -1
- package/templates-app/star-survivor/src/game.scene.json +31 -17
- package/templates-app/star-survivor/src/main.ts +8 -1
- package/templates-app/tps-3d/package.json +1 -1
- package/templates-app/tps-3d/src/game.scene.json +14 -8
- package/templates-app/tps-3d/src/main.ts +8 -1
- 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/main.ts +8 -1
- package/templates-app/village-quest-3d/src/village.scene.json +10 -7
- package/editor/assets/debug-CkbJICYp.js +0 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which of the dev servers that answered is THIS project's — the one decision
|
|
3
|
+
* `incanto-frame` and `incanto-logs` must never get wrong, in one place.
|
|
4
|
+
*
|
|
5
|
+
* They each had their own copy, and each copy had the same hatch: when exactly
|
|
6
|
+
* one server answered and it did not say whose it was, take it anyway ("an
|
|
7
|
+
* older incanto that predates the identity field"). A server that will not
|
|
8
|
+
* identify itself is the ONE kind the check cannot clear, so the hatch let
|
|
9
|
+
* through precisely what it was written to stop. Measured in an empty temp
|
|
10
|
+
* directory with one foreign page listening, on a scene with two nodes and no
|
|
11
|
+
* assets:
|
|
12
|
+
*
|
|
13
|
+
* ✓ draws — subject fills 37.5% of the frame
|
|
14
|
+
* ✗ says — asset …/NO-SUCH-TEXTURE.jpg never loaded: 404
|
|
15
|
+
*
|
|
16
|
+
* A ✓ measured off another game, and a ✗ naming a texture that appears nowhere
|
|
17
|
+
* in this project. It surfaced in this repo as an intermittent failure of
|
|
18
|
+
* `incanto-verify.test.ts`, which is a gate that lies in both directions.
|
|
19
|
+
*
|
|
20
|
+
* The real dev server has sent `root` since 0.41 (`frame-endpoint.test.ts`
|
|
21
|
+
* pins it), so nothing is lost; `--port` still names a server on purpose.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/** Is `body.root` this directory, an ancestor of it, or inside it? */
|
|
25
|
+
export function sameProject(body, cwd) {
|
|
26
|
+
if (!body?.root) return false;
|
|
27
|
+
const root = String(body.root);
|
|
28
|
+
return root === cwd || cwd.startsWith(`${root}/`) || root.startsWith(`${cwd}/`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* `{ pick }` when one of them is ours, `{ refusal }` when some answered and
|
|
33
|
+
* none is, `{}` when nothing answered at all (the caller says "no dev server").
|
|
34
|
+
*/
|
|
35
|
+
export function pickServer(answered, cwd, what) {
|
|
36
|
+
const mine = answered.filter((a) => sameProject(a, cwd));
|
|
37
|
+
if (mine.length > 0) return { pick: mine[0] };
|
|
38
|
+
if (answered.length === 0) return {};
|
|
39
|
+
const listed = answered
|
|
40
|
+
.map((a) => ` :${a.port} ${a.project || '(unnamed)'} ${a.root ?? "doesn't say"}`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
return {
|
|
43
|
+
refusal:
|
|
44
|
+
`${answered.length} incanto dev server(s) are running and none of them is this ` +
|
|
45
|
+
`directory (${cwd}):\n${listed}\n\n${what}\n` +
|
|
46
|
+
"Start this project's dev server, or pass --port.",
|
|
47
|
+
};
|
|
48
|
+
}
|
package/bin/incanto-check.mjs
CHANGED
|
@@ -155,7 +155,9 @@ if (files.length === 0) {
|
|
|
155
155
|
process.exit(1);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
const { auditScene, validateScene } = await import(
|
|
158
|
+
const { auditScene, instancedScenes, validateScene } = await import(
|
|
159
|
+
pathToFileURL(join(DIST, 'test.js')).href
|
|
160
|
+
);
|
|
159
161
|
|
|
160
162
|
/**
|
|
161
163
|
* Every `strings` table in the corpus, unioned.
|
|
@@ -176,9 +178,16 @@ const declaredElsewhere = {};
|
|
|
176
178
|
* part of its project it can.
|
|
177
179
|
*/
|
|
178
180
|
const declaredAssets = {};
|
|
181
|
+
/**
|
|
182
|
+
* Every scene some OTHER scene embeds — the only honest way to tell a fragment
|
|
183
|
+
* from a first draft. Without it a 3D scene with no camera and no light checked
|
|
184
|
+
* CLEAN, because "has no camera" was doing double duty as "is a fragment".
|
|
185
|
+
*/
|
|
186
|
+
const corpus = [];
|
|
179
187
|
for (const file of files) {
|
|
180
188
|
try {
|
|
181
189
|
const doc = JSON.parse(readFileSync(file, 'utf-8'));
|
|
190
|
+
corpus.push({ path: resolve(file), json: doc });
|
|
182
191
|
for (const [locale, entries] of Object.entries(doc?.strings ?? {})) {
|
|
183
192
|
if (typeof entries !== 'object' || entries === null) continue;
|
|
184
193
|
declaredElsewhere[locale] = { ...(declaredElsewhere[locale] ?? {}), ...entries };
|
|
@@ -191,6 +200,8 @@ for (const file of files) {
|
|
|
191
200
|
}
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
const instanced = instancedScenes(corpus, (from, ref) => resolve(dirname(from), ref));
|
|
204
|
+
|
|
194
205
|
const results = files.map((file) => {
|
|
195
206
|
let json;
|
|
196
207
|
try {
|
|
@@ -207,7 +218,10 @@ const results = files.map((file) => {
|
|
|
207
218
|
return {
|
|
208
219
|
file,
|
|
209
220
|
ok: true,
|
|
210
|
-
warnings: [
|
|
221
|
+
warnings: [
|
|
222
|
+
...auditScene(json, { declaredElsewhere, instanced: instanced.has(resolve(file)) }),
|
|
223
|
+
...missingArt(file, json),
|
|
224
|
+
],
|
|
211
225
|
};
|
|
212
226
|
}
|
|
213
227
|
return {
|
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
|
@@ -20,6 +20,7 @@ import { readFileSync, writeFileSync } from 'node:fs';
|
|
|
20
20
|
import { createRequire } from 'node:module';
|
|
21
21
|
import { dirname, join } from 'node:path';
|
|
22
22
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
23
|
+
import { pickServer } from './_pick-server.mjs';
|
|
23
24
|
|
|
24
25
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
26
|
const { listeningPorts, parseProcNetTcp } = await import(
|
|
@@ -150,11 +151,28 @@ const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
|
150
151
|
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
151
152
|
|
|
152
153
|
let found = null;
|
|
154
|
+
/**
|
|
155
|
+
* WHOSE dev server answered.
|
|
156
|
+
*
|
|
157
|
+
* This scans a port range, so before the ping carried identity, whoever
|
|
158
|
+
* answered first won — and a passing number from ANOTHER project silently
|
|
159
|
+
* overrode this one's failing one. Measured: project A on :5188 with a real
|
|
160
|
+
* page reported "the subject fills almost nothing (0.3%)", while `incanto
|
|
161
|
+
* verify` scanning from the same directory reported `✓ draws — 37.5%`,
|
|
162
|
+
* measured on project B. Corroboration from the other direction: four scenes
|
|
163
|
+
* across two unrelated projects all reported 22.6%.
|
|
164
|
+
*
|
|
165
|
+
* So: prefer the server rooted at THIS directory. If several answer and none is
|
|
166
|
+
* ours, refuse and list them — the same refusal `verify` already makes when a
|
|
167
|
+
* project has several scenes and guessing would verify the wrong one.
|
|
168
|
+
*/
|
|
169
|
+
const answered = [];
|
|
153
170
|
// BOTH hosts, deliberately: vite binds to ::1 on some machines and 127.0.0.1 on
|
|
154
171
|
// others, and a probe of the wrong one reports "no dev server" while the page
|
|
155
172
|
// is plainly on screen.
|
|
156
173
|
const HOSTS = ['localhost', '127.0.0.1'];
|
|
157
|
-
|
|
174
|
+
const CWD = process.cwd();
|
|
175
|
+
for (const port of ports) {
|
|
158
176
|
for (const host of HOSTS) {
|
|
159
177
|
try {
|
|
160
178
|
const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
|
|
@@ -163,8 +181,8 @@ outer: for (const port of ports) {
|
|
|
163
181
|
if (!res.ok) continue;
|
|
164
182
|
const body = await res.json();
|
|
165
183
|
if (body?.incanto) {
|
|
166
|
-
|
|
167
|
-
break
|
|
184
|
+
answered.push({ host, port, ...body });
|
|
185
|
+
break; // one host per port is enough
|
|
168
186
|
}
|
|
169
187
|
} catch {
|
|
170
188
|
// not listening, not ours, or not answering — either way, next
|
|
@@ -172,6 +190,22 @@ outer: for (const port of ports) {
|
|
|
172
190
|
}
|
|
173
191
|
}
|
|
174
192
|
|
|
193
|
+
if (explicit) {
|
|
194
|
+
found = answered[0] ?? null;
|
|
195
|
+
} else {
|
|
196
|
+
const { pick, refusal } = pickServer(
|
|
197
|
+
answered,
|
|
198
|
+
CWD,
|
|
199
|
+
"Measuring one project on another project's page reports a number that has\n" +
|
|
200
|
+
'nothing to do with this game.',
|
|
201
|
+
);
|
|
202
|
+
if (pick) found = pick;
|
|
203
|
+
else if (refusal) {
|
|
204
|
+
console.error(refusal);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
175
209
|
if (!found) {
|
|
176
210
|
console.error(
|
|
177
211
|
'no incanto dev server found.\n' +
|
package/bin/incanto-logs.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { readFileSync } from 'node:fs';
|
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import { dirname, join } from 'node:path';
|
|
23
23
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
24
|
+
import { pickServer } from './_pick-server.mjs';
|
|
24
25
|
|
|
25
26
|
const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
26
27
|
const { listeningPorts } = await import(pathToFileURL(join(PKG, 'dist', 'vite.js')).href);
|
|
@@ -75,7 +76,12 @@ const discovered = explicit ? [explicit] : await listeningPorts({ read });
|
|
|
75
76
|
const ports = explicit ? discovered : [...new Set([...bsdListeningPorts(), ...discovered])];
|
|
76
77
|
|
|
77
78
|
let found = null;
|
|
78
|
-
|
|
79
|
+
// Whose dev server answered — see incanto-frame.mjs for the measurement that
|
|
80
|
+
// made this necessary: a scan takes whoever replies first, and a foreign
|
|
81
|
+
// project's page answered both the frame and the logs request.
|
|
82
|
+
const answered = [];
|
|
83
|
+
const CWD = process.cwd();
|
|
84
|
+
for (const port of ports) {
|
|
79
85
|
for (const host of ['localhost', '127.0.0.1']) {
|
|
80
86
|
try {
|
|
81
87
|
const res = await fetch(`http://${host}:${port}/__incanto/ping`, {
|
|
@@ -84,14 +90,28 @@ outer: for (const port of ports) {
|
|
|
84
90
|
if (!res.ok) continue;
|
|
85
91
|
const body = await res.json();
|
|
86
92
|
if (body?.incanto) {
|
|
87
|
-
|
|
88
|
-
break
|
|
93
|
+
answered.push({ host, port, ...body });
|
|
94
|
+
break;
|
|
89
95
|
}
|
|
90
96
|
} catch {
|
|
91
97
|
// not listening, not ours, or not answering — next
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
}
|
|
101
|
+
if (explicit) {
|
|
102
|
+
found = answered[0] ? { host: answered[0].host, port: answered[0].port } : null;
|
|
103
|
+
} else {
|
|
104
|
+
const { pick, refusal } = pickServer(
|
|
105
|
+
answered,
|
|
106
|
+
CWD,
|
|
107
|
+
"Reading another project's log buffer would report its errors as yours.",
|
|
108
|
+
);
|
|
109
|
+
if (pick) found = { host: pick.host, port: pick.port };
|
|
110
|
+
else if (refusal) {
|
|
111
|
+
console.error(refusal);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
95
115
|
|
|
96
116
|
if (!found) {
|
|
97
117
|
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 {
|
|
2
|
-
import { n as
|
|
3
|
-
import {
|
|
1
|
+
import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-DOrq7sP-.js";
|
|
2
|
+
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-DoFPYrgo.js";
|
|
3
|
+
import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
|
|
4
|
+
import { i as SceneJson, s as JsonObject } from "./schema-B6ugCV1Q.js";
|
|
4
5
|
import { r as FrameStats } from "./frame-report-DCnHFmto.js";
|
|
5
|
-
import { t as LoadSceneOptions } from "./loader-
|
|
6
|
+
import { t as LoadSceneOptions } from "./loader-CcB533FR.js";
|
|
6
7
|
import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
|
|
7
|
-
import { i as ParticleSim, o as ParticleView, r as SpatialPose } from "./audio-player-_UAcHxnC.js";
|
|
8
8
|
import { Group, Mesh, Object3D, Scene, Texture } from "three";
|
|
9
9
|
import * as RapierNs from "@dimforge/rapier2d-compat";
|
|
10
10
|
|
|
@@ -279,6 +279,16 @@ declare class Physics2D {
|
|
|
279
279
|
/** Who is currently inside each sensor/body, maintained from the event drain. */
|
|
280
280
|
private readonly overlaps;
|
|
281
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;
|
|
282
292
|
/** The bodies currently overlapping `node`. Freed nodes are dropped on read. */
|
|
283
293
|
overlapping(node: PhysicsBody2D): PhysicsBody2D[];
|
|
284
294
|
/**
|
|
@@ -386,6 +396,18 @@ interface CreateGame2DOptions {
|
|
|
386
396
|
createElement(tag: string): HTMLElement;
|
|
387
397
|
};
|
|
388
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"];
|
|
389
411
|
fixedHz?: number;
|
|
390
412
|
/** Renderer extras (pixelRatio, antialias, custom asset store). */
|
|
391
413
|
pixelRatio?: number;
|
|
@@ -425,7 +447,22 @@ interface Game2DRenderer extends GameRenderer {
|
|
|
425
447
|
}
|
|
426
448
|
interface Game2D {
|
|
427
449
|
engine: Engine;
|
|
428
|
-
|
|
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;
|
|
429
466
|
/**
|
|
430
467
|
* What the last drawn frame actually looks like, as numbers.
|
|
431
468
|
*
|
|
@@ -1210,4 +1247,4 @@ declare function parseCells(cells: readonly (string | readonly number[])[], lege
|
|
|
1210
1247
|
*/
|
|
1211
1248
|
declare function mergeSolidRects(grid: readonly (readonly number[])[], solid: Set<number>): TileRect[];
|
|
1212
1249
|
//#endregion
|
|
1213
|
-
export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, mergeSolidRects, parseCells, registerNodes2D, spriteFromLibraryMeta, syncTree2D };
|
|
1250
|
+
export { AnimatedSprite2D, Area2D, type AssetStatus, AssetStore2D, Camera2D, CharacterBody2D, CharacterController2D, ColorRect2D, type CreateGame2DOptions, type Game2D, Joint2D, type JointType2D, Label, Node2D, Particles2D, Physics2D, type Physics2DOptions, PhysicsBody2D, Renderer2D, type Renderer2DOptions, type ResolvedSpriteTexture, RigidBody2D, type SheetInfo, Sprite2D, type SpriteFromLibraryResult, StaticBody2D, type Sync2DResult, type TextureLoadCallbacks, TileMap2D, type TileRect, type UIAnchor, UILayer, createGame2D, enablePhysics2D, isWebGLAvailable, mergeSolidRects, parseCells, registerNodes2D, showBootFailure, spriteFromLibraryMeta, syncTree2D };
|