incanto 0.60.0 → 0.62.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 (68) hide show
  1. package/bin/incanto-check.mjs +13 -0
  2. package/bin/incanto-verify.mjs +134 -55
  3. package/dist/2d.d.ts +64 -3
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +5 -5
  6. package/dist/3d.js +5 -4
  7. package/dist/{pathfinding-BqWBb0kh.d.ts → audio-player-D5GJgb_x.d.ts} +103 -38
  8. package/dist/{behavior-DWKTUzKI.d.ts → behavior-DsgayMsH.d.ts} +79 -1
  9. package/dist/{create-game-ClnIb_M5.js → create-game-BpunnGPX.js} +78 -13
  10. package/dist/{create-game-BCm38FJV.js → create-game-Caut3bqN.js} +21 -294
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/debug.js +1 -1
  13. package/dist/{duplicate-DJQd44CD.js → duplicate-E4FUs5Bn.js} +1 -1
  14. package/dist/editor.js +41 -19
  15. package/dist/{environment-presets-8cjF3t6w.js → environment-presets-BAWeOeqf.js} +50 -13
  16. package/dist/{gameplay-BBEjPFsR.js → gameplay-CaHqDiQD.js} +177 -24
  17. package/dist/gameplay.d.ts +1 -1
  18. package/dist/gameplay.js +1 -1
  19. package/dist/index.d.ts +97 -6
  20. package/dist/index.js +7 -7
  21. package/dist/{loader-D8n7TU8W.js → loader-DEe272nY.js} +900 -2
  22. package/dist/{loader-TvkRFbyL.d.ts → loader-DolLJWJn.d.ts} +13 -1
  23. package/dist/net.d.ts +2 -2
  24. package/dist/net.js +1 -1
  25. package/dist/pathfinding-_fGrCFmH.d.ts +28 -0
  26. package/dist/{physics-2d-DqdVp1bt.js → physics-2d-BXmu2i7W.js} +11 -3
  27. package/dist/{physics-3d-BP0DZb_1.js → physics-3d-ClxP6Uv7.js} +121 -17
  28. package/dist/react.d.ts +1 -1
  29. package/dist/react.js +1 -1
  30. package/dist/{register-BSu2dWGC.js → register-CNh4FlbD.js} +104 -16
  31. package/dist/{register-Da3hXh2H.js → register-D3yx8D4r.js} +228 -16
  32. package/dist/{registry-WWcQcfMr.js → registry-CF70EArN.js} +55 -3
  33. package/dist/{replay-BCMK_VRP.d.ts → replay-C5x2vPF5.d.ts} +2 -2
  34. package/dist/{replay-BlNuIDdg.js → replay-DlgHItNv.js} +57 -197
  35. package/dist/{split-screen-CL5Yvxse.js → split-screen-CBM9wcMX.js} +3 -3
  36. package/dist/{split-screen-BQ3tAsf-.d.ts → split-screen-D7OopelJ.d.ts} +2 -2
  37. package/dist/{src-DFpXBMJN.js → src-CH00_JsR.js} +1 -1
  38. package/dist/{teardown-CTTwhWSe.js → teardown-C7qP-dcC.js} +1 -1
  39. package/dist/{test-BeZ95pqw.js → test-BMkg8zMV.js} +29 -16
  40. package/dist/test.d.ts +45 -4
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +2 -2
  43. package/editor/assets/{agent8-DSJries_.js → agent8-m5mtAO_A.js} +1 -1
  44. package/editor/assets/{debug-D15Wi5TO.js → debug-CPhzCT8f.js} +1 -1
  45. package/editor/assets/{index-BjC88k97.js → index-D422P4kW.js} +92 -92
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +91 -0
  49. package/skills/incanto-3d-models.md +1 -1
  50. package/skills/incanto-assets.md +10 -1
  51. package/skills/incanto-audio.md +21 -13
  52. package/skills/incanto-behaviors-and-scripts.md +44 -3
  53. package/skills/incanto-building-2d-games.md +32 -4
  54. package/skills/incanto-building-3d-games.md +1 -1
  55. package/skills/incanto-editor.md +1 -1
  56. package/skills/incanto-gameplay-behaviors.md +39 -2
  57. package/skills/incanto-hud.md +5 -3
  58. package/skills/incanto-localization.md +7 -0
  59. package/skills/incanto-node-reference.md +12 -0
  60. package/skills/incanto-physics-and-input.md +23 -2
  61. package/skills/incanto-scene-json-authoring.md +6 -1
  62. package/skills/incanto-verifying-your-game.md +31 -1
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/platformer-2d/package.json +1 -1
  65. package/templates-app/star-survivor/package.json +1 -1
  66. package/templates-app/tps-3d/package.json +1 -1
  67. package/templates-app/village-quest-3d/package.json +1 -1
  68. package/dist/particle-sim-C5OfBbmU.d.ts +0 -77
@@ -551,6 +551,14 @@ interface SynthOptions {
551
551
  seed?: number;
552
552
  }
