incanto 0.29.0 → 0.31.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 (55) hide show
  1. package/bin/incanto-editor.mjs +101 -28
  2. package/dist/2d.d.ts +28 -6
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +81 -7
  5. package/dist/3d.js +4 -4
  6. package/dist/{audit-C6rMyict.js → audit-C4kmDK0o.js} +16 -0
  7. package/dist/{behavior-BAc0erXF.d.ts → behavior-D_jMpFh8.d.ts} +117 -55
  8. package/dist/{create-game-CNKXGfpr.js → create-game-DqOjMBUS.js} +61 -48
  9. package/dist/{create-game-CbuLWorm.js → create-game-z5XaB1p5.js} +27 -15
  10. package/dist/debug-draw-BM3DsvtT.js +18 -0
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +8 -3
  13. package/dist/{duplicate-BgtFrFo4.js → duplicate-KRPtUtzl.js} +1 -1
  14. package/dist/{editor-switch-B0wB_DSr.d.ts → editor-switch-BJb-CWfA.d.ts} +12 -1
  15. package/dist/editor.js +1346 -1231
  16. package/dist/{register-C44aSduO.js → environment-presets--DigHNg4.js} +42 -11
  17. package/dist/{gameplay-L05WgWd1.js → gameplay-BftxM_It.js} +14 -5
  18. package/dist/gameplay.d.ts +1 -1
  19. package/dist/gameplay.js +1 -1
  20. package/dist/index.d.ts +12 -3
  21. package/dist/index.js +6 -6
  22. package/dist/{loader-B242FF6N.d.ts → loader-BYBrqTxP.d.ts} +1 -1
  23. package/dist/{loader-BZqOKfI2.js → loader-BlaRQGaA.js} +482 -6
  24. package/dist/net.d.ts +1 -1
  25. package/dist/net.js +3 -3
  26. package/dist/{pathfinding-BwD974Ss.d.ts → pathfinding-BwhqPD3i.d.ts} +1 -1
  27. package/dist/{physics-2d-CCVTrKOd.js → physics-2d-D9wquBvK.js} +30 -4
  28. package/dist/{physics-3d-BZZLtwJu.js → physics-3d-CnPygVGo.js} +41 -5
  29. package/dist/react.d.ts +1 -1
  30. package/dist/react.js +1 -1
  31. package/dist/{register-MelqEdza.js → register-CUY284Is.js} +3 -3
  32. package/dist/{register-BTg0EM7s.js → register-Dzkd6-os.js} +56 -380
  33. package/dist/{register-DWcWq4QG.js → register-tkR_8tWg.js} +4 -4
  34. package/dist/{registry-BVJ2HbCn.js → registry-CJdGpT2V.js} +6 -2
  35. package/dist/{editor-switch-CAKlJMIY.js → teardown-ByzfDPyu.js} +83 -4
  36. package/dist/test.d.ts +2 -7
  37. package/dist/test.js +43 -14
  38. package/dist/vite.d.ts +87 -1
  39. package/dist/vite.js +262 -3
  40. package/editor/assets/{agent8-DEVkEa3d.js → agent8-o27_Y1xN.js} +1 -1
  41. package/editor/assets/{debug-zGAtpDF0.js → debug-DfcWX3uW.js} +3 -2
  42. package/editor/assets/index-C9fb5QcT.js +10696 -0
  43. package/editor/index.html +1 -1
  44. package/package.json +1 -1
  45. package/skills/incanto-editor.md +61 -8
  46. package/skills/incanto-physics-and-input.md +15 -0
  47. package/skills/incanto-verifying-your-game.md +68 -0
  48. package/templates-app/beacon-isle-3d/package.json +1 -1
  49. package/templates-app/beacon-isle-3d/vite.config.ts +7 -0
  50. package/templates-app/tps-3d/package.json +1 -1
  51. package/templates-app/tps-3d/vite.config.ts +7 -0
  52. package/templates-app/village-quest-3d/package.json +1 -1
  53. package/templates-app/village-quest-3d/vite.config.ts +7 -0
  54. package/dist/debug-draw-CZmOYjL2.js +0 -13
  55. package/editor/assets/index-Bx4UtWYY.js +0 -10586
@@ -9,7 +9,7 @@
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
@@ -27,7 +27,7 @@
27
27
  * request host. Bodies are capped at 8MB. Addressable paths are root-bounded
28
28
  * and must end in .scene.json.
29
29
  */
30
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
31
31
  import { createServer } from 'node:http';
32
32
  import { basename, dirname, extname, join, normalize, relative, resolve, sep } from 'node:path';
33
33
  import { fileURLToPath } from 'node:url';
