incanto 0.29.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 (38) hide show
  1. package/bin/incanto-editor.mjs +101 -28
  2. package/dist/2d.d.ts +16 -1
  3. package/dist/2d.js +2 -2
  4. package/dist/3d.d.ts +34 -1
  5. package/dist/3d.js +4 -4
  6. package/dist/{create-game-CbuLWorm.js → create-game-5z_QVtLx.js} +13 -9
  7. package/dist/{create-game-CNKXGfpr.js → create-game-DuBTv2zI.js} +14 -10
  8. package/dist/debug-draw-BM3DsvtT.js +18 -0
  9. package/dist/debug.js +1 -1
  10. package/dist/editor.js +1346 -1231
  11. package/dist/{gameplay-L05WgWd1.js → gameplay-Cfr6aFZ1.js} +12 -3
  12. package/dist/gameplay.js +1 -1
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +1 -1
  15. package/dist/{physics-2d-CCVTrKOd.js → physics-2d-3kOQCtgd.js} +26 -1
  16. package/dist/{physics-3d-BZZLtwJu.js → physics-3d-CeRH-Ff_.js} +38 -3
  17. package/dist/react.js +1 -1
  18. package/dist/{register-C44aSduO.js → register-DJ0SByQg.js} +1 -1
  19. package/dist/{editor-switch-CAKlJMIY.js → teardown-ByzfDPyu.js} +83 -4
  20. package/dist/test.js +6 -6
  21. package/dist/vite.d.ts +87 -1
  22. package/dist/vite.js +262 -3
  23. package/editor/assets/{agent8-DEVkEa3d.js → agent8-CAp0i5qn.js} +1 -1
  24. package/editor/assets/{debug-zGAtpDF0.js → debug-BoEYfbqK.js} +1 -1
  25. package/editor/assets/index-BO6WU8by.js +10696 -0
  26. package/editor/index.html +1 -1
  27. package/package.json +1 -1
  28. package/skills/incanto-editor.md +61 -8
  29. package/skills/incanto-physics-and-input.md +15 -0
  30. package/skills/incanto-verifying-your-game.md +6 -0
  31. package/templates-app/beacon-isle-3d/package.json +1 -1
  32. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  33. package/templates-app/tps-3d/package.json +1 -1
  34. package/templates-app/tps-3d/vite.config.ts +7 -0
  35. package/templates-app/village-quest-3d/package.json +1 -1
  36. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  37. package/dist/debug-draw-CZmOYjL2.js +0 -13
  38. package/editor/assets/index-Bx4UtWYY.js +0 -10586
@@ -2367,6 +2367,15 @@ var Node3D = class Node3D extends Node {
2367
2367
  this.position[2] ?? 0
2368
2368
  ];
2369
2369
  }
2370
+ /**
2371
+ * @internal The backing object IF it has been built — never builds one.
2372
+ *
2373
+ * `_ensureObject3D()` is the wrong call during teardown: it allocates a mesh
2374
+ * for a node that never rendered, purely so the next line can dispose it.
2375
+ */
2376
+ get _builtObject3D() {
2377
+ return this._object3D;
2378
+ }
2370
2379
  /** @internal The backing three object (lazily created). */
