incanto 0.28.0 → 0.29.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 (58) hide show
  1. package/README.md +44 -0
  2. package/bin/incanto-editor.mjs +118 -1
  3. package/dist/2d.d.ts +38 -5
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +319 -15
  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-BRgWpNsa.js → create-game-CNKXGfpr.js} +270 -47
  9. package/dist/{create-game-71TJjW1T.js → create-game-CbuLWorm.js} +53 -14
  10. package/dist/debug.d.ts +51 -6
  11. package/dist/debug.js +242 -19
  12. package/dist/editor-switch-B0wB_DSr.d.ts +59 -0
  13. package/dist/editor-switch-CAKlJMIY.js +161 -0
  14. package/dist/editor.d.ts +49 -0
  15. package/dist/editor.js +8439 -0
  16. package/dist/{gameplay-DEG-TP7D.js → gameplay-L05WgWd1.js} +207 -37
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +22 -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-CCVTrKOd.js} +62 -2
  26. package/dist/{physics-3d--y5clE2j.js → physics-3d-BZZLtwJu.js} +439 -8
  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-C44aSduO.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/test.d.ts +35 -3
  34. package/dist/test.js +104 -10
  35. package/dist/vite.d.ts +53 -1
  36. package/dist/vite.js +94 -2
  37. package/editor/assets/{agent8-CojUfCXN.js → agent8-DEVkEa3d.js} +1 -1
  38. package/editor/assets/debug-zGAtpDF0.js +2 -0
  39. package/editor/assets/index-Bx4UtWYY.js +10586 -0
  40. package/editor/index.html +3 -157
  41. package/package.json +3 -2
  42. package/schemas/scene.schema.json +102 -4
  43. package/skills/incanto-3d-models.md +38 -0
  44. package/skills/incanto-assets.md +13 -0
  45. package/skills/incanto-building-3d-games.md +83 -6
  46. package/skills/incanto-editor.md +151 -0
  47. package/skills/incanto-environment.md +65 -1
  48. package/skills/incanto-node-reference.md +33 -1
  49. package/skills/incanto-physics-and-input.md +45 -6
  50. package/skills/incanto-verifying-your-game.md +51 -2
  51. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +5 -0
  52. package/templates-app/beacon-isle-3d/package.json +1 -1
  53. package/templates-app/tps-3d/docs/project-3d-rules.md +5 -0
  54. package/templates-app/tps-3d/package.json +1 -1
  55. package/templates-app/village-quest-3d/docs/project-3d-rules.md +5 -0
  56. package/templates-app/village-quest-3d/package.json +1 -1
  57. package/editor/assets/index-D6RQgROR.js +0 -8330
  58. 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).
@@ -13,6 +13,14 @@
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
@@ -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) => {
@@ -217,6 +326,14 @@ const server = createServer((req, res) => {
217
326
  );
218
327
  }
219
328
 
329
+ if (url.pathname === '/api/library') {
330
+ if (req.method !== 'GET') {
331
+ return send(res, 405, JSON.stringify({ error: 'method not allowed' }));
332
+ }
333
+ proxyLibrary(req, res, url);
334
+ return;
335
+ }
336
+
220
337
  if (url.pathname === '/api/scenes') {
221
338
  if (MODE !== 'project') {
222
339
  return send(res, 400, JSON.stringify({ error: 'scene listing is project-mode only' }));
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";
@@ -160,6 +161,8 @@ declare class Physics2D {
160
161
  private readonly engine;
161
162
  /** Render collider outlines in the GAME view (renderers pick this up). */
162
163
  debugDraw: boolean;
164
+ /** Narrow those outlines to one node's subtree (see DebugLineSource). */
165
+ debugScope: object | null;
163
166
  readonly dimension = "2d";
164
167
  private readonly unregisterDebug;
165
168
  private readonly warnedNoCollider;
@@ -179,6 +182,13 @@ declare class Physics2D {
179
182
  moveAndSlide(node: CharacterBody2D): void;
180
183
  /** Rapier debug segments, scaled back to pixels. Null while debugDraw is off. */
181
184
  debugLines(): Float32Array | null;
185
+ /**
186
+ * Only the selected node's colliders (meters — the caller scales to pixels).
187
+ * Rapier draws the whole world or nothing, so a chosen few are outlined from
188
+ * the same shape data the solver holds. 2D colliders are rect/circle/capsule,
189
+ * which is the whole switch.
190
+ */
191
+ private scopedLines;
182
192
  /** Create newly-arrived joints; tear down departed ones. */
183
193
  private syncJoints;
184
194
  /**
@@ -249,6 +259,17 @@ interface CreateGame2DOptions {
249
259
  * to development, e.g. `debug: import.meta.env.VITE_INCANTO_DEBUG === '1'`.
250
260
  */
251
261
  debug?: boolean;
262
+ /**
263
+ * LIVE EDITING: the ☰ debug menu gains "edit this scene", which turns this
264
+ * page into the scene editor — same window, no reload — and the editor's play
265
+ * button turns it back into the game.
266
+ *
267
+ * DEFAULTS TO `debug`, costs nothing until clicked (the editor is a lazy
268
+ * chunk), and is never reachable without it — the ☰ menu is its only
269
+ * entrance. `false` opts out; `{ open }` loads the editor yourself (apps that
270
+ * alias incanto to source); `{ save }` makes its save button write your file.
271
+ */
272
+ editor?: boolean | EditorSwitchOptions;
252
273
  /** @internal Test seam — replaces `document` for the debug overlay. */
253
274
  _debugDoc?: {
254
275
  createElement(tag: string): HTMLElement;
@@ -272,10 +293,22 @@ interface GameRenderer {
272
293
  interface Game2D {
273
294
  engine: Engine;
274
295
  scene: Scene$1;
296
+ /**
297
+ * The AUTHORED scene this game booted from — the JSON, not the live tree.
298
+ * A running tree has moved; the editor gets what was written down.
299
+ */
300
+ sourceJson: SceneJson;
275
301
  renderer: GameRenderer;
276
302
  physics: Physics2D | null;
277
303
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
278
304
  stats(): GameStats;
305
+ /**
306
+ * Turn this page into the scene editor — what the ☰ debug menu's "edit this
307
+ * scene" calls. The game is disposed on the way out and rebooted when the
308
+ * editor hands it back, so THIS handle is dead afterwards. `null` when the
309
+ * game was booted with `editor: false`.
310
+ */
311
+ openEditor: (() => Promise<void>) | null;
279
312
  /** Tear the whole game down (renderer, loop, scene, listeners). */
280
313
  dispose(): void;
281
314
  }
@@ -724,8 +757,8 @@ declare class Renderer2D {
724
757
  /** The world view used by the LAST render (center/zoom/canvas size). */
725
758
  view(): View2D;
726
759
  /**
727
- * GPU counters for the LAST rendered frame (world + UI passes combined),
728
- * straight from three's per-frame-maintained `renderer.info`.
760
+ * GPU counters for the LAST rendered frame world + UI passes combined (see
761
+ * the `info.reset()` in the render pass), plus GPU memory.
729
762
  */
730
763
  stats(): RendererStats;
731
764
  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-CbuLWorm.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-CCVTrKOd.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