incanto 0.28.0 → 0.30.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.
Files changed (63) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +219 -29
  3. package/dist/2d.d.ts +54 -6
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +353 -16
  6. package/dist/3d.js +5 -5
  7. package/dist/{behavior-CPibUfnH.d.ts → behavior-BAc0erXF.d.ts} +21 -0
  8. package/dist/{create-game-71TJjW1T.js → create-game-5z_QVtLx.js} +59 -16
  9. package/dist/{create-game-BRgWpNsa.js → create-game-DuBTv2zI.js} +276 -49
  10. package/dist/debug-draw-BM3DsvtT.js +18 -0
  11. package/dist/debug.d.ts +51 -6
  12. package/dist/debug.js +242 -19
  13. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8554 -0
  16. package/dist/{gameplay-DEG-TP7D.js → gameplay-Cfr6aFZ1.js} +219 -40
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +31 -3
  20. package/dist/index.js +2 -2
  21. package/dist/{loader-DILt9PGC.d.ts → loader-B242FF6N.d.ts} +1 -1
  22. package/dist/net.d.ts +1 -1
  23. package/dist/net.js +2 -2
  24. package/dist/{pathfinding-RWYkNKx9.d.ts → pathfinding-BwD974Ss.d.ts} +1 -1
  25. package/dist/{physics-2d-DiVFFlH3.js → physics-2d-3kOQCtgd.js} +88 -3
  26. package/dist/{physics-3d--y5clE2j.js → physics-3d-CeRH-Ff_.js} +475 -9
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-nObreUQR.js → register-BTg0EM7s.js} +35 -3
  30. package/dist/{register-6R75AC7-.js → register-DJ0SByQg.js} +5550 -5073
  31. package/dist/{register-CvpSUU3O.js → register-DWcWq4QG.js} +22 -2
  32. package/dist/{register-BFFE1Mh1.js → register-MelqEdza.js} +1 -1
  33. package/dist/teardown-ByzfDPyu.js +240 -0
  34. package/dist/test.d.ts +35 -3
  35. package/dist/test.js +104 -10
  36. package/dist/vite.d.ts +139 -1
  37. package/dist/vite.js +355 -4
  38. package/editor/assets/{agent8-CojUfCXN.js → agent8-CAp0i5qn.js} +1 -1
  39. package/editor/assets/debug-BoEYfbqK.js +2 -0
  40. package/editor/assets/index-BO6WU8by.js +10696 -0
  41. package/editor/index.html +3 -157
  42. package/package.json +3 -2
  43. package/schemas/scene.schema.json +102 -4
  44. package/skills/incanto-3d-models.md +38 -0
  45. package/skills/incanto-assets.md +13 -0
  46. package/skills/incanto-building-3d-games.md +83 -6
  47. package/skills/incanto-editor.md +212 -8
  48. package/skills/incanto-environment.md +65 -1
  49. package/skills/incanto-node-reference.md +33 -1
  50. package/skills/incanto-physics-and-input.md +60 -6
  51. package/skills/incanto-verifying-your-game.md +57 -2
  52. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  53. package/templates-app/beacon-isle-3d/package.json +1 -1
  54. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  55. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/tps-3d/package.json +1 -1
  57. package/templates-app/tps-3d/vite.config.ts +7 -0
  58. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  59. package/templates-app/village-quest-3d/package.json +1 -1
  60. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  61. package/dist/debug-draw-CZmOYjL2.js +0 -13
  62. package/editor/assets/index-D6RQgROR.js +0 -8330
  63. package/editor/assets/index-D8QvwvOm.css +0 -1
package/README.md CHANGED
@@ -53,6 +53,50 @@ engine.start();
53
53
  - `schemas/scene.schema.json` — generated JSON Schema for scene files (the agent contract)
54
54
  - `templates/agent8-server.js` — the multiplayer server kernel for the agent8 platform
55
55
 