553
553
  /**
554
+ * How long a preset lasts, in seconds — the same `attack + sustain + decay`
555
+ * `synthSfx` uses to size its buffer.
556
+ *
557
+ * `pitch` deliberately does not enter into it: it is a frequency multiplier,
558
+ * not a playback rate, so a preset played an octave up is the same length.
559
+ */
560
+ declare function sfxDuration(params: SfxParams): number;
561
+ /**
554
562
  * Render a preset to mono PCM in [-1, 1]. PURE: identical inputs → identical
555
563
  * output (the only randomness is the seeded noise generator), so determinism is
556
564
  * unit-testable headlessly. Length = ceil((attack+sustain+decay) * sampleRate).
@@ -1780,6 +1788,44 @@ declare class Engine {
1780
1788
  tick(nowMs: number): void;
1781
1789
  }
1782
1790
  //#endregion
1791
+ //#region src/core/prop-range.d.ts
1792
+ /**
1793
+ * The numeric range a prop documents, checked at LOAD.
1794
+ *
1795
+ * The docs give ranges constantly — `metalness`/`roughness` "0..1 … `-1` = keep
1796
+ * authored", `targetHeight` "> 0 uniformly scales the model", `opacity` 0..1 —
1797
+ * and the schema had no way to say so, so every one was a type check and
1798
+ * nothing more. Measured on `ModelInstance3D`:
1799
+ *
1800
+ * ```
1801
+ * targetHeight: -1.8 NO ERROR (kept -1.8)
1802
+ * metalness: 5 NO ERROR (kept 5)
1803
+ * roughness: -3 NO ERROR (kept -3)
1804
+ * ```
1805
+ *
1806
+ * The two failure modes differ, which is why neither is noticed. A negative
1807
+ * `targetHeight` folds into the same branch as "not set", so the model keeps
1808
+ * its authored size and looks like the prop did nothing; `metalness: 5` reaches
1809
+ * three and the surface goes wrong. An author who mistypes a decimal point gets
1810
+ * a different wrong answer depending on which prop they mistyped it in, and no
1811
+ * message in either case.
1812
+ */
1813
+ interface PropRange {
1814
+ /** Lowest accepted value. */
1815
+ min?: number;
1816
+ /** Highest accepted value. */
1817
+ max?: number;
1818
+ /** `min` is a bound the value must EXCEED, not reach (`targetHeight > 0`). */
1819
+ exclusiveMin?: boolean;
1820
+ /**
1821
+ * Values outside the range that mean something anyway.
1822
+ *
1823
+ * `metalness: -1` is "keep whatever the GLB authored" — a real value with a
1824
+ * real meaning, and the reason a range cannot just be a clamp.
1825
+ */
1826
+ allow?: readonly number[];
1827
+ }
1828
+ //#endregion
1783
1829
  //#region src/core/registry.d.ts
1784
1830
  /** A node prop definition. The default both documents and types the prop. */