2371
2380
  _ensureObject3D() {
2372
2381
  if (!this._object3D) {
@@ -3341,9 +3350,9 @@ var Water3D = class Water3D extends Node3D {
3341
3350
  } : null;
3342
3351
  }
3343
3352
  free() {
3344
- const mesh = this._ensureObject3D();
3345
- mesh.geometry?.dispose();
3346
- mesh.material?.dispose?.();
3353
+ const mesh = this._builtObject3D;
3354
+ mesh?.geometry?.dispose();
3355
+ (mesh?.material)?.dispose?.();
3347
3356
  this.cubeTarget?.dispose();
3348
3357
  this.cubeTarget = null;
3349
3358
  this.cubeCamera = null;
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-L05WgWd1.js";
1
+ import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-Cfr6aFZ1.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/index.d.ts CHANGED
@@ -47,6 +47,15 @@ declare class WebAudioMusicBackend implements MusicBackend {
47
47
  */
48
48
  interface DebugLineSource {
49
49
  readonly dimension: "2d" | "3d";
50
+ /**
51
+ * The engine whose world these lines describe.
52
+ *
53
+ * A page can hold more than one at a time — the editor runs its edit engine
54
+ * and a preview engine side by side — and a renderer must draw ITS world, not
55
+ * whichever source happens to be first in a module-level Set. Without this,
56
+ * one leaked physics instance paints its frozen colliders over everybody.
57
+ */
58
+ readonly engine: object;
50
59
  /** Toggle at runtime: `physics.debugDraw = true`. */
51
60
  debugDraw: boolean;
52
61
  /**
package/dist/index.js CHANGED
@@ -295,6 +295,6 @@ function newUid() {
295
295
  //#endregion
296
296
  //#region src/index.ts
297
297
  /** Engine version. Kept in sync with package.json by the release pipeline. */
298
- const VERSION = "0.29.0";
298
+ const VERSION = "0.30.0";
299
299
  //#endregion
300
300
  export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiText, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, startRecording, synthSfx };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-DWcWq4QG.js";
4
- import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
4
+ import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
5
5
  //#region src/2d/physics/physics-2d.ts
6
6
  var physics_2d_exports = /* @__PURE__ */ __exportAll({
7
7
  Physics2D: () => Physics2D,
@@ -46,6 +46,7 @@ var Physics2D = class {
46
46
  this.R = R;
47
47
  this.engine = engine;
48
48
  this.optsGravity = opts?.gravity;
49
+ this.simulate = opts?.simulate ?? true;
49
50
  this.lastScene = engine.scene;
50
51
  const declared = engine.scene?.physics?.gravity;
51
52
  const g = opts?.gravity ?? declared ?? [0, 980];
@@ -64,8 +65,15 @@ var Physics2D = class {
64
65
  this.syncBodies();
65
66
  }
66
67
  /** @internal Driven by engine.fixedUpdated. */
68
+ /** Whether the solver runs at all. False = collider outlines only. */
69
+ simulate;
67
70
  step(dt) {
68
71
  this.lastDt = dt;
72
+ if (!this.simulate) {
73
+ this.syncBodies();
74
+ this.poseFromTree();
75
+ return;
76
+ }
69
77
  if (this.engine.scene !== this.lastScene) {
70
78
  this.lastScene = this.engine.scene;
71
79
  if (!this.optsGravity) {
@@ -294,6 +302,23 @@ var Physics2D = class {
294
302
  jointSet = /* @__PURE__ */ new Set();
295
303
  joints = /* @__PURE__ */ new Map();
296
304
  bodySet = /* @__PURE__ */ new Set();
305
+ /**
306
+ * Move every body to its node's current transform — outline mode only.
307
+ *
308
+ * Computed exactly as `ensureEntry` computes a body's first pose, so an
309
+ * outline drawn in the editor is the outline the running game draws.
310
+ */
311
+ poseFromTree() {
312
+ for (const [node, e] of this.entries) {
313
+ const [wx, wy] = worldPosition2D(node);
314
+ e.body.setTranslation({
315
+ x: wx / SCALE,
316
+ y: wy / SCALE
317
+ }, false);
318
+ e.body.setRotation(node.rotation * DEG, false);
319
+ }
320
+ this.world.propagateModifiedBodyPositionsToColliders();
321
+ }
297
322
  syncBodies() {
298
323
  const scene = this.engine.scene;
299
324
  if (!scene) return;
@@ -1,8 +1,8 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
- import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-L05WgWd1.js";
4
- import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
5
- import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-C44aSduO.js";
3
+ import { F as PhysicsBody3D, I as RigidBody3D, N as Area3D, P as CharacterBody3D, R as Node3D, z as validateCollider3D } from "./gameplay-Cfr6aFZ1.js";
4
+ import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
5
+ import { I as buildMeshGeometry, N as Joint3D, P as InstancedMesh3D, k as Terrain3D } from "./register-DJ0SByQg.js";
6
6
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
7
7
  //#region src/3d/physics/collider-lines.ts
8
8
  /**
@@ -241,6 +241,7 @@ var Physics3D = class {
241
241
  this.R = R;
242
242
  this.engine = engine;
243
243
  this.optsGravity = opts?.gravity;
244
+ this.simulate = opts?.simulate ?? true;
244
245
  this.lastScene = engine.scene;
245
246
  const declared = engine.scene?.physics?.gravity;
246
247
  const g = opts?.gravity ?? declared ?? [
@@ -259,9 +260,16 @@ var Physics3D = class {
259
260
  this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
260
261
  this.syncBodies();
261
262
  }
263
+ /** Whether the solver runs at all. False = collider outlines only. */
264
+ simulate;
262
265
  /** @internal Driven by engine.fixedUpdated. */
263
266
  step(dt) {
264
267
  this.lastDt = dt;
268
+ if (!this.simulate) {
269
+ this.syncBodies();
270
+ this.poseFromTree();
271
+ return;
272
+ }
265
273
  if (this.engine.scene !== this.lastScene) {
266
274
  this.lastScene = this.engine.scene;
267
275
  if (!this.optsGravity) {
@@ -332,6 +340,33 @@ var Physics3D = class {
332
340
  }
333
341
  });
334
342
  }
343
+ /**
344
+ * Move every body to its node's current transform.
345
+ *
346
+ * Only used in outline mode: normally physics owns the pose and writes it
347
+ * INTO the node, so nothing re-reads the tree. Deliberately computed the same
348
+ * way `ensureEntry` computes a body's initial pose — world position, node-local
349
+ * euler — so an outline in the editor is the outline the game will draw, right
350
+ * down to the places where that pair disagrees with a rotated parent.
351
+ */
352
+ poseFromTree() {
353
+ for (const [node, e] of this.entries) {
354
+ const [wx, wy, wz] = worldPosition3D(node);
355
+ e.body.setTranslation({
356
+ x: wx,
357
+ y: wy,
358
+ z: wz
359
+ }, false);
360
+ quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
361
+ e.body.setRotation({
362
+ x: quatScratch.x,
363
+ y: quatScratch.y,
364
+ z: quatScratch.z,
365
+ w: quatScratch.w
366
+ }, false);
367
+ }
368
+ this.world.propagateModifiedBodyPositionsToColliders();
369
+ }
335
370
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
336
371
  moveAndSlide(node) {
337
372
  const e = this.ensureEntry(node);
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-CNKXGfpr.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CbuLWorm.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-DuBTv2zI.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-5z_QVtLx.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -4,7 +4,7 @@ import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-L05WgWd1.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-Cfr6aFZ1.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -69,6 +69,35 @@ function devServerLibrary(token) {
69
69
  };
70
70
  }
71
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
+ /**
72
101
  * The default opener: pull `incanto/editor` in on demand and mount it.
73
102
  *
74
103
  * It is a dynamic import so the editor — an IDE's worth of UI — never enters a
@@ -83,18 +112,44 @@ async function openBundledEditor(opts) {
83
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)})`);
84
113
  }
85
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
+ };
86
124
  handle = await mod.mountEditor({
87
125
  ...opts.camera ? { camera: opts.camera } : {},
88
126
  api: {
89
127
  meta: async () => ({
90
128
  version: "live",
91
- mode: "single",
129
+ mode: "project",
92
130
  root: "",
93
131
  input: null,
94
132
  output: null
95
133
  }),
96
- loadScene: async () => opts.scene,
97
- ...opts.save ? { saveScene: async (json) => void opts.save?.(json) } : {},
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
+ },
98
153
  ...opts.library ? { library: opts.library } : {}
99
154
  },
100
155
  onPlay: (choice, working) => {
@@ -158,4 +213,28 @@ function poseFromRenderer(renderer, fallbackDistance = 8) {
158
213
  };
159
214
  }
160
215
  //#endregion
161
- export { poseFromRenderer as i, devServerLibrary as n, openBundledEditor as r, crossFade as t };
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.js CHANGED
@@ -4,9 +4,9 @@ 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-L05WgWd1.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-Cfr6aFZ1.js";
8
8
  import { t as registerNodes2D } from "./register-DWcWq4QG.js";
9
- import { t as registerNodes3D } from "./register-C44aSduO.js";
9
+ import { t as registerNodes3D } from "./register-DJ0SByQg.js";
10
10
  import { t as registerNodesNet } from "./register-MelqEdza.js";
11
11
  //#region src/test/index.ts
12
12
  /**
@@ -218,10 +218,10 @@ async function runScript(json, opts) {
218
218
  engine.setScene(scene);
219
219
  const physics = opts.physics ?? "auto";
220
220
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
221
- const { enablePhysics2D } = await import("./physics-2d-CCVTrKOd.js").then((n) => n.r);
221
+ const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
222
222
  await enablePhysics2D(engine);
223
223
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
224
- const { enablePhysics3D } = await import("./physics-3d-BZZLtwJu.js").then((n) => n.r);
224
+ const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
225
225
  await enablePhysics3D(engine);
226
226
  }
227
227
  const failures = [];
@@ -323,10 +323,10 @@ async function createPlaySession(json, opts = {}) {
323
323
  engine.setScene(scene);
324
324
  const physics = opts.physics ?? "auto";
325
325
  if (physics === "2d" || physics === "auto" && scene.dimension === "2d") {
326
- const { enablePhysics2D } = await import("./physics-2d-CCVTrKOd.js").then((n) => n.r);
326
+ const { enablePhysics2D } = await import("./physics-2d-3kOQCtgd.js").then((n) => n.r);
327
327
  await enablePhysics2D(engine);
328
328
  } else if (physics === "3d" || physics === "auto" && scene.dimension === "3d") {
329
- const { enablePhysics3D } = await import("./physics-3d-BZZLtwJu.js").then((n) => n.r);
329
+ const { enablePhysics3D } = await import("./physics-3d-CeRH-Ff_.js").then((n) => n.r);
330
330
  await enablePhysics3D(engine);
331
331
  }
332
332
  const stepMs = 1e3 / (opts.fixedHz ?? 60);
package/dist/vite.d.ts CHANGED
@@ -17,11 +17,97 @@ 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;
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;
24
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>;
25
111
  interface DevServer {
26
112
  middlewares: {
27
113
  use(path: string, handler: (req: LibraryReq, res: LibraryRes) => void): void;
@@ -75,4 +161,4 @@ declare function incantoLibrary(opts?: IncantoLibraryOptions): {
75
161
  /** @internal Exported for tests — the whole request/response behaviour. */
76
162
  declare function serveLibrary(req: LibraryReq, res: LibraryRes, opts?: IncantoLibraryOptions, doFetch?: typeof fetch): Promise<void>;
77
163
  //#endregion
78
- export { IncantoLibraryOptions, IncantoScenesOptions, incantoLibrary, incantoScenes, serveLibrary };
164
+ export { IncantoLibraryOptions, IncantoScenesOptions, ProjectFileEntry, discoverScenes, incantoLibrary, incantoScenes, resolveSceneFile, sceneFacts, serveLibrary, serveSceneFile, serveSceneList };