56
+ ## Debug mode
57
+
58
+ Off by default, with **no URL toggle** — a deployed build can never be switched on by a player.
59
+ Gate it to your own dev server (every template already ships the line):
60
+
61
+ ```ts
62
+ createGame3D({ ..., debug: import.meta.env.VITE_INCANTO_DEBUG === '1' });
63
+ ```
64
+
65
+ ```bash
66
+ VITE_INCANTO_DEBUG=1 bun run dev # overlay ON; plain `bun run dev` and prod builds = off
67
+ ```
68
+
69
+ A **☰ debug** menu appears top-left: **Explorer** (live scene tree; the selected node's bounds
70
+ light up orange in-game), **Inspector** (its props, editable while running), **Logs**
71
+ (`engine.log` tail with filters), **Stats** (fps · frame ms · nodes · tris · draw calls), and
72
+ **Colliders** (physics shapes as wireframe; one click cycles off → all → **selected**, and
73
+ `selected` draws only the collider of whatever the Explorer has picked — the way to find one shape
74
+ in a world full of them). In code: `physics.debugDraw = true` + `physics.debugScope = node`, or
75
+ `(await import('incanto/debug')).attachDebugOverlay(engine)` to boot the overlay by hand.
76
+
77
+ ## The agent8 asset library
78
+
79
+ `bunx incanto-editor --token <v8 access token>` (or `INCANTO_V8_TOKEN`) adds a
80
+ **📚** button to every field that takes a resource — model, texture, sprite
81
+ sheet, sound. Shelves, search, real previews (a GLB is rendered by the engine
82
+ itself), and one pick writes the URL, or the scene `assets{}` entry plus its
83
+ `$ref` for fields that take one. Without a token the panel asks for one and
84
+ keeps it in the browser; the editor server proxies the catalog because it is
85
+ per-account and not CORS-open to localhost.
86
+
87
+ ## Play ⇄ edit, in one window
88
+
89
+ The menu's last item, **✎ edit this scene**, turns the running game into the scene editor in the
90
+ page you are already in — same window, no reload, and the edit camera starts where the player's
91
+ eye was. The editor's **▶ play** offers the way back as a choice: **preview here** (simulate in
92
+ the viewport, scripts stripped) or **run as the game** (this page becomes the real game again,
93
+ with your edits). **✕ exit** returns without playing.
94
+
95
+ It follows `debug`, so an overlay build already has it. `editor: false` opts out;
96
+ `editor: { save }` gives the editor a save button that writes your file; `game.openEditor()` is
97
+ the same switch as a function. The editor loads lazily (`incanto/editor`) the first time it is
98
+ asked for — a game that never opens it never downloads it.
99
+
56
100
  Docs, agent skills, examples, and architecture: **https://github.com/rareboe/Incanto**
57
101
 
58
102
  © 2026 Verse8. All rights reserved. Proprietary — see [LICENSE](./LICENSE). Bundled third-party OSS notices: [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md).
@@ -9,17 +9,25 @@
9
9
  *
10
10
  * API (the page can only address *.scene.json files under the launch directory):
11
11
  * GET /api/meta → { version, mode, root, input?, output? }
12
- * GET /api/scenes → [{ rel, abs }] (project mode)
12
+ * GET /api/scenes → [{ rel, abs, size, modified, scene?|notScene? }]
13
13
  * POST /api/scenes → { path } creates a minimal scene (project mode)
14
14
  * GET /api/scene[?file=] → scene JSON (single mode ignores ?file=)
15
15
  * PUT /api/scene[?file=] → validates the scene shape, writes the file
16
+ * GET /api/library?tags=… → proxies the agent8 asset catalog (see below)
17
+ *
18
+ * The library proxy exists because the catalog cannot be reached from a page:
19
+ * it wants a Verse8 access token and its CORS allow-list has no localhost in
20
+ * it. This server has neither problem. The token comes from --token, from
21
+ * INCANTO_V8_TOKEN / V8_ACCESS_TOKEN, or from the page's own
22
+ * `x-incanto-v8-token` header (what the browser stores when you paste one) —
23
+ * it is forwarded upstream and never written to disk.
16
24
  *
17
25
  * Security: on loopback binds, the Host header must be loopback (defeats DNS
18
26
  * rebinding); writes additionally require Origin (when present) to match the
19
27
  * request host. Bodies are capped at 8MB. Addressable paths are root-bounded
20
28
  * and must end in .scene.json.
21
29
  */