1785
1831
  interface PropDef {
@@ -1808,6 +1854,38 @@ interface PropDef {
1808
1854
  */
1809
1855
  required?: boolean;
1810
1856
  /**
1857
+ * This prop names an entry in the scene's `assets{}` block.
1858
+ *
1859
+ * Declaring it is what makes a typo a LOAD error. The check already existed —
1860
+ * `AssetStore2D.resolve` throws `UNKNOWN_ASSET` — and it runs inside the
1861
+ * renderer, in a browser, when the texture is first needed. Headless the
1862
+ * scene loaded clean, so `incanto-check` certified a sprite that will draw
1863
+ * nothing, and the docs' promise that "the engine hard-fails on a raw URL"
1864
+ * was not true anywhere an author could see it.
1865
+ *
1866
+ * The same story as `nodePath` and `required`: the rule existed, nothing
1867
+ * routed to it at author time.
1868
+ */
1869
+ /**
1870
+ * The numeric range this prop documents.
1871
+ *
1872
+ * Without it a documented range is prose only: `metalness: 5` and
1873
+ * `targetHeight: -1.8` both passed the kind check and then failed in two
1874
+ * different silent ways. See prop-range.
1875
+ */
1876
+ range?: PropRange;
1877
+ asset?: {
1878
+ /** The `assets{}` entry `type` a `$ref` here must point at. */kind: "texture" | "spritesheet" | "model" | "animation";
1879
+ /**
1880
+ * A value that is not a `$ref` is an error.
1881
+ *
1882
+ * False for props that legitimately take something else — a raw URL
1883
+ * (`UiImage.src`, `Sprite3D.texture`) or the name of a clip baked into a
1884
+ * GLB (`ModelInstance3D.animation`).
1885
+ */
1886
+ refOnly?: boolean;
1887
+ };
1888
+ /**
1811
1889
  * For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
1812
1890
  * `shape` decides which other keys are required. One declaration gives an
1813
1891
  * inspector both the dropdown AND something valid to write when the tag
@@ -1943,4 +2021,4 @@ declare function registeredBehaviors(): string[];
1943
2021
  /** Test isolation helper. */
1944
2022
  declare function clearBehaviors(): void;
1945
2023
  //#endregion
1946
- export { T_PREFIX as $, SettingsValues as A, Node as At, captureBehaviors as B, EngineStats as C, synthSfx as Ct, QualityCaps as D, PlayMusicOptions as Dt, DeviceHints as E, MusicTrack as Et, BehaviorState as F, LogManager as Ft, ORDER_GROUP_BASE as G, savesWithoutUid as H, RestoreReport as I, Signal as It, effectiveOrder as J, OrderGroup as K, SaveSlot as L, SignalListener as Lt, readDeviceHints as M, SceneTree as Mt, suggestQuality as N, LogEntry as Nt, QualityTier as O, AudioBuses as Ot, Scene as P, LogLevel as Pt, Localization as Q, SaveSlots as R, Scheduler as S, SynthOptions as St, RendererStats as T, MusicManager as Tt, SaveStore as U, restoreBehaviors as V, createSaveStore as W, BASE_LOCALE as X, resolveOrderGroups as Y, LocaleTables as Z, mergeStaticSignals as _, spatialPan as _t, clearBehaviors as a, EffectLog as at, Engine as b, SfxParams as bt, registeredBehaviors as c, isAudioContextAvailable as ct, PropSchema as d, Listener as dt, suggestLocale as et, clearRegistry as f, ROLLOFF_MODELS as ft, getNodeType as g, spatialGain as gt, getNodeSignals as h, Vec3 as ht, behaviorSignals as i, EffectKind as it, qualityEnvironment as j, NodeLifecycle as jt, Settings as k, BusName as kt, NodeCtor as l, Voice as lt, getNodeSchema as m, SpatialParams as mt, BehaviorCtor as n, InputMap as nt, getBehavior as o, SfxEngine as ot, createNode as p, RolloffModel as pt, OrderGroupTable as q, behaviorSchema as r, EffectEvent as rt, registerBehavior as s, SfxPlayOptions as st, Behavior as t, translationKey as tt, PropDef as u, VoicePreset as ut, registerNode as v, SFX_PRESETS as vt, GameStats as w, MusicBackend as wt, EngineOptions as x, SfxWave as xt, registeredTypes as y, SFX_PRESET_NAMES as yt, behaviorsWithoutSave as z };
2024
+ export { T_PREFIX as $, SettingsValues as A, BusName as At, captureBehaviors as B, EngineStats as C, sfxDuration as Ct, QualityCaps as D, MusicTrack as Dt, DeviceHints as E, MusicManager as Et, BehaviorState as F, LogLevel as Ft, ORDER_GROUP_BASE as G, savesWithoutUid as H, RestoreReport as I, LogManager as It, effectiveOrder as J, OrderGroup as K, SaveSlot as L, Signal as Lt, readDeviceHints as M, NodeLifecycle as Mt, suggestQuality as N, SceneTree as Nt, QualityTier as O, PlayMusicOptions as Ot, Scene as P, LogEntry as Pt, Localization as Q, SaveSlots as R, SignalListener as Rt, Scheduler as S, SynthOptions as St, RendererStats as T, MusicBackend as Tt, SaveStore as U, restoreBehaviors as V, createSaveStore as W, BASE_LOCALE as X, resolveOrderGroups as Y, LocaleTables as Z, mergeStaticSignals as _, spatialPan as _t, clearBehaviors as a, EffectLog as at, Engine as b, SfxParams as bt, registeredBehaviors as c, isAudioContextAvailable as ct, PropSchema as d, Listener as dt, suggestLocale as et, clearRegistry as f, ROLLOFF_MODELS as ft, getNodeType as g, spatialGain as gt, getNodeSignals as h, Vec3 as ht, behaviorSignals as i, EffectKind as it, qualityEnvironment as j, Node as jt, Settings as k, AudioBuses as kt, NodeCtor as l, Voice as lt, getNodeSchema as m, SpatialParams as mt, BehaviorCtor as n, InputMap as nt, getBehavior as o, SfxEngine as ot, createNode as p, RolloffModel as pt, OrderGroupTable as q, behaviorSchema as r, EffectEvent as rt, registerBehavior as s, SfxPlayOptions as st, Behavior as t, translationKey as tt, PropDef as u, VoicePreset as ut, registerNode as v, SFX_PRESETS as vt, GameStats as w, synthSfx as wt, EngineOptions as x, SfxWave as xt, registeredTypes as y, SFX_PRESET_NAMES as yt, behaviorsWithoutSave as z };
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { c as resolveViewport, j as registerBehavior, n as loadScene, s as computeViewport, w as diagnose } from "./loader-D8n7TU8W.js";
3
- import { _ as Engine, g as AudioPlayer } from "./register-Da3hXh2H.js";
2
+ import { d as computeViewport, f as resolveViewport, k as diagnose, n as loadScene, z as registerBehavior } from "./loader-DEe272nY.js";
3
+ import { _ as AudioPlayer, v as Engine } from "./register-D3yx8D4r.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CTTwhWSe.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-C7qP-dcC.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
9
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-BSu2dWGC.js";
8
+ import { n as registerGameplayBehaviors } from "./gameplay-CaHqDiQD.js";
9
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-CNh4FlbD.js";
10
10
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
11
- import { n as enablePhysics2D } from "./physics-2d-DqdVp1bt.js";
11
+ import { n as enablePhysics2D } from "./physics-2d-BXmu2i7W.js";
12
12
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
13
13
  //#region src/2d/assets.ts
14
14
  /**
@@ -199,14 +199,19 @@ function selectionOutline2D(node) {
199
199
  function createSync2DScratch() {
200
200
  return {
201
201
  visited: /* @__PURE__ */ new Set(),
202
- cameras: []
202
+ cameras: [],
203
+ emitters: []
203
204
  };
204
205
  }