@@ -213,9 +213,20 @@ function discoverScenes() {
213
213
  for (const e of entries) {
214
214
  if (e.isDirectory()) {
215
215
  if (!SKIP_DIRS.has(e.name) && !e.name.startsWith('.')) walk(join(dir, e.name), depth + 1);
216
- } else if (e.name.endsWith('.scene.json')) {
216
+ } else if (e.name.endsWith('.json')) {
217
217
  const abs = join(dir, e.name);
218
- 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) });
219
230
  }
220
231
  }
221
232
  };
@@ -223,9 +234,65 @@ function discoverScenes() {
223
234
  return found.sort((a, b) => a.rel.localeCompare(b.rel));
224
235
  }
225
236
 
226
- /** 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
+ */
227
294
  function resolveSceneFile(rel) {
228
- if (typeof rel !== 'string' || !rel.endsWith('.scene.json')) return null;
295
+ if (typeof rel !== 'string' || !rel.endsWith('.json')) return null;
229
296
  const abs = normalize(resolve(ROOT, rel));
230
297
  if (abs !== ROOT && !abs.startsWith(ROOT + sep)) return null;
231
298
  return abs;
@@ -276,18 +343,6 @@ function originAllowed(req) {
276
343
  }
277
344
  }
278
345
 
279
- function isSceneShaped(json) {
280
- return (
281
- json !== null &&
282
- typeof json === 'object' &&
283
- json.format === 1 &&
284
- json.type === 'scene' &&
285
- typeof json.name === 'string' &&
286
- json.root !== null &&
287
- typeof json.root === 'object'
288
- );
289
- }
290
-
291
346
  function readBody(req, res, onJson) {
292
347
  const chunks = [];
293
348
  let received = 0;
@@ -346,18 +401,23 @@ const server = createServer((req, res) => {
346
401
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
347
402
  }
348
403
  readBody(req, res, (body) => {
349
- 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);
350
410
  if (!abs) {
351
411
  return send(
352
412
  res,
353
413
  400,
354
- 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' }),
355
415
  );
356
416
  }
357
417
  if (existsSync(abs)) {
358
- return send(res, 409, JSON.stringify({ error: `already exists: ${body.path}` }));
418
+ return send(res, 409, JSON.stringify({ error: `already exists: ${wanted}` }));
359
419
  }
360
- const name = basename(abs).replace(/\.scene\.json$/, '') || 'Scene';
420
+ const name = basename(abs).replace(/\.scene\.json$|\.json$/, '') || 'Scene';
361
421
  const scene = {
362
422
  format: 1,
363
423
  type: 'scene',
@@ -388,26 +448,39 @@ const server = createServer((req, res) => {
388
448
  );
389
449
  }
390
450
  if (req.method === 'GET') {
451
+ let text;
452
+ let parsed;
391
453
  try {
392
- const text = readFileSync(target.input, 'utf-8');
393
- JSON.parse(text); // must at least be JSON before the page gets it
394
- 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
395
456
  } catch (error) {
396
457
  return send(res, 422, JSON.stringify({ error: String(error.message ?? error) }));
397
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);
398
471
  }
399
472
  if (req.method === 'PUT') {
400
473
  if (!originAllowed(req)) {
401
474
  return send(res, 403, JSON.stringify({ error: 'cross-origin write rejected' }));
402
475
  }
403
476
  readBody(req, res, (json, raw) => {
404
- if (!isSceneShaped(json)) {
477
+ const shape = sceneFacts(json);
478
+ if (!shape.ok) {
405
479
  return send(
406
480
  res,
407
481
  422,
408
482
  JSON.stringify({
409
- error:
410
- '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.`,
411
484
  }),
412
485
  );
413
486
  }
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
2
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
1
3
  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
+ import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -24,9 +24,15 @@ interface SheetInfo {
24
24
  * `"$key"`. The texture loader is injectable so tests run headless.
25
25
  */
26
26
  declare class AssetStore2D {
27
+ private readonly engine;
27
28
  private readonly entries;
28
29
  private readonly loader;
29
- constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture);
30
+ /**
31
+ * `engine` is optional so a bare store still constructs in tests. Given one,
32
+ * a texture that fails to load lands in `engine.log` — where the overlay and
33
+ * `runScript()` can see it — instead of only in the browser console.
34
+ */
35
+ constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture, engine?: DiagnosticSink | null);
30
36
  /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
31
37
  load(assets: Record<string, JsonObject>): void;
32
38
  /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
@@ -146,6 +152,12 @@ type Rapier = typeof RapierNs;
146
152
  interface Physics2DOptions {
147
153
  /** px/s², y-down. Default: scene `physics.gravity`, else [0, 980]. */
148
154
  gravity?: [number, number];
155
+ /**
156
+ * Build the colliders and keep them on the tree, but never advance the world
157
+ * (default: `true`, i.e. simulate). See {@link Physics3DOptions.simulate} —
158
+ * the scene editor uses it to draw the REAL colliders without running them.
159
+ */
160
+ simulate?: boolean;
149
161
  }
150
162
  /**
151
163
  * Enable 2D physics for an engine. Dynamically imports Rapier (compat build,
@@ -158,7 +170,7 @@ declare function enablePhysics2D(engine: Engine, opts?: Physics2DOptions): Promi
158
170
  */
159
171
  declare class Physics2D {
160
172
  private readonly R;
161
- private readonly engine;
173
+ readonly engine: Engine;
162
174
  /** Render collider outlines in the GAME view (renderers pick this up). */
163
175
  debugDraw: boolean;
164
176
  /** Narrow those outlines to one node's subtree (see DebugLineSource). */
@@ -177,6 +189,8 @@ declare class Physics2D {
177
189
  private lastScene;
178
190
  constructor(R: Rapier, engine: Engine, opts?: Physics2DOptions);
179
191
  /** @internal Driven by engine.fixedUpdated. */
192
+ /** Whether the solver runs at all. False = collider outlines only. */
193
+ private readonly simulate;
180
194
  step(dt: number): void;
181
195
  /** @internal Called by CharacterBody2D.moveAndSlide (during tree fixedUpdate). */
182
196
  moveAndSlide(node: CharacterBody2D): void;
@@ -210,6 +224,13 @@ declare class Physics2D {
210
224
  private readonly jointSet;
211
225
  private readonly joints;
212
226
  private readonly bodySet;
227
+ /**
228
+ * Move every body to its node's current transform — outline mode only.
229
+ *
230
+ * Computed exactly as `ensureEntry` computes a body's first pose, so an
231
+ * outline drawn in the editor is the outline the running game draws.
232
+ */
233
+ private poseFromTree;
213
234
  private syncBodies;
214
235
  private ensureEntry;
215
236
  }
@@ -742,7 +763,8 @@ declare class Renderer2D {
742
763
  private readonly worldCam;
743
764
  private readonly uiCam;
744
765
  private readonly engine;
745
- private readonly assets;
766
+ /** The texture store. Public so `renderer.assets.status(key)` is reachable. */
767
+ readonly assets: AssetStore2D;
746
768
  private readonly loadedAssetScenes;
747
769
  private readonly disconnect;
748
770
  private readonly canvas;
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-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";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-z5XaB1p5.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-tkR_8tWg.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-D9wquBvK.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
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
+ import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
2
+ import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
1
3
  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
+ import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwD974Ss.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwhqPD3i.js";
7
7
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Vector3, WebGLRenderer } from "three";
8
8
  import { VRM } from "@pixiv/three-vrm";
9
9
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -44,12 +44,28 @@ interface AnimationEntry {
44
44
  * @pixiv/three-vrm (VRM 0.x facing fixed via rotateVRM0).
45
45
  */
46
46
  declare class AssetStore3D {
47
+ private readonly engine;
47
48
  private readonly models;
48
49
  private readonly animations;
49
50
  private readonly modelKeys;
50
51
  private readonly animationKeys;
51
52
  private readonly loader;
52
- constructor();
53
+ /**
54
+ * `engine` is optional only so a bare store still constructs in tests. Give it
55
+ * one and every load failure lands in `engine.log` — where the debug overlay,
56
+ * `runScript()` and any agent tool can see it. Without it a 404'd GLB was a
57
+ * console line and an invisible model.
58
+ */
59
+ constructor(engine?: DiagnosticSink | null);
60
+ /**
61
+ * Every asset that failed, with the reason — the answer to "why is my model
62
+ * not there". Empty is the healthy case.
63
+ */
64
+ errors(): Array<{
65
+ ref: string;
66
+ url: string;
67
+ error: string;
68
+ }>;
53
69
  /** Map a scene's `assets` header. */
54
70
  load(assets: JsonObject): void;
55
71
  /** Animation `$key`s declared by the scene (editor dropdowns). */
@@ -185,6 +201,13 @@ declare class Node3D extends Node {
185
201
  * back to a local Y by subtracting the parents' offset.
186
202
  */
187
203
  applyGroundSnap(): void;
204
+ /**
205
+ * @internal The backing object IF it has been built — never builds one.
206
+ *
207
+ * `_ensureObject3D()` is the wrong call during teardown: it allocates a mesh
208
+ * for a node that never rendered, purely so the next line can dispose it.
209
+ */
210
+ protected get _builtObject3D(): Object3D | null;
188
211
  /** @internal The backing three object (lazily created). */
189
212
  _ensureObject3D(): Object3D;
190
213
  /** @internal Override point for subclasses (mesh, camera, light…). */
@@ -269,6 +292,20 @@ type Rapier = typeof RapierNs;
269
292
  interface Physics3DOptions {
270
293
  /** m/s², y-up. Default: scene `physics.gravity`, else [0, -9.81, 0]. */
271
294
  gravity?: [number, number, number];
295
+ /**
296
+ * Build the colliders and keep them on the tree, but never advance the world
297
+ * (default: `true`, i.e. simulate).
298
+ *
299
+ * This exists for the scene editor. "Show colliders" there used to hand-draw
300
+ * wireframes from the `collider` JSON — which cannot show what the engine
301
+ * actually creates, because the shape is not always in the JSON (a mesh-fitted
302
+ * body, a scatter's hulls, a character capsule) and the drawing ignored
303
+ * rotation and scale. With `simulate: false` the editor gets the SAME lines
304
+ * play mode gets, from the same Rapier world, without anything falling over
305
+ * the moment you look at it: bodies are posed FROM the tree each tick and the
306
+ * solver never runs.
307
+ */
308
+ simulate?: boolean;
272
309
  }
273
310
  /**
274
311
  * Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
@@ -279,7 +316,7 @@ declare function enablePhysics3D(engine: Engine, opts?: Physics3DOptions): Promi
279
316
  /** Per-engine 3D physics world. Same contract as Physics2D. */
280
317
  declare class Physics3D {
281
318
  private readonly R;
282
- private readonly engine;
319
+ readonly engine: Engine;
283
320
  /** Render collider outlines in the GAME view (renderers pick this up). */
284
321
  debugDraw: boolean;
285
322
  /** Narrow those outlines to one node's subtree (see DebugLineSource). */
@@ -297,8 +334,20 @@ declare class Physics3D {
297
334
  private readonly optsGravity;
298
335
  private lastScene;
299
336
  constructor(R: Rapier, engine: Engine, opts?: Physics3DOptions);
337
+ /** Whether the solver runs at all. False = collider outlines only. */
338
+ private readonly simulate;
300
339
  /** @internal Driven by engine.fixedUpdated. */
301
340
  step(dt: number): void;
341
+ /**
342
+ * Move every body to its node's current transform.
343
+ *
344
+ * Only used in outline mode: normally physics owns the pose and writes it
345
+ * INTO the node, so nothing re-reads the tree. Deliberately computed the same
346
+ * way `ensureEntry` computes a body's initial pose — world position, node-local
347
+ * euler — so an outline in the editor is the outline the game will draw, right
348
+ * down to the places where that pair disagrees with a rotated parent.
349
+ */
350
+ private poseFromTree;
302
351
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
303
352
  moveAndSlide(node: CharacterBody3D): void;
304
353
  /** Rapier debug segments (meters). Null while debugDraw is off. */
@@ -462,6 +511,14 @@ interface CreateGame3DOptions {
462
511
  interface GameRenderer {
463
512
  dispose(): void;
464
513
  stats?(): RendererStats;
514
+ /** Loaded models/animations — `assets.errors()` answers "why is my model missing". */
515
+ assets?: {
516
+ errors(): Array<{
517
+ ref: string;
518
+ url: string;
519
+ error: string;
520
+ }>;
521
+ };
465
522
  }
466
523
  interface Game3D {
467
524
  engine: Engine;
@@ -480,6 +537,18 @@ interface Game3D {
480
537
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
481
538
  stats(): GameStats;
482
539
  /**
540
+ * Assets that failed to load, with the reason.
541
+ *
542
+ * "The model just never appeared" used to be unanswerable from outside: the
543
+ * store was private on the renderer and the 404 went to the console only.
544
+ * Check this — and `stats().errors` — before assuming a scene is fine.
545
+ */
546
+ assetErrors(): Array<{
547
+ ref: string;
548
+ url: string;
549
+ error: string;
550
+ }>;
551
+ /**
483
552
  * Turn this page into the scene editor, right where the player is standing —
484
553
  * what the ☰ debug menu's "edit this scene" calls. Bind it to your own key or
485
554
  * button if you want the switch without the overlay.
@@ -2958,7 +3027,12 @@ declare class Renderer3D {
2958
3027
  private readonly engine;
2959
3028
  private readonly disconnect;
2960
3029
  private readonly canvas;
2961
- private readonly assets;
3030
+ /**
3031
+ * The model/animation store. Public so a game (and an agent's tool) can ask
3032
+ * `renderer.assets.errors()` why a model is not there — it used to be private,
3033
+ * so a 404'd GLB was unanswerable from outside.
3034
+ */
3035
+ readonly assets: AssetStore3D;
2962
3036
  private readonly ownsAssets;
2963
3037
  private readonly loadedAssetScenes;
2964
3038
  /** The scene whose GPU programs have been pre-compiled (warm-up, see render). */
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-L05WgWd1.js";
3
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-CNKXGfpr.js";
4
- import { A as DEFAULT_TERRAIN_TEXTURE_BASE, B as movementState, C as FLOWER_VARIETIES, D as BoneAttachment3D, E as BoneLookAt3D, F as MeshInstance3D, L as QUARTER_PITCH, M as terrainThemeLayers, N as Joint3D, O as Billboard3D, P as InstancedMesh3D, R as cameraRelative, S as resolveFlowerDensity, T as Camera3D, V as rigPose, _ as DirectionalLight3D, a as Trail3D, b as DENSITY_PRESETS, c as findRiverCoverageGaps, d as riverStepFor, f as smoothCourse, g as LoftMesh3D, h as ModelInstance3D, i as Tree3D, j as TERRAIN_THEMES, k as Terrain3D, l as projectToRiver, m as Particles3D, n as VOXEL_PALETTE, o as River3D, p as traceDownhillPath, r as VoxelGrid3D, s as buildRiverRings, t as registerNodes3D, u as riverCarveChannels, v as OmniLight3D, w as CharacterController3D, x as Flowers3D, y as Foliage3D, z as keyboardIntensity } from "./register-C44aSduO.js";
2
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BftxM_It.js";
3
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-DqOjMBUS.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, V as movementState, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets--DigHNg4.js";
5
5
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
6
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-BZZLtwJu.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CnPygVGo.js";
7
7
  //#region src/3d/environment-runtime.ts
8
8
  /**
9
9
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -25,6 +25,19 @@ const HUD_WIDGETS = new Set([
25
25
  "UiButton",
26
26
  "UiDialogue"
27
27
  ]);
28
+ const LIGHT_TYPES = new Set([
29
+ "DirectionalLight3D",
30
+ "OmniLight3D",
31
+ "SpotLight3D"
32
+ ]);
33
+ /** Does the scene header light anything by itself (sky, hdri, ambient, preset)? */
34
+ function isLit(environment) {
35
+ if (!environment) return false;
36
+ if (environment.preset !== void 0 || environment.sky !== void 0) return true;
37
+ if (environment.hdri !== void 0) return true;
38
+ const ambient = environment.ambient;
39
+ return typeof ambient?.intensity === "number" && ambient.intensity > 0;
40
+ }
28
41
  /** Human-readable warnings (empty = clean). Pure JSON walk, no registry. */
29
42
  function auditScene(scene) {
30
43
  const warnings = [];
@@ -32,6 +45,7 @@ function auditScene(scene) {
32
45
  if (!root) return warnings;
33
46
  let currentCameras = 0;
34
47
  let cameras = 0;
48
+ let lights = 0;
35
49
  const walk = (node, path, insideHud, staticRoot) => {
36
50
  const type = node.type ?? "";
37
51
  const props = node.props ?? {};
@@ -39,6 +53,7 @@ function auditScene(scene) {
39
53
  cameras += 1;
40
54
  if (props.current === true) currentCameras += 1;
41
55
  }
56
+ if (LIGHT_TYPES.has(type)) lights += 1;
42
57
  if (BODY_TYPES.has(type)) {
43
58
  const collider = props.collider;
44
59
  if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
@@ -52,6 +67,7 @@ function auditScene(scene) {
52
67
  walk(root, root.name ?? "root", false, root.props?.static === true ? root.name ?? "root" : null);
53
68
  if (cameras > 0 && currentCameras === 0) warnings.push("no camera has \"current\": true — the screen renders from a default view, probably not what you framed.");
54
69
  if (currentCameras > 1) warnings.push(`${currentCameras} cameras claim "current": true — only the first found wins.`);
70
+ if (scene.dimension === "3d" && lights === 0 && !isLit(scene.environment)) warnings.push("nothing lights this 3D scene — no light node, no environment sky/hdri, and no ambient intensity. It will render black. Add a DirectionalLight3D, or an \"environment\": { \"sky\": { \"type\": \"atmosphere\" } } / { \"preset\": \"...\" }.");
55
71
  return warnings;
56
72
  }
57
73
  //#endregion