22
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
23
31
  import { createServer } from 'node:http';
24
32
  import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
25
33
  import { fileURLToPath } from 'node:url';
@@ -60,6 +68,7 @@ function parseArgs(argv) {
60
68
  for (let i = 0; i < argv.length; i++) {
61
69
  const a = argv[i];
62
70
  if (a === '--output') args.output = argv[++i];
71
+ else if (a === '--token') args.token = argv[++i];
63
72
  else if (a === '--port') args.port = Number(argv[++i]);
64
73
  else if (a === '--host') args.host = argv[++i];
65
74
  else if (a === '--help' || a === '-h') args.help = true;
@@ -79,7 +88,11 @@ if (args.help || args.error || Number.isNaN(args.port) || (args.output && !args.
79
88
  Without a file: PROJECT mode — discovers every *.scene.json under the current
80
89
  directory (a picker opens; a single scene auto-opens; new scenes can be created).
81
90
  With a file: SINGLE mode — that file only; saving writes to --output (default:
82
- the input file). --output requires an input file. Use --host 0.0.0.0 inside containers.`);
91
+ the input file). --output requires an input file. Use --host 0.0.0.0 inside containers.
92
+
93
+ --token <v8 access token> (or INCANTO_V8_TOKEN) unlocks the agent8 asset library
94
+ inside the editor. Without it the 📚 button asks for a token and remembers it in
95
+ the browser instead.`);
83
96
  process.exit(args.help && !args.error ? 0 : 1);
84
97
  }
85
98
 
@@ -91,6 +104,102 @@ const VERSION = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf-8')
91
104
 
92
105
  const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'out', 'coverage']);
93
106
 