206
+ function isSpatialConsumer(node) {
207
+ return node.spatial === true && typeof node._setSpatialPose === "function";
208
+ }
205
209
  function syncTree2D(root, world, ui, assets, uiSize, scratch, opts) {
206
210
  const s = scratch ?? createSync2DScratch();
207
211
  s.visited.clear();
208
212
  s.cameras.length = 0;
209
- walk(root, world, ui, false, s.visited, s.cameras, assets, uiSize, opts?.ignoreStatic === true);
213
+ s.emitters.length = 0;
214
+ walk(root, world, ui, false, s.visited, s.cameras, s.emitters, assets, uiSize, opts?.ignoreStatic === true);
210
215
  prune(world, s.visited);
211
216
  prune(ui, s.visited);
212
217
  let current = null;
@@ -218,9 +223,66 @@ function syncTree2D(root, world, ui, assets, uiSize, scratch, opts) {
218
223
  }
219
224
  }
220
225
  if (!current) current = s.cameras[0] ?? null;
226
+ feedSpatialEmitters2D(s.emitters, current);
221
227
  return { activeCamera: current };
222
228
  }
223
- function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize, ignoreStatic) {
229
+ const emitterScratch = new Vector3();
230
+ const listenerScratch = new Vector3();
231
+ const upScratch = new Vector3();
232
+ /**
233
+ * Give a 2D scene's spatial AudioPlayers the same per-frame pose the 3D sync
234
+ * gives its own.
235
+ *
236
+ * `spatial`, `refDistance`, `maxDistance` and `rolloff` have been props of
237
+ * `AudioPlayer` since positional audio landed, and in a 2D scene all four did
238
+ * NOTHING: the node only attenuates when an adapter pushes it a pose, and only
239
+ * the 3D one ever did. The scene loaded, `incanto check` said valid, and the
240
+ * sound played at full volume from the far side of the level.
241
+ *
242
+ * The listener is the active `Camera2D` — its world position is the view
243
+ * centre, its own rotation turns the stereo image with it, and the view looks
244
+ * down −Z like every other camera. Distances are therefore in the same units
245
+ * the rest of a 2D scene is in: PIXELS. `refDistance: 1` / `maxDistance: 50`
246
+ * are metre-shaped defaults and read as "silent one sprite away" here, so a 2D
247
+ * scene has to set them; `auditScene` says so when it sees the defaults.
248
+ */
249
+ function feedSpatialEmitters2D(emitters, camera) {
250
+ if (emitters.length === 0 || !camera) return;
251
+ const view = camera._ensureObject2D();
252
+ view.updateWorldMatrix(true, false);
253
+ view.getWorldPosition(listenerScratch);
254
+ upScratch.set(0, 1, 0).applyQuaternion(view.quaternion);
255
+ const listener = {
256
+ position: [
257
+ listenerScratch.x,
258
+ listenerScratch.y,
259
+ 0
260
+ ],
261
+ forward: [
262
+ 0,
263
+ 0,
264
+ -1
265
+ ],
266
+ up: [
267
+ upScratch.x,
268
+ upScratch.y,
269
+ upScratch.z
270
+ ]
271
+ };
272
+ for (const { node, parent } of emitters) {
273
+ parent.updateWorldMatrix(true, false);
274
+ parent.getWorldPosition(emitterScratch);
275
+ node._setSpatialPose({
276
+ position: [
277
+ emitterScratch.x,
278
+ emitterScratch.y,
279
+ 0
280
+ ],
281
+ listener
282
+ });
283
+ }
284
+ }
285
+ function walk(node, parentObj, ui, inUi, visited, cameras, emitters, assets, uiSize, ignoreStatic) {
224
286
  let nextParent = parentObj;
225
287
  let nextInUi = inUi;
226
288
  if (node instanceof Node2D && !(node instanceof UILayer)) {
@@ -249,11 +311,14 @@ function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize, ignor
249
311
  visited.add(obj);
250
312
  if (node instanceof Camera2D && !inUi) cameras.push(node);
251
313
  nextParent = obj;
252
- }
253
- for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, assets, uiSize, ignoreStatic);
314
+ } else if (isSpatialConsumer(node)) emitters.push({
315
+ node,
316
+ parent: nextParent
317
+ });
318
+ for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, emitters, assets, uiSize, ignoreStatic);
254
319
  if (node instanceof Node2D && !(node instanceof UILayer)) {
255
320
  const obj = node._ensureObject2D();
256
- if (node.static && !ignoreStatic) {
321
+ if (node.static && !ignoreStatic && node._staticReady(assets)) {
257
322
  obj.userData.incantoStaticSynced = true;
258
323
  obj.userData.incantoStatic = true;
259
324
  } else if (obj.userData.incantoStaticSynced === true) {
@@ -474,7 +539,7 @@ var Renderer2D = class {
474
539
  const { activeCamera } = syncTree2D(scene.root, this.worldScene, this.uiScene, this.assets, {
475
540
  width: uiW,
476
541
  height: uiH
477
- }, this.syncScratch);
542
+ }, this.syncScratch, { ignoreStatic: this.ignoreStatic });
478
543
  let center = vp ? {
479
544
  x: vp.design[0] / 2,
480
545
  y: vp.design[1] / 2
@@ -1,14 +1,14 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { j as registerBehavior, n as loadScene, w as diagnose } from "./loader-D8n7TU8W.js";
3
- import { S as qualityRendering, _ as Engine, b as qualityCaps, g as AudioPlayer } from "./register-Da3hXh2H.js";
2
+ import { c as parseEnvironment3D, k as diagnose, n as loadScene, s as horizonColorFromSky, u as sunDirectionFromSky, z as registerBehavior } from "./loader-DEe272nY.js";
3
+ import { C as qualityRendering, _ as AudioPlayer, v as Engine, x as qualityCaps } from "./register-D3yx8D4r.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-DESwnpOc.js";
6
- import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-CTTwhWSe.js";
6
+ import { a as openBundledEditor, c as claimCanvasGestures, i as devServerLibrary, l as audioErrors, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as wireDevChannel, t as teardown } from "./teardown-C7qP-dcC.js";
7
7
  import { o as frameStats } from "./frame-report-BSMny7oe.js";
8
- import { R as PhysicsBody3D, U as createCausticsQuad, V as Node3D, n as registerGameplayBehaviors } from "./gameplay-BBEjPFsR.js";
8
+ import { K as createCausticsQuad, V as PhysicsBody3D, W as Node3D, n as registerGameplayBehaviors } from "./gameplay-CaHqDiQD.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-8cjF3t6w.js";
11
- import { n as enablePhysics3D } from "./physics-3d-BP0DZb_1.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BAWeOeqf.js";
11
+ import { n as enablePhysics3D } from "./physics-3d-ClxP6Uv7.js";
12
12
  import { ACESFilmicToneMapping, AmbientLight, Box3, BufferAttribute, BufferGeometry, Color, DepthTexture, EquirectangularReflectionMapping, FloatType, Fog, HalfFloatType, LineBasicMaterial, LineSegments, Matrix4, Mesh, PCFShadowMap, PMREMGenerator, PerspectiveCamera, PlaneGeometry, Quaternion, Raycaster, Scene, ShaderMaterial, Vector2, Vector3, WebGLRenderTarget, WebGLRenderer } from "three";
13
13
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
14
14
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -125,6 +125,10 @@ var AssetStore3D = class {
125
125
  } else entry.scene = gltf.scene;
126
126
  entry.animations = gltf.animations ?? [];
127
127
  entry.status = "ready";
128
+ if (!hasAnyMesh(entry.scene)) {
129
+ entry.error = "loaded, but it contains no meshes — this is an animation-only GLB. A clip file belongs in `animation`, not `model`.";
130
+ diagnose(this.engine, "error", `incanto: '${url}' ${entry.error}`);
131
+ }
128
132
  }, void 0, (error) => {
129
133
  entry.status = "error";
130
134
  entry.error = error instanceof Error ? error.message : String(error);
@@ -172,6 +176,16 @@ var AssetStore3D = class {
172
176
  this.animations.clear();
173
177
  }
174
178
  };
179
+ /** Does this loaded scene graph contain anything that will actually draw? */
180
+ function hasAnyMesh(scene) {
181
+ const walk = scene?.traverse;
182
+ if (!walk) return true;
183
+ let found = false;
184
+ walk.call(scene, (o) => {
185
+ if (o.isMesh) found = true;
186
+ });
187
+ return found;
188
+ }
175
189
  //#endregion
176
190
  //#region src/core/yield-frame.ts
177
191
  /**
@@ -226,293 +240,6 @@ function yieldToFrame(maxWaitMs = DEFAULT_MAX_WAIT_MS, raf = typeof requestAnima
226
240
  });
227
241
  }
228
242
  //#endregion
229
- //#region src/3d/environment.ts
230
- const SKY_KEYS = [
231
- "type",
232
- "sunPosition",
233
- "elevationDeg",
234
- "azimuthDeg",
235
- "turbidity",
236
- "rayleigh"
237
- ];
238
- const FOG_KEYS = [
239
- "color",
240
- "near",
241
- "far"
242
- ];
243
- const CLOUD_KEYS = [
244
- "coverage",
245
- "density",
246
- "base",
247
- "top",
248
- "color",
249
- "shadeColor",
250
- "speed",
251
- "scale"
252
- ];
253
- const BLOOM_KEYS = ["threshold", "strength"];
254
- const POST_KEYS = [
255
- "vignette",
256
- "saturation",
257
- "contrast"
258
- ];
259
- const SHADOW_KEYS = [
260
- "mapSize",
261
- "radius",
262
- "static"
263
- ];
264
- const SHADOW_MAP_SIZES = [1024, 2048];
265
- const DEFAULT_TURBIDITY = 2;
266
- const DEFAULT_RAYLEIGH = 1;
267
- const DEFAULT_FOG_NEAR = 50;
268
- const DEFAULT_FOG_FAR = 800;
269
- /** Fallback haze when fog is declared without a sky to derive a horizon from. */
270
- const DEFAULT_FOG_COLOR = "#cfd8e0";
271
- const DEG2RAD = Math.PI / 180;
272
- /** Parse + hard-validate the 3D slice of a scene's `environment` header. */
273
- function parseEnvironment3D(env) {
274
- return {
275
- exposure: parseExposure(env?.exposure),
276
- sky: parseSky(env?.sky),
277
- fog: parseFog(env?.fog, env?.sky !== void 0),
278
- clouds: parseClouds(env?.clouds),
279
- bloom: parseBloom(env?.bloom),
280
- post: parsePost(env?.post),
281
- shadows: parseShadows(env?.shadows)
282
- };
283
- }
284
- /**
285
- * elevation/azimuth (degrees) → unit direction. Matches three's Sky example
286
- * (`Vector3.setFromSphericalCoords`): azimuth 0 = +Z, 90 = +X; elevation 90 =
287
- * straight up.
288
- */
289
- function sunDirectionFromElevationAzimuth(elevationDeg, azimuthDeg) {
290
- const phi = (90 - elevationDeg) * DEG2RAD;
291
- const theta = azimuthDeg * DEG2RAD;
292
- return [
293
- Math.sin(phi) * Math.sin(theta),
294
- Math.cos(phi),
295
- Math.sin(phi) * Math.cos(theta)
296
- ];
297
- }
298
- /** The sky's sun as a UNIT vector — what Water3D/Foliage3D uniforms consume. */
299
- function sunDirectionFromSky(sky) {
300
- const [x, y, z] = sky.sunPosition;
301
- const len = Math.hypot(x, y, z) || 1;
302
- return [
303
- x / len,
304
- y / len,
305
- z / len
306
- ];
307
- }
308
- /**
309
- * A horizon-ish haze color derived from the sky config — the default fog
310
- * color. Cheap model, judged by eye: clear skies haze blue-grey, turbid skies
311
- * whiten, and a low sun warms the band toward amber.
312
- */
313
- function horizonColorFromSky(sky) {
314
- const clear = [
315
- 191,
316
- 213,
317
- 232
318
- ];
319
- const hazy = [
320
- 233,
321
- 228,
322
- 217
323
- ];
324
- const warm = [
325
- 242,
326
- 201,
327
- 150
328
- ];
329
- const t = clamp01((sky.turbidity - DEFAULT_TURBIDITY) / 8);
330
- const dir = sunDirectionFromSky(sky);
331
- const w = clamp01((18 - Math.asin(clamp(dir[1], -1, 1)) / DEG2RAD) / 18) * .8;
332
- const mix = (i) => Math.round(lerp(lerp(clear[i], hazy[i], t), warm[i], w));
333
- return `#${[
334
- mix(0),
335
- mix(1),
336
- mix(2)
337
- ].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
338
- }
339
- function parseExposure(value) {
340
- if (value === void 0) return 1;
341
- if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new IncantoError("BAD_FORMAT", `environment.exposure must be a finite number > 0 (tone-mapping exposure, default 1), got ${JSON.stringify(value)}.`, { prop: "exposure" });
342
- return value;
343
- }
344
- function parseSky(value) {
345
- if (value === void 0) return null;
346
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.sky must be an object ({ type?: "atmosphere", sunPosition? | elevationDeg?+azimuthDeg?, turbidity?, rayleigh? }), got ${JSON.stringify(value)}.`, { prop: "sky" });
347
- const sky = value;
348
- for (const key of Object.keys(sky)) if (!SKY_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.sky has unknown key '${key}'. Valid keys: [${SKY_KEYS.join(", ")}].`, {
349
- prop: "sky",
350
- validOptions: SKY_KEYS
351
- });
352
- if (sky.type !== void 0 && sky.type !== "atmosphere") throw new IncantoError("BAD_FORMAT", `environment.sky.type must be 'atmosphere' (the only sky type so far), got ${JSON.stringify(sky.type)}.`, {
353
- prop: "sky",
354
- validOptions: ["atmosphere"]
355
- });
356
- const hasAngles = sky.elevationDeg !== void 0 || sky.azimuthDeg !== void 0;
357
- if (sky.sunPosition !== void 0 && hasAngles) throw new IncantoError("BAD_FORMAT", `environment.sky takes sunPosition OR elevationDeg/azimuthDeg, not both.`, {
358
- prop: "sky",
359
- validOptions: ["sunPosition", "elevationDeg+azimuthDeg"]
360
- });
361
- let sunPosition;
362
- if (sky.sunPosition !== void 0) {
363
- const sp = sky.sunPosition;
364
- if (!Array.isArray(sp) || sp.length !== 3 || !sp.every((v) => typeof v === "number" && Number.isFinite(v)) || Math.hypot(sp[0], sp[1], sp[2]) === 0) throw new IncantoError("BAD_FORMAT", `environment.sky.sunPosition must be a non-zero [x, y, z] vector, got ${JSON.stringify(sp)}.`, { prop: "sky" });
365
- sunPosition = [
366
- sp[0],
367
- sp[1],
368
- sp[2]
369
- ];
370
- } else {
371
- const elevation = numberOr(sky.elevationDeg, 32, "sky.elevationDeg");
372
- const azimuth = numberOr(sky.azimuthDeg, 135, "sky.azimuthDeg");
373
- if (elevation < -90 || elevation > 90) throw new IncantoError("BAD_FORMAT", `environment.sky.elevationDeg must be in [-90, 90] (degrees above the horizon), got ${elevation}.`, { prop: "sky" });
374
- sunPosition = sunDirectionFromElevationAzimuth(elevation, azimuth);
375
- }
376
- const turbidity = numberOr(sky.turbidity, DEFAULT_TURBIDITY, "sky.turbidity");
377
- if (turbidity <= 0) throw new IncantoError("BAD_FORMAT", `environment.sky.turbidity must be > 0 (atmospheric haze; 2 ≈ clear day), got ${turbidity}.`, { prop: "sky" });
378
- const rayleigh = numberOr(sky.rayleigh, DEFAULT_RAYLEIGH, "sky.rayleigh");
379
- if (rayleigh < 0) throw new IncantoError("BAD_FORMAT", `environment.sky.rayleigh must be >= 0 (Rayleigh scattering; 1 ≈ earth-like), got ${rayleigh}.`, { prop: "sky" });
380
- return {
381
- sunPosition,
382
- turbidity,
383
- rayleigh
384
- };
385
- }
386
- function parseFog(value, hasSky) {
387
- if (value === void 0) return null;
388
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.fog must be an object ({ color?, near?, far? }), got ${JSON.stringify(value)}.`, { prop: "fog" });
389
- const fog = value;
390
- for (const key of Object.keys(fog)) if (!FOG_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.fog has unknown key '${key}'. Valid keys: [${FOG_KEYS.join(", ")}].`, {
391
- prop: "fog",
392
- validOptions: FOG_KEYS
393
- });
394
- if (fog.color !== void 0 && typeof fog.color !== "string") throw new IncantoError("BAD_FORMAT", `environment.fog.color must be a hex color string, got ${JSON.stringify(fog.color)}.`, { prop: "fog" });
395
- const near = numberOr(fog.near, DEFAULT_FOG_NEAR, "fog.near");
396
- const far = numberOr(fog.far, DEFAULT_FOG_FAR, "fog.far");
397
- if (near < 0) throw new IncantoError("BAD_FORMAT", `environment.fog.near must be >= 0 meters, got ${near}.`, { prop: "fog" });
398
- if (far <= near) throw new IncantoError("BAD_FORMAT", `environment.fog.far must be > near (got near ${near}, far ${far}).`, { prop: "fog" });
399
- return {
400
- color: fog.color ?? (hasSky ? "" : DEFAULT_FOG_COLOR),
401
- near,
402
- far
403
- };
404
- }
405
- function parseClouds(value) {
406
- if (value === void 0) return null;
407
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.clouds must be an object ({ coverage?, density?, base?, top?, color?, shadeColor?, speed?, scale? }), got ${JSON.stringify(value)}.`, { prop: "clouds" });
408
- const c = value;
409
- for (const key of Object.keys(c)) if (!CLOUD_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.clouds has unknown key '${key}'. Valid keys: [${CLOUD_KEYS.join(", ")}].`, {
410
- prop: "clouds",
411
- validOptions: CLOUD_KEYS
412
- });
413
- for (const k of ["color", "shadeColor"]) if (c[k] !== void 0 && typeof c[k] !== "string") throw new IncantoError("BAD_FORMAT", `environment.clouds.${k} must be a hex color string, got ${JSON.stringify(c[k])}.`, { prop: "clouds" });
414
- const coverage = numberOr(c.coverage, .5, "clouds.coverage");
415
- if (coverage < 0 || coverage > 1) throw new IncantoError("BAD_FORMAT", `environment.clouds.coverage must be in [0, 1] (how much sky is cloudy), got ${coverage}.`, { prop: "clouds" });
416
- const density = numberOr(c.density, 1, "clouds.density");
417
- if (density < 0) throw new IncantoError("BAD_FORMAT", `environment.clouds.density must be >= 0 (optical thickness), got ${density}.`, { prop: "clouds" });
418
- const base = numberOr(c.base, 120, "clouds.base");
419
- const top = numberOr(c.top, 320, "clouds.top");
420
- if (top <= base) throw new IncantoError("BAD_FORMAT", `environment.clouds.top must be > base (got base ${base}, top ${top}).`, { prop: "clouds" });
421
- const speed = numberOr(c.speed, 1, "clouds.speed");
422
- const scale = numberOr(c.scale, 240, "clouds.scale");
423
- if (scale <= 0) throw new IncantoError("BAD_FORMAT", `environment.clouds.scale must be > 0 (feature size in world units), got ${scale}.`, { prop: "clouds" });
424
- return {
425
- coverage,
426
- density,
427
- base,
428
- top,
429
- color: c.color ?? "#ffffff",
430
- shadeColor: c.shadeColor ?? "#9fb0c8",
431
- speed,
432
- scale
433
- };
434
- }
435
- function parseBloom(value) {
436
- if (value === void 0) return null;
437
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.bloom must be an object ({ threshold?, strength? }), got ${JSON.stringify(value)}.`, { prop: "bloom" });
438
- const b = value;
439
- for (const key of Object.keys(b)) if (!BLOOM_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.bloom has unknown key '${key}'. Valid keys: [${BLOOM_KEYS.join(", ")}].`, {
440
- prop: "bloom",
441
- validOptions: BLOOM_KEYS
442
- });
443
- const threshold = numberOr(b.threshold, 1, "bloom.threshold");
444
- if (threshold < 0 || threshold > 8) throw new IncantoError("BAD_FORMAT", `environment.bloom.threshold must be in [0, 8] (linear-HDR luminance; 1 = white), got ${threshold}.`, { prop: "bloom" });
445
- const strength = numberOr(b.strength, .8, "bloom.strength");
446
- if (strength < 0) throw new IncantoError("BAD_FORMAT", `environment.bloom.strength must be >= 0, got ${strength}.`, { prop: "bloom" });
447
- return {
448
- threshold,
449
- strength
450
- };
451
- }
452
- function parsePost(value) {
453
- if (value === void 0) return null;
454
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.post must be an object ({ vignette?, saturation?, contrast? }), got ${JSON.stringify(value)}.`, { prop: "post" });
455
- const p = value;
456
- for (const key of Object.keys(p)) if (!POST_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.post has unknown key '${key}'. Valid keys: [${POST_KEYS.join(", ")}].`, {
457
- prop: "post",
458
- validOptions: POST_KEYS
459
- });
460
- const vignette = numberOr(p.vignette, 0, "post.vignette");
461
- if (vignette < 0 || vignette > 1) throw new IncantoError("BAD_FORMAT", `environment.post.vignette must be in [0, 1], got ${vignette}.`, { prop: "post" });
462
- const saturation = numberOr(p.saturation, 1, "post.saturation");
463
- if (saturation < 0 || saturation > 4) throw new IncantoError("BAD_FORMAT", `environment.post.saturation must be in [0, 4] (1 = neutral), got ${saturation}.`, { prop: "post" });
464
- const contrast = numberOr(p.contrast, 1, "post.contrast");
465
- if (contrast < .2 || contrast > 3) throw new IncantoError("BAD_FORMAT", `environment.post.contrast must be in [0.2, 3] (1 = neutral), got ${contrast}.`, { prop: "post" });
466
- return {
467
- vignette,
468
- saturation,
469
- contrast
470
- };
471
- }
472
- function parseShadows(value) {
473
- if (value === void 0) return null;
474
- if (value === false) return false;
475
- if (value === true) return {
476
- mapSize: 2048,
477
- radius: 1,
478
- static: false
479
- };
480
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new IncantoError("BAD_FORMAT", `environment.shadows must be true, false or an object ({ mapSize?, radius? }), got ${JSON.stringify(value)}.`, { prop: "shadows" });
481
- const shadows = value;
482
- for (const key of Object.keys(shadows)) if (!SHADOW_KEYS.includes(key)) throw new IncantoError("BAD_FORMAT", `environment.shadows has unknown key '${key}'. Valid keys: [${SHADOW_KEYS.join(", ")}].`, {
483
- prop: "shadows",
484
- validOptions: SHADOW_KEYS
485
- });
486
- const mapSize = shadows.mapSize === void 0 ? 2048 : shadows.mapSize;
487
- if (!SHADOW_MAP_SIZES.includes(mapSize)) throw new IncantoError("BAD_FORMAT", `environment.shadows.mapSize must be one of [${SHADOW_MAP_SIZES.join(", ")}], got ${JSON.stringify(shadows.mapSize)}.`, {
488
- prop: "shadows",
489
- validOptions: SHADOW_MAP_SIZES.map(String)
490
- });
491
- const radius = numberOr(shadows.radius, 1, "shadows.radius");
492
- if (radius < 0) throw new IncantoError("BAD_FORMAT", `environment.shadows.radius must be >= 0, got ${radius}.`, { prop: "shadows" });
493
- const staticShadows = shadows.static === void 0 ? false : shadows.static;
494
- if (typeof staticShadows !== "boolean") throw new IncantoError("BAD_FORMAT", `environment.shadows.static must be a boolean, got ${JSON.stringify(shadows.static)}.`, { prop: "shadows" });
495
- return {
496
- mapSize,
497
- radius,
498
- static: staticShadows
499
- };
500
- }
501
- function numberOr(value, fallback, at) {
502
- if (value === void 0) return fallback;
503
- if (typeof value !== "number" || !Number.isFinite(value)) throw new IncantoError("BAD_FORMAT", `environment.${at} must be a finite number, got ${JSON.stringify(value)}.`, { prop: at });
504
- return value;
505
- }
506
- function clamp(v, min, max) {
507
- return Math.min(Math.max(v, min), max);
508
- }
509
- function clamp01(v) {
510
- return clamp(v, 0, 1);
511
- }
512
- function lerp(a, b, t) {
513
- return a + (b - a) * t;
514
- }
515
- //#endregion
516
243
  //#region src/3d/environment-runtime.ts
517
244
  /**
518
245
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -2666,4 +2393,4 @@ function wireAudioUnlock(engine, root, canvas) {
2666
2393
  return detachAll;
2667
2394
  }
2668
2395
  //#endregion
2669
- export { Environment3D as a, parseEnvironment3D as c, AssetStore3D as d, syncTree as i, sunDirectionFromElevationAzimuth as l, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, horizonColorFromSky as s, createGame3D as t, sunDirectionFromSky as u };
2396
+ export { Environment3D as a, syncTree as i, create_game_exports as n, setEnvironment3D as o, Renderer3D as r, AssetStore3D as s, createGame3D as t };
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Pt as LogLevel, T as RendererStats, b as Engine } from "./behavior-DWKTUzKI.js";
1
+ import { Ft as LogLevel, T as RendererStats, b as Engine } from "./behavior-DsgayMsH.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
package/dist/debug.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { t as jsonClone } from "./json-CwwhxQgb.js";
2
- import { s as mergeStaticProps } from "./registry-WWcQcfMr.js";
2
+ import { s as mergeStaticProps } from "./registry-CF70EArN.js";
3
3
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
4
  //#region src/debug/capture.ts
5
5
  /**
@@ -1,4 +1,4 @@
1
- import { a as serializeNode, t as buildNodeJson } from "./loader-D8n7TU8W.js";
1
+ import { a as serializeNode, t as buildNodeJson } from "./loader-DEe272nY.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;