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
@@ -1,5 +1,5 @@
1
1
  import { f as Node } from "./loader-BZqOKfI2.js";
2
- import { t as registerCoreNodes } from "./register-nObreUQR.js";
2
+ import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
@@ -115,7 +115,27 @@ var Node2D = class extends Node {
115
115
  * `{shape:'capsule', radius, height}` — never child shape nodes.
116
116
  */
117
117
  var PhysicsBody2D = class extends Node2D {
118
- static props = { collider: { default: {} } };
118
+ static props = { collider: {
119
+ default: {},
120
+ variants: {
121
+ tag: "shape",
122
+ byTag: {
123
+ rect: {
124
+ shape: "rect",
125
+ size: [32, 32]
126
+ },
127
+ circle: {
128
+ shape: "circle",
129
+ radius: 16
130
+ },
131
+ capsule: {
132
+ shape: "capsule",
133
+ radius: 12,
134
+ height: 32
135
+ }
136
+ }
137
+ }
138
+ } };
119
139
  /** The unified collision model: every collider participant can emit these. */
120
140
  static signals = ["triggerEnter", "triggerExit"];
121
141
  _collider = {};
@@ -1,5 +1,5 @@
1
1
  import { f as Node, t as buildNodeJson, y as Signal } from "./loader-BZqOKfI2.js";
2
- import { t as registerCoreNodes } from "./register-nObreUQR.js";
2
+ import { t as registerCoreNodes } from "./register-BTg0EM7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
5
5
  import { l as registerNode } from "./registry-BVJ2HbCn.js";
@@ -0,0 +1,240 @@
1
+ //#region src/core/editor-switch.ts
2
+ /**
3
+ * Cover the page while it changes what it is.
4
+ *
5
+ * A mode switch tears down a whole game and builds a whole IDE in its place;
6
+ * between those two the window shows a dead canvas, a flash of white body, and
7
+ * then a fully-formed editor. Every one of those frames is honest and none of
8
+ * them is pleasant. A veil makes the crossing read as one movement: fade to the
9
+ * editor's own background, do the work behind it, fade back.
10
+ *
11
+ * Deliberately CSS-only and dependency-free — it has to work in a game page
12
+ * whose styles we know nothing about, so it sits at the top of the stacking
13
+ * order, eats pointer events (nobody clicks a UI that is being replaced), and
14
+ * removes itself completely. In a headless boot there is no document and this
15
+ * is a pair of no-ops.
16
+ */
17
+ const VEIL_MS = 160;
18
+ function raiseVeil(label = "", background = "#0b0d14") {
19
+ if (typeof document === "undefined") return { lift: () => {} };
20
+ const el = document.createElement("div");
21
+ el.dataset.incantoSwitch = "";
22
+ el.style.cssText = `position:fixed;inset:0;z-index:2147483646;background:${background};display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity ${VEIL_MS}ms ease;pointer-events:auto;`;
23
+ if (label) {
24
+ const text = document.createElement("div");
25
+ text.textContent = label;
26
+ text.style.cssText = "font:13px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#5d6680;letter-spacing:0.04em;";
27
+ el.appendChild(text);
28
+ }
29
+ document.body.appendChild(el);
30
+ requestAnimationFrame(() => {
31
+ el.style.opacity = "1";
32
+ });
33
+ let lifted = false;
34
+ return { lift() {
35
+ if (lifted) return;
36
+ lifted = true;
37
+ requestAnimationFrame(() => {
38
+ el.style.opacity = "0";
39
+ setTimeout(() => el.remove(), 200);
40
+ });
41
+ } };
42
+ }
43
+ /** Hold the veil up for at least one paint so the fade is visible at all. */
44
+ async function settle(ms = VEIL_MS) {
45
+ if (typeof requestAnimationFrame !== "function") return;
46
+ await new Promise((resolve) => setTimeout(resolve, ms));
47
+ }
48
+ /**
49
+ * The `/api/library` route `incanto/vite`'s `incantoLibrary()` (and the
50
+ * `incanto-editor` server) serve. Used when a game passes `library: true`.
51
+ */
52
+ function devServerLibrary(token) {
53
+ return async (query) => {
54
+ const params = new URLSearchParams({
55
+ tags: query.tags.join(","),
56
+ page: String(query.page ?? 1),
57
+ limit: String(query.limit ?? 24)
58
+ });
59
+ if (query.keyword?.trim()) params.set("keyword", query.keyword.trim());
60
+ const res = await fetch(`/api/library?${params}`, { headers: token ? { "x-incanto-v8-token": token } : {} });
61
+ if (res.status === 401) {
62
+ const body = await res.json().catch(() => ({}));
63
+ const error = new Error(body.error ?? "the asset library needs a Verse8 access token");
64
+ error.needsToken = true;
65
+ throw error;
66
+ }
67
+ if (!res.ok) throw new Error(`asset library: HTTP ${res.status}`);
68
+ return await res.json();
69
+ };
70
+ }
71
+ /**
72
+ * Does the page's own dev server serve the project's scenes?
73
+ *
74
+ * `incantoScenes()` from `incanto/vite` (the plugin that already validates
75
+ * scenes on save; `incanto-new` puts it in every scaffolded project) answers
76
+ * `GET /api/scenes`. When it does, the
77
+ * editor a game switches into is a PROJECT editor: it can list, open, create
78
+ * and save every scene in the project, and play whichever one you loaded.
79
+ * When it does not (a built game, someone else's server), the editor still
80
+ * edits the scene the game is running, exactly as before.
81
+ */
82
+ async function projectScenes() {
83
+ try {
84
+ const res = await fetch("/api/scenes");
85
+ if (!res.ok) return null;
86
+ const body = await res.json();
87
+ return Array.isArray(body) ? body : null;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ /** What to say when the dev server has no idea what scenes this project has. */
93
+ const NO_SCENE_ROUTES = "this dev server does not serve the project's scenes yet. Add incantoScenes() to vite.config.ts and restart:\n\n import { incantoScenes } from 'incanto/vite';\n export default defineConfig({ plugins: [incantoScenes()] });\n\nIt also validates every scene you save, before the page reloads into a broken one.";
94
+ async function readProjectScene(file) {
95
+ const res = await fetch(`/api/scene?file=${encodeURIComponent(file)}`);
96
+ if (res.ok) return await res.json();
97
+ const body = await res.json().catch(() => null);
98
+ throw new Error(body?.error ?? `could not open ${file}: HTTP ${res.status}`);
99
+ }
100
+ /**
101
+ * The default opener: pull `incanto/editor` in on demand and mount it.
102
+ *
103
+ * It is a dynamic import so the editor — an IDE's worth of UI — never enters a
104
+ * production bundle. A build that cannot resolve it fails HERE, with the two
105
+ * ways to fix it, rather than silently offering a menu item that does nothing.
106
+ */
107
+ async function openBundledEditor(opts) {
108
+ let mod;
109
+ try {
110
+ mod = await import("incanto/editor");
111
+ } catch (error) {
112
+ throw new Error(`createGame3D editor: could not load 'incanto/editor'. If this app aliases incanto to the engine SOURCE (the examples and playground do), add an alias for 'incanto/editor' → packages/editor/src/main.ts — or pass editor: { open } and load it yourself. (${error instanceof Error ? error.message : String(error)})`);
113
+ }
114
+ let handle = null;
115
+ const project = await projectScenes();
116
+ const format = mod.formatSceneJson ?? ((json) => JSON.stringify(json, null, 2));
117
+ const writeProjectScene = async (file, json) => {
118
+ const res = await fetch(`/api/scene?file=${encodeURIComponent(file)}`, {
119
+ method: "PUT",
120
+ body: format(json)
121
+ });
122
+ if (!res.ok) throw new Error(`could not save ${file}: HTTP ${res.status} ${await res.text()}`);
123
+ };
124
+ handle = await mod.mountEditor({
125
+ ...opts.camera ? { camera: opts.camera } : {},
126
+ api: {
127
+ meta: async () => ({
128
+ version: "live",
129
+ mode: "project",
130
+ root: "",
131
+ input: null,
132
+ output: null
133
+ }),
134
+ loadScene: async (file) => file ? await readProjectScene(file) : opts.scene,
135
+ ...opts.save || project ? { saveScene: async (json, file) => {
136
+ if (file) return writeProjectScene(file, json);
137
+ await opts.save?.(json);
138
+ } } : {},
139
+ scenes: async () => {
140
+ const found = await projectScenes();
141
+ if (!found) throw new Error(NO_SCENE_ROUTES);
142
+ return found;
143
+ },
144
+ createScene: async (path) => {
145
+ const res = await fetch("/api/scenes", {
146
+ method: "POST",
147
+ body: JSON.stringify({ path })
148
+ });
149
+ if (res.status === 404) throw new Error(NO_SCENE_ROUTES);
150
+ if (!res.ok) throw new Error(`could not create ${path}: ${await res.text()}`);
151
+ return await res.json();
152
+ },
153
+ ...opts.library ? { library: opts.library } : {}
154
+ },
155
+ onPlay: (choice, working) => {
156
+ if (choice !== "debug") return void 0;
157
+ const camera = handle?.camera();
158
+ return crossFade(async () => {
159
+ handle?.dispose();
160
+ handle = null;
161
+ await opts.play(working, camera);
162
+ }, "starting the game…");
163
+ },
164
+ ...opts.exit ? { onExit: () => crossFade(async () => {
165
+ handle?.dispose();
166
+ handle = null;
167
+ await opts.exit?.();
168
+ }, "back to the game…") } : {}
169
+ });
170
+ }
171
+ /**
172
+ * Run a mode change behind the veil: cover, swap, reveal.
173
+ *
174
+ * The reveal waits for the new page to have PAINTED (two frames — one for the
175
+ * layout the swap just produced, one for it to reach the screen), otherwise the
176
+ * fade-out races the boot and shows the very flash it exists to hide.
177
+ */
178
+ async function crossFade(swap, label = "") {
179
+ const veil = raiseVeil(label);
180
+ await settle();
181
+ try {
182
+ await swap();
183
+ } finally {
184
+ await settle(0);
185
+ veil.lift();
186
+ }
187
+ }
188
+ /**
189
+ * The camera the player is actually looking through, as a pose.
190
+ *
191
+ * Reads the renderer's LAST rendered camera rather than the scene's Camera3D:
192
+ * a third-person rig, a cutscene camera and a debug free-fly all end up in the
193
+ * same place, and it is the view on screen — the thing continuity is about.
194
+ */
195
+ function poseFromRenderer(renderer, fallbackDistance = 8) {
196
+ const basis = renderer.cameraBasis;
197
+ const posOf = renderer.lastCameraPosition;
198
+ if (typeof basis !== "function" || typeof posOf !== "function") return void 0;
199
+ const eye = posOf.call(renderer);
200
+ if (!eye) return void 0;
201
+ const { forward } = basis.call(renderer);
202
+ return {
203
+ position: [
204
+ eye.x,
205
+ eye.y,
206
+ eye.z
207
+ ],
208
+ target: [
209
+ eye.x + forward.x * fallbackDistance,
210
+ eye.y + forward.y * fallbackDistance,
211
+ eye.z + forward.z * fallbackDistance
212
+ ]
213
+ };
214
+ }
215
+ //#endregion
216
+ //#region src/core/teardown.ts
217
+ function teardown(steps, onError = defaultReport) {
218
+ const failures = [];
219
+ for (const step of steps) {
220
+ if (!step) continue;
221
+ const [what, run] = step;
222
+ try {
223
+ run();
224
+ } catch (error) {
225
+ failures.push({
226
+ what,
227
+ error
228
+ });
229
+ try {
230
+ onError(what, error);
231
+ } catch {}
232
+ }
233
+ }
234
+ return { failures };
235
+ }
236
+ function defaultReport(what, error) {
237
+ console.error(`[incanto] teardown: ${what} failed (continuing)`, error);
238
+ }
239
+ //#endregion
240
+ export { poseFromRenderer as a, openBundledEditor as i, crossFade as n, devServerLibrary as r, teardown as t };
package/dist/test.d.ts CHANGED
@@ -1,9 +1,41 @@
1
1
  import { c as JsonValue, i as SceneJson } from "./schema-CcoWb32N.js";
2
- import { $ as Node, T as LogEntry, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-CPibUfnH.js";
3
- import { t as LoadSceneOptions } from "./loader-DILt9PGC.js";
2
+ import { $ as Node, T as LogEntry, n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAc0erXF.js";
3
+ import { t as LoadSceneOptions } from "./loader-B242FF6N.js";
4
4
  import { i as auditScene, t as IncantoError } from "./errors-1dXlIwoR.js";
5
5
 
6
6
  //#region src/test/index.d.ts
7
+ /** One thing that is not standing where it should be. */
8
+ interface GroundingIssue {
9
+ /** Scene path, e.g. `/World/Footbridge/PostL0`. */
10
+ path: string;
11
+ /** Metres between the node's underside and the ground: + floats, − buried. */
12
+ gap: number;
13
+ }
14
+ interface GroundingOptions {
15
+ /** How far a foot may sit off the ground before it is a bug (default 0.1 m). */
16
+ tolerance?: number;
17
+ /** Node types to check (default: the ones that read as props on a map). */
18
+ types?: readonly string[];
19
+ }
20
+ /**
21
+ * Every prop that is floating or buried.
22
+ *
23
+ * THE most repeated mistake in scene authoring, by humans and agents alike, is
24
+ * a hand-computed Y: correct when it is written and wrong the moment anything
25
+ * reshapes the ground under it — a river carving its bed, a new seed, a
26
+ * generator that mirrors a carve slightly differently. This is the check that
27
+ * catches it, and `snapToGround` is the fix that stops it happening.
28
+ *
29
+ * Anything explicitly marked `snapToGround: false` is skipped: that is the
30
+ * author saying "yes, this hangs in the air", which a bridge deck, a lantern
31
+ * and a cloud all legitimately do.
32
+ *
33
+ * ```ts
34
+ * const off = findFloatingProps(gameJson);
35
+ * ok(`nothing floats (${off.length})`, off.length === 0);
36
+ * ```
37
+ */
38
+ declare function findFloatingProps(json: unknown, opts?: GroundingOptions): GroundingIssue[];
7
39
  declare function registerAllNodes(): void;
8
40
  interface NodeCapture {
9
41
  /** Absolute path ('/Root/Player') — stable, grep-able. */
@@ -149,4 +181,4 @@ interface PlaySession {
149
181
  */
150
182
  declare function createPlaySession(json: unknown, opts?: PlaySessionOptions): Promise<PlaySession>;
151
183
  //#endregion
152
- export { NodeCapture, PlaySession, PlaySessionOptions, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, auditScene, captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
184
+ export { GroundingIssue, GroundingOptions, NodeCapture, PlaySession, PlaySessionOptions, RunContext, RunFailure, RunResult, RunScriptOptions, SceneCapture, ScriptStep, ValidateSceneOptions, ValidationResult, auditScene, captureScene, createPlaySession, describeCapture, findFloatingProps, registerAllNodes, runScript, validateScene };
package/dist/test.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { _ as registerBehavior, n as loadScene } from "./loader-BZqOKfI2.js";
2
- import { u as Engine } from "./register-nObreUQR.js";
2
+ import { u as Engine } from "./register-BTg0EM7s.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as auditScene } from "./audit-C6rMyict.js";
5
5
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
6
6
  import { i as getNodeSchema, s as mergeStaticProps } from "./registry-BVJ2HbCn.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-DEG-TP7D.js";
8
- import { t as registerNodes2D } from "./register-CvpSUU3O.js";
9
- import { t as registerNodes3D } from "./register-6R75AC7-.js";
10
- import { t as registerNodesNet } from "./register-BFFE1Mh1.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
+ import { t as registerNodes2D } from "./register-DWcWq4QG.js";
9
+ import { t as registerNodes3D } from "./register-DJ0SByQg.js";
10
+ import { t as registerNodesNet } from "./register-MelqEdza.js";
11
11
  //#region src/test/index.ts
12
12
  /**
13
13
  * incanto/test — the browserless verification harness.
@@ -25,6 +25,100 @@ import { t as registerNodesNet } from "./register-BFFE1Mh1.js";
25
25
  * Deterministic by construction: fixed timestep, seeded `engine.rng`,
26
26
  * no requestAnimationFrame.
27
27
  */
28
+ /** Node types whose whole job is to sit on the ground. Characters, water,
29
+ * clouds, birds and lights are all legitimately off it. */
30
+ const GROUNDED_TYPES = ["MeshInstance3D", "InstancedMesh3D"];
31
+ /** Under one of these, the BODY owns where the thing stands — a character's
32
+ * skin hangs at whatever offset its rig wants, and is not a floating prop. */
33
+ const BODY_OWNED = new Set([
34
+ "CharacterBody3D",
35
+ "RigidBody3D",
36
+ "StaticBody3D",
37
+ "AreaBody3D"
38
+ ]);
39
+ /**
40
+ * Every prop that is floating or buried.
41
+ *
42
+ * THE most repeated mistake in scene authoring, by humans and agents alike, is
43
+ * a hand-computed Y: correct when it is written and wrong the moment anything
44
+ * reshapes the ground under it — a river carving its bed, a new seed, a
45
+ * generator that mirrors a carve slightly differently. This is the check that
46
+ * catches it, and `snapToGround` is the fix that stops it happening.
47
+ *
48
+ * Anything explicitly marked `snapToGround: false` is skipped: that is the
49
+ * author saying "yes, this hangs in the air", which a bridge deck, a lantern
50
+ * and a cloud all legitimately do.
51
+ *
52
+ * ```ts
53
+ * const off = findFloatingProps(gameJson);
54
+ * ok(`nothing floats (${off.length})`, off.length === 0);
55
+ * ```
56
+ */
57
+ function findFloatingProps(json, opts = {}) {
58
+ registerAllNodes();
59
+ const tolerance = opts.tolerance ?? .1;
60
+ const types = new Set(opts.types ?? GROUNDED_TYPES);
61
+ let scene;
62
+ try {
63
+ scene = loadScene(json);
64
+ } catch {
65
+ return [];
66
+ }
67
+ let ground = null;
68
+ const findGround = (node) => {
69
+ if (!ground && node.constructor.typeName === "Terrain3D" && typeof node.heightAt === "function") ground = node;
70
+ for (const child of node.children) findGround(child);
71
+ };
72
+ findGround(scene.root);
73
+ if (!ground) return [];
74
+ const out = [];
75
+ const walk = (node, path, ox, oy, oz, yaw, bodyOwned) => {
76
+ const n = node;
77
+ let wx = ox;
78
+ let wy = oy;
79
+ let wz = oz;
80
+ let ownYaw = yaw;
81
+ if (Array.isArray(n.position)) {
82
+ const c = Math.cos(yaw);
83
+ const s = Math.sin(yaw);
84
+ const lx = n.position[0] ?? 0;
85
+ const lz = n.position[2] ?? 0;
86
+ wx = ox + lx * c + lz * s;
87
+ wz = oz - lx * s + lz * c;
88
+ wy = oy + (n.position[1] ?? 0);
89
+ ownYaw = yaw + (n.rotation?.[1] ?? 0) * Math.PI / 180;
90
+ }
91
+ const type = node.constructor.typeName ?? "";
92
+ const soil = ground;
93
+ if (types.has(type) && n.visible !== false && n.snapToGround !== false && !bodyOwned) {
94
+ const rows = node.transforms;
95
+ if (Array.isArray(rows) && rows.length > 0) {
96
+ let worst = null;
97
+ for (const row of rows) {
98
+ const scale = row[4] ?? 1;
99
+ const half = (n.size?.[1] ?? 0) * scale / 2;
100
+ const gap = wy + (row[1] ?? 0) - half - soil.heightAt(wx + (row[0] ?? 0), wz + (row[2] ?? 0));
101
+ if (Math.abs(gap) > tolerance && (!worst || Math.abs(gap) > Math.abs(worst.gap))) worst = {
102
+ path,
103
+ gap: Math.round(gap * 100) / 100
104
+ };
105
+ }
106
+ if (worst) out.push(worst);
107
+ } else {
108
+ const half = (n.size?.[1] ?? 0) / 2;
109
+ const gap = wy - half - soil.heightAt(wx, wz);
110
+ if (Math.abs(gap) > tolerance) out.push({
111
+ path,
112
+ gap: Math.round(gap * 100) / 100
113
+ });
114
+ }
115
+ }
116
+ const childBodyOwned = bodyOwned || BODY_OWNED.has(type);
117
+ for (const child of node.children) walk(child, `${path}/${child.name}`, wx, wy, wz, ownYaw, childBodyOwned);
118
+ };
119
+ walk(scene.root, `/${scene.root.name}`, 0, 0, 0, 0, false);
120
+ return out;
121
+ }
28
122
  function registerAllNodes() {
29
123
  registerNodes2D();
30
124
  registerNodes3D();
@@ -124,10 +218,10 @@ async function runScript(json, opts) {
124
218
  engine.setScene(scene);
125
219
  const physics = opts.physics ?? "auto";
126
220
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
127
- const { enablePhysics2D } = await import("./physics-2d-DiVFFlH3.js").then((n) => n.r);
221
+ const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
128
222
  await enablePhysics2D(engine);
129
223
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
130
- const { enablePhysics3D } = await import("./physics-3d--y5clE2j.js").then((n) => n.r);
224
+ const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
131
225
  await enablePhysics3D(engine);
132
226
  }
133
227
  const failures = [];
@@ -229,10 +323,10 @@ async function createPlaySession(json, opts = {}) {
229
323
  engine.setScene(scene);
230
324
  const physics = opts.physics ?? "auto";
231
325
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
232
- const { enablePhysics2D } = await import("./physics-2d-DiVFFlH3.js").then((n) => n.r);
326
+ const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
233
327
  await enablePhysics2D(engine);
234
328
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
235
- const { enablePhysics3D } = await import("./physics-3d--y5clE2j.js").then((n) => n.r);
329
+ const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
236
330
  await enablePhysics3D(engine);
237
331
  }
238
332
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
@@ -267,4 +361,4 @@ async function createPlaySession(json, opts = {}) {
267
361
  };
268
362
  }
269
363
  //#endregion
270
- export { auditScene, captureScene, createPlaySession, describeCapture, registerAllNodes, runScript, validateScene };
364
+ export { auditScene, captureScene, createPlaySession, describeCapture, findFloatingProps, registerAllNodes, runScript, validateScene };
package/dist/vite.d.ts CHANGED
@@ -17,10 +17,148 @@ interface HotUpdateContext {
17
17
  interface IncantoScenesOptions {
18
18
  /** Hard-fail on unregistered behaviors too (default: structure-only). */
19
19
  strictBehaviors?: boolean;
20
+ /**
21
+ * Where scenes are found and written. Default: vite's `root` (your project).
22
+ * Nothing outside it is readable or writable, whatever a request asks for.
23
+ */
24
+ root?: string;
20
25
  }
21
26
  declare function incantoScenes(opts?: IncantoScenesOptions): {
22
27
  name: string;
23
28
  handleHotUpdate(ctx: HotUpdateContext): Promise<undefined | never[]>;
29
+ configureServer(server: SceneDevServer): void;
24
30
  };
31
+ /**
32
+ * `GET /api/scenes` → every `*.scene.json` under the project root
33
+ * `POST /api/scenes` `{path}` → create a minimal scene there
34
+ * `GET /api/scene?file=<rel>` → that scene's JSON
35
+ * `PUT /api/scene?file=<rel>` → write it back (bytes verbatim)
36
+ *
37
+ * These are the routes `incanto-editor` serves, on YOUR dev server, so the
38
+ * editor a game switches into is a project editor rather than a single-file
39
+ * one: it can list, open, create and save any scene in the project, and play
40
+ * whichever one you loaded.
41
+ *
42
+ * Everything is bounded to the root and to `*.scene.json`. A dev server is
43
+ * still a server: `?file=../../.ssh/id_rsa` has to be a 400, not a read.
44
+ */
45
+ interface SceneDevServer {
46
+ config?: {
47
+ root?: string;
48
+ };
49
+ middlewares: {
50
+ use(path: string, handler: (req: ApiReq, res: LibraryRes) => void): void;
51
+ };
52
+ }
53
+ interface ApiReq extends LibraryReq {
54
+ on(event: "data" | "end" | "error", cb: (chunk?: unknown) => void): void;
55
+ }
56
+ /**
57
+ * One JSON file in the project, and whether it is a scene.
58
+ *
59
+ * A file browser that only shows `*.scene.json` cannot show you the scene you
60
+ * saved as `level.json` — and, worse, gives no reason for its absence. So the
61
+ * list is every `.json` under the project (a scene is JSON by definition, so
62
+ * nothing else can be one), and `scene` says which of them actually are.
63
+ */
64
+ interface ProjectFileEntry {
65
+ rel: string;
66
+ abs: string;
67
+ size: number;
68
+ modified: number;
69
+ /** Present only when the file IS a scene — read from the file, not the name. */
70
+ scene?: {
71
+ name: string;
72
+ dimension?: "2d" | "3d";
73
+ nodes: number;
74
+ };
75
+ /** Why it is not one, in a form a person can act on. */
76
+ notScene?: string;
77
+ }
78
+ /**
79
+ * Is this JSON an incanto scene? The file itself answers.
80
+ *
81
+ * A scene declares what it is — `{"format": 1, "type": "scene", "name", "root"}`
82
+ * — and those are exactly the four keys the loader demands before it will build
83
+ * anything (`validateHeader`). So "is this a scene" is not a guess from the file
84
+ * name; it is a fact readable from the first object in the file. That is what
85
+ * lets the browser mark a row ✓ instead of hoping.
86
+ */
87
+ declare function sceneFacts(json: unknown): {
88
+ ok: true;
89
+ name: string;
90
+ dimension?: "2d" | "3d";
91
+ nodes: number;
92
+ } | {
93
+ ok: false;
94
+ why: string;
95
+ };
96
+ /** @internal Exported for tests. Root-bounded walk, deepest 6 levels. */
97
+ declare function discoverScenes(root: string): ProjectFileEntry[];
98
+ /**
99
+ * @internal The one place a request's path becomes a file path.
100
+ *
101
+ * `.json` under the project, nothing else — the file browser offers every JSON
102
+ * and the loader decides, but a dev server still must not be a way to read
103
+ * `.env` or somebody's key file. Content is gated separately: a JSON that is
104
+ * not a scene is never sent to the page, only the reason it is not one.
105
+ */
106
+ declare function resolveSceneFile(root: string, rel: unknown): string | null;
107
+ /** @internal Exported for tests — `GET`/`POST /api/scenes`. */
108
+ declare function serveSceneList(req: ApiReq, res: LibraryRes, root: string): Promise<void>;
109
+ /** @internal Exported for tests — `GET`/`PUT /api/scene?file=`. */
110
+ declare function serveSceneFile(req: ApiReq, res: LibraryRes, root: string): Promise<void>;
111
+ interface DevServer {
112
+ middlewares: {
113
+ use(path: string, handler: (req: LibraryReq, res: LibraryRes) => void): void;
114
+ };
115
+ }
116
+ interface LibraryReq {
117
+ url?: string | undefined;
118
+ method?: string | undefined;
119
+ headers: Record<string, string | string[] | undefined>;
120
+ }
121
+ interface LibraryRes {
122
+ statusCode: number;
123
+ setHeader(name: string, value: string): void;
124
+ end(body?: string): void;
125
+ }
126
+ interface IncantoLibraryOptions {
127
+ /**
128
+ * Verse8 access token. Defaults to INCANTO_V8_TOKEN / V8_ACCESS_TOKEN.
129
+ * Read at request time so a token exported after the server started works.
130
+ */
131
+ token?: string;
132
+ /** Upstream catalog (default: the agent8 backend). */
133
+ url?: string;
134
+ }
135
+ /**
136
+ * Serve the agent8 asset catalog at `/api/library` on the dev server.
137
+ *
138
+ * The catalog wants a bearer token and does not CORS-allow localhost, so a page
139
+ * cannot call it — but the dev server can, and it is already running. This is
140
+ * the same proxy `incanto-editor` ships, so the editor's 📚 works identically
141
+ * whether it opened as a page or over a running game.
142
+ *
143
+ * DEV ONLY (vite `configureServer`): nothing about it reaches a build, and the
144
+ * token stays in the terminal that started the server.
145
+ *
146
+ * ```ts
147
+ * // vite.config.ts
148
+ * import { incantoLibrary, incantoScenes } from 'incanto/vite';
149
+ * export default defineConfig({ plugins: [incantoScenes(), incantoLibrary()] });
150
+ * ```
151
+ * ```ts
152
+ * // main.ts — the editor then offers the library
153
+ * createGame3D({ ..., debug: DEV, editor: { library: true } });
154
+ * ```
155
+ */
156
+ declare function incantoLibrary(opts?: IncantoLibraryOptions): {
157
+ name: string;
158
+ apply: "serve";
159
+ configureServer(server: DevServer): void;
160
+ };
161
+ /** @internal Exported for tests — the whole request/response behaviour. */
162
+ declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
25
163
  //#endregion
26
- export { IncantoScenesOptions, incantoScenes };
164
+ export { IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, resolveSceneFile, sceneFacts, serveLibrary, serveSceneFile, serveSceneList };