107
+ // --- agent8 asset library -------------------------------------------------
108
+ const LIBRARY_URL =
109
+ process.env.INCANTO_LIBRARY_URL ?? 'https://agent8-backend.verse8.io/api/resources';
110
+ const ENV_TOKEN = args.token ?? process.env.INCANTO_V8_TOKEN ?? process.env.V8_ACCESS_TOKEN ?? '';
111
+ const LIBRARY_TIMEOUT_MS = 15_000;
112
+
113
+ /** Upstream row → the shape the editor consumes (ids are strings here). */
114
+ function libraryItem(row) {
115
+ return {
116
+ id: String(row?.id ?? row?.url ?? ''),
117
+ url: String(row?.url ?? ''),
118
+ description: typeof row?.description === 'string' ? row.description : '',
119
+ metadata: row?.metadata && typeof row.metadata === 'object' ? row.metadata : {},
120
+ };
121
+ }
122
+
123
+ async function proxyLibrary(req, res, url) {
124
+ // the page's token wins over the server's: a user who pasted one is telling
125
+ // us which account to search as
126
+ const token = (req.headers['x-incanto-v8-token'] || '').toString().trim() || ENV_TOKEN;
127
+ if (!token) {
128
+ return send(
129
+ res,
130
+ 401,
131
+ JSON.stringify({
132
+ error:
133
+ 'no Verse8 access token. Start the editor with --token <t> (or INCANTO_V8_TOKEN), or paste one into the library panel.',
134
+ needsToken: true,
135
+ }),
136
+ );
137
+ }
138
+ const params = new URLSearchParams({
139
+ tags: url.searchParams.get('tags') ?? '',
140
+ page: url.searchParams.get('page') ?? '1',
141
+ limit: url.searchParams.get('limit') ?? '24',
142
+ });
143
+ const keyword = url.searchParams.get('keyword');
144
+ if (keyword) params.set('keyword', keyword);
145
+
146
+ const controller = new AbortController();
147
+ const timer = setTimeout(() => controller.abort(), LIBRARY_TIMEOUT_MS);
148
+ try {
149
+ const upstream = await fetch(`${LIBRARY_URL}?${params}`, {
150
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json' },
151
+ signal: controller.signal,
152
+ });
153
+ const text = await upstream.text();
154
+ if (upstream.status === 401 || upstream.status === 403) {
155
+ return send(
156
+ res,
157
+ 401,
158
+ JSON.stringify({
159
+ error: 'the asset library rejected that token (expired or wrong account)',
160
+ needsToken: true,
161
+ }),
162
+ );
163
+ }
164
+ if (!upstream.ok) {
165
+ return send(res, 502, JSON.stringify({ error: `asset library: HTTP ${upstream.status}` }));
166
+ }
167
+ let body;
168
+ try {
169
+ body = JSON.parse(text);
170
+ } catch {
171
+ return send(res, 502, JSON.stringify({ error: 'asset library returned non-JSON' }));
172
+ }
173
+ if (body?.success === false) {
174
+ return send(res, 502, JSON.stringify({ error: String(body.error ?? 'asset library error') }));
175
+ }
176
+ const rows = Array.isArray(body?.data) ? body.data : [];
177
+ return send(
178
+ res,
179
+ 200,
180
+ JSON.stringify({
181
+ items: rows.map(libraryItem).filter((i) => i.url),
182
+ page: Number(body?.pagination?.page ?? params.get('page') ?? 1),
183
+ hasNext: body?.pagination?.hasNext === true,
184
+ total: typeof body?.pagination?.total === 'number' ? body.pagination.total : undefined,
185
+ }),
186
+ );
187
+ } catch (error) {
188
+ const aborted = error?.name === 'AbortError';
189
+ return send(
190
+ res,
191
+ 502,
192
+ JSON.stringify({
193
+ error: aborted
194
+ ? `asset library timed out after ${LIBRARY_TIMEOUT_MS / 1000}s`
195
+ : `asset library unreachable: ${String(error?.message ?? error)}`,
196
+ }),
197
+ );
198
+ } finally {
199
+ clearTimeout(timer);
200
+ }
201
+ }
202
+
94
203
  function discoverScenes() {
95
204
  const found = [];
96
205
  const walk = (dir, depth) => {
@@ -104,9 +213,20 @@ function discoverScenes() {
104
213
  for (const e of entries) {
105
214
  if (e.isDirectory()) {
106
215
  if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), depth + 1);
107
- } else if (e.name.endsWith('.scene.json')) {
216
+ } else if (e.name.endsWith('.json')) {
108
217
  const abs = join(dir, e.name);
109
- found.push({ rel: relative(ROOT, abs), abs });
218
+ // size + mtime are what the browser prints beside each row; a folder
219
+ // whose files are all "just now" is the one you were working in
220
+ let size = 0;
221
+ let modified = 0;
222
+ try {
223
+ const stat = statSync(abs);
224
+ size = stat.size;
225
+ modified = stat.mtimeMs;
226
+ } catch {
227
+ // listed without facts beats not listed at all
228
+ }
229
+ found.push({ rel: relative(ROOT, abs), abs, size, modified, ...inspectJson(abs, size) });
110
230
  }
111
231
  }
112
232
  };
@@ -114,9 +234,65 @@ function discoverScenes() {
114
234
  return found.sort((a, b) => a.rel.localeCompare(b.rel));
115
235
  }
116
236
 
117
- /** Root-bounded, *.scene.json-only — the page can never address anything else. */
237
+ /**
238
+ * Is this JSON a scene? The file says so itself — `type: "scene"`, the same key
239
+ * the loader demands — so a row can be marked instead of guessed from its name.
240
+ */
241
+ function sceneFacts(json) {
242
+ if (json === null || typeof json !== 'object' || Array.isArray(json)) {
243
+ return { ok: false, why: 'the file is JSON, but not a JSON object' };
244
+ }
245
+ if (json.type !== 'scene') {
246
+ return {
247
+ ok: false,
248
+ why:
249
+ json.type === undefined
250
+ ? '"type" must be "scene", and this file has no "type"'
251
+ : `"type" must be "scene", not ${JSON.stringify(json.type)}`,
252
+ };
253
+ }
254
+ if (json.format !== 1)
255
+ return { ok: false, why: `"format" must be 1, not ${JSON.stringify(json.format)}` };
256
+ if (typeof json.root !== 'object' || json.root === null) {
257
+ return { ok: false, why: '"root" must be a node object' };
258
+ }
259
+ const count = (n) =>
260
+ Array.isArray(n?.children) ? n.children.reduce((t, c) => t + count(c), 1) : 1;
261
+ return {
262
+ ok: true,
263
+ name: typeof json.name === 'string' ? json.name : '',
264
+ // only when the file SAYS so — `dimension` is optional and has no default
265
+ ...(json.dimension === '2d' || json.dimension === '3d' ? { dimension: json.dimension } : {}),
266
+ nodes: count(json.root),
267
+ };
268
+ }
269
+
270
+ function inspectJson(abs, size) {
271
+ if (size > 16_000_000) return { notScene: 'too large to check without opening it' };
272
+ let parsed;
273
+ try {
274
+ parsed = JSON.parse(readFileSync(abs, 'utf-8'));
275
+ } catch (error) {
276
+ return { notScene: `not valid JSON: ${String(error.message ?? error)}` };
277
+ }
278
+ const facts = sceneFacts(parsed);
279
+ return facts.ok
280
+ ? {
281
+ scene: {
282
+ name: facts.name,
283
+ ...(facts.dimension ? { dimension: facts.dimension } : {}),
284
+ nodes: facts.nodes,
285
+ },
286
+ }
287
+ : { notScene: facts.why };
288
+ }
289
+
290
+ /**
291
+ * Root-bounded, .json-only — the browser offers every JSON and the LOAD decides,
292
+ * but this must never become a way to read .env or somebody's key file.
293
+ */
118
294
  function resolveSceneFile(rel) {
119
- if (typeof rel !== 'string' || !rel.endsWith('.scene.json')) return null;
295
+ if (typeof rel !== 'string' || !rel.endsWith('.json')) return null;
120
296
  const abs = normalize(resolve(ROOT, rel));
121
297
  if (abs !== ROOT && !abs.startsWith(ROOT + sep)) return null;
122
298
  return abs;
@@ -167,18 +343,6 @@ function originAllowed(req) {
167
343
  }
168
344
  }
169
345
 
170
- function isSceneShaped(json) {
171
- return (
172
- json !== null &&
173
- typeof json === 'object' &&
174
- json.format === 1 &&
175
- json.type === 'scene' &&
176
- typeof json.name === 'string' &&
177
- json.root !== null &&
178
- typeof json.root === 'object'
179
- );
180
- }
181
-
182
346
  function readBody(req, res, onJson) {
183
347
  const chunks = [];
184
348
  let received = 0;
@@ -217,6 +381,14 @@ const server = createServer((req, res) => {
217
381
  );
218
382
  }
219
383
 
384
+ if (url.pathname === '/api/library') {
385
+ if (req.method !== 'GET') {
386
+ return send(res, 405, JSON.stringify({ error: 'method not allowed' }));
387
+ }
388
+ proxyLibrary(req, res, url);
389
+ return;
390
+ }
391
+
220
392
  if (url.pathname === '/api/scenes') {
221
393
  if (MODE !== 'project') {
222
394
  return send(res, 400, JSON.stringify({ error: 'scene listing is project-mode only' }));
@@ -229,18 +401,23 @@ const server = createServer((req, res) => {
229
401
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
230
402
  }
231
403
  readBody(req, res, (body) => {
232
- const abs = resolveSceneFile(body?.path);
404
+ // a path with no extension means the usual thing, so `levels/arena` works
405
+ const wanted =
406
+ typeof body?.path === 'string' && body.path && !body.path.endsWith('.json')
407
+ ? `${body.path}.scene.json`
408
+ : body?.path;
409
+ const abs = resolveSceneFile(wanted);
233
410
  if (!abs) {
234
411
  return send(
235
412
  res,
236
413
  400,
237
- JSON.stringify({ error: 'path must be a *.scene.json inside the project' }),
414
+ JSON.stringify({ error: 'path must be a .json file inside the project' }),
238
415
  );
239
416
  }
240
417
  if (existsSync(abs)) {
241
- return send(res, 409, JSON.stringify({ error: `already exists: ${body.path}` }));
418
+ return send(res, 409, JSON.stringify({ error: `already exists: ${wanted}` }));
242
419
  }
243
- const name = basename(abs).replace(/\.scene\.json$/, '') || 'Scene';
420
+ const name = basename(abs).replace(/\.scene\.json$|\.json$/, '') || 'Scene';
244
421
  const scene = {
245
422
  format: 1,
246
423
  type: 'scene',
@@ -271,26 +448,39 @@ const server = createServer((req, res) => {
271
448
  );
272
449
  }
273
450
  if (req.method === 'GET') {
451
+ let text;
452
+ let parsed;
274
453
  try {
275
- const text = readFileSync(target.input, 'utf-8');
276
- JSON.parse(text); // must at least be JSON before the page gets it
277
- return send(res, 200, text);
454
+ text = readFileSync(target.input, 'utf-8');
455
+ parsed = JSON.parse(text); // must at least be JSON before the page gets it
278
456
  } catch (error) {
279
457
  return send(res, 422, JSON.stringify({ error: String(error.message ?? error) }));
280
458
  }
459
+ // A JSON that is not a scene is refused WITH THE REASON and its contents
460
+ // never leave: "offer everything, explain the no" must not also mean
461
+ // "this server will read you any JSON in the project".
462
+ const facts = sceneFacts(parsed);
463
+ if (!facts.ok) {
464
+ return send(
465
+ res,
466
+ 422,
467
+ JSON.stringify({ error: `not an incanto scene — ${facts.why}.`, notScene: true }),
468
+ );
469
+ }
470
+ return send(res, 200, text);
281
471
  }
282
472
  if (req.method === 'PUT') {
283
473
  if (!originAllowed(req)) {
284
474
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
285
475
  }
286
476
  readBody(req, res, (json, raw) => {
287
- if (!isSceneShaped(json)) {
477
+ const shape = sceneFacts(json);
478
+ if (!shape.ok) {
288
479
  return send(
289
480
  res,
290
481
  422,
291
482
  JSON.stringify({
292
- error:
293
- 'not an incanto scene — required: format: 1, type: "scene", name: string, root: object. The file was NOT written.',
483
+ error: `not an incanto scene — ${shape.why}. The file was NOT written.`,
294
484
  }),
295
485
  );
296
486
  }
package/dist/2d.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { s as JsonObject } from "./schema-CcoWb32N.js";
2
- import { $ as Node, C as RendererStats, S as GameStats, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-CPibUfnH.js";
3
- import { t as LoadSceneOptions } from "./loader-DILt9PGC.js";
1
+ import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
+ import { $ as Node, C as RendererStats, S as GameStats, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-BAc0erXF.js";
3
+ import { t as EditorSwitchOptions } from "./editor-switch-B0wB_DSr.js";
4
+ import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
4
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
5
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
6
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -145,6 +146,12 @@ type Rapier = typeof RapierNs;
145
146
  interface Physics2DOptions {
146
147
  /** px/s², y-down. Default: scene `physics.gravity`, else [0, 980]. */
147
148
  gravity?: [number, number];
149
+ /**
150
+ * Build the colliders and keep them on the tree, but never advance the world
151
+ * (default: `true`, i.e. simulate). See {@link Physics3DOptions.simulate} —
152
+ * the scene editor uses it to draw the REAL colliders without running them.
153
+ */
154
+ simulate?: boolean;
148
155
  }
149
156
  /**
150
157
  * Enable 2D physics for an engine. Dynamically imports Rapier (compat build,
@@ -157,9 +164,11 @@ declare function enablePhysics2D(engine: Engine, opts?: Physics2DOptions): Promi
157
164
  */
158
165
  declare class Physics2D {
159
166
  private readonly R;
160
- private readonly engine;
167
+ readonly engine: Engine;
161
168
  /** Render collider outlines in the GAME view (renderers pick this up). */
162
169
  debugDraw: boolean;
170
+ /** Narrow those outlines to one node's subtree (see DebugLineSource). */
171
+ debugScope: object | null;
163
172
  readonly dimension = "2d";
164
173
  private readonly unregisterDebug;
165
174
  private readonly warnedNoCollider;
@@ -174,11 +183,20 @@ declare class Physics2D {
174
183
  private lastScene;
175
184
  constructor(R: Rapier, engine: Engine, opts?: Physics2DOptions);
176
185
  /** @internal Driven by engine.fixedUpdated. */
186
+ /** Whether the solver runs at all. False = collider outlines only. */
187
+ private readonly simulate;
177
188
  step(dt: number): void;
178
189
  /** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */
179
190
  moveAndSlide(node: CharacterBody2D): void;
180
191
  /** Rapier debug segments, scaled back to pixels. Null while debugDraw is off. */
181
192
  debugLines(): Float32Array | null;
193
+ /**
194
+ * Only the selected node's colliders (meters — the caller scales to pixels).
195
+ * Rapier draws the whole world or nothing, so a chosen few are outlined from
196
+ * the same shape data the solver holds. 2D colliders are rect/circle/capsule,
197
+ * which is the whole switch.
198
+ */
199
+ private scopedLines;
182
200
  /** Create newly-arrived joints; tear down departed ones. */
183
201
  private syncJoints;
184
202
  /**
@@ -200,6 +218,13 @@ declare class Physics2D {
200
218
  private readonly jointSet;
201
219
  private readonly joints;
202
220
  private readonly bodySet;
221
+ /**
222
+ * Move every body to its node's current transform — outline mode only.
223
+ *
224
+ * Computed exactly as `ensureEntry` computes a body's first pose, so an
225
+ * outline drawn in the editor is the outline the running game draws.
226
+ */
227
+ private poseFromTree;
203
228
  private syncBodies;
204
229
  private ensureEntry;
205
230
  }
@@ -249,6 +274,17 @@ interface CreateGame2DOptions {
249
274
  * to development, e.g. `debug: import.meta.env.VITE_INCANTO_DEBUG === '1'`.
250
275
  */
251
276
  debug?: boolean;
277
+ /**
278
+ * LIVE EDITING: the ☰ debug menu gains "edit this scene", which turns this
279
+ * page into the scene editor — same window, no reload — and the editor's play
280
+ * button turns it back into the game.
281
+ *
282
+ * DEFAULTS TO `debug`, costs nothing until clicked (the editor is a lazy
283
+ * chunk), and is never reachable without it — the ☰ menu is its only
284
+ * entrance. `false` opts out; `{ open }` loads the editor yourself (apps that
285
+ * alias incanto to source); `{ save }` makes its save button write your file.
286
+ */
287
+ editor?: boolean | EditorSwitchOptions;
252
288
  /** @internal Test seam — replaces `document` for the debug overlay. */
253
289
  _debugDoc?: {
254
290
  createElement(tag: string): HTMLElement;
@@ -272,10 +308,22 @@ interface GameRenderer {
272
308
  interface Game2D {
273
309
  engine: Engine;
274
310
  scene: Scene$1;
311
+ /**
312
+ * The AUTHORED scene this game booted from — the JSON, not the live tree.
313
+ * A running tree has moved; the editor gets what was written down.
314
+ */
315
+ sourceJson: SceneJson;
275
316
  renderer: GameRenderer;
276
317
  physics: Physics2D | null;
277
318
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
278
319
  stats(): GameStats;
320
+ /**
321
+ * Turn this page into the scene editor — what the ☰ debug menu's "edit this
322
+ * scene" calls. The game is disposed on the way out and rebooted when the
323
+ * editor hands it back, so THIS handle is dead afterwards. `null` when the
324
+ * game was booted with `editor: false`.
325
+ */
326
+ openEditor: (() => Promise<void>) | null;
279
327
  /** Tear the whole game down (renderer, loop, scene, listeners). */
280
328
  dispose(): void;
281
329
  }
@@ -724,8 +772,8 @@ declare class Renderer2D {
724
772
  /** The world view used by the LAST render (center/zoom/canvas size). */
725
773
  view(): View2D;
726
774
  /**
727
- * GPU counters for the LAST rendered frame (world + UI passes combined),
728
- * straight from three's per-frame-maintained `renderer.info`.
775
+ * GPU counters for the LAST rendered frame world + UI passes combined (see
776
+ * the `info.reset()` in the render pass), plus GPU memory.
729
777
  */
730
778
  stats(): RendererStats;
731
779
  worldFromScreen(sx: number, sy: number): {
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-71TJjW1T.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-CvpSUU3O.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-DiVFFlH3.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-5z_QVtLx.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-DWcWq4QG.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-3kOQCtgd.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset