incanto 0.51.0 → 0.53.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 (67) hide show
  1. package/dist/2d.d.ts +86 -3
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +9 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-rZNfzVbH.d.ts → behavior-Do0Da56m.d.ts} +108 -0
  6. package/dist/{create-game-CkzKZ4v5.js → create-game-9D87XaiX.js} +112 -15
  7. package/dist/{create-game-BNaOC3Px.js → create-game-CDJ1lVqK.js} +21 -13
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/debug.js +1 -1
  10. package/dist/{duplicate-EybyYeyL.js → duplicate-MNLMAcbz.js} +1 -1
  11. package/dist/editor.js +1931 -1579
  12. package/dist/{environment-presets-C08pOC6H.js → environment-presets-SkGanr2s.js} +3 -3
  13. package/dist/{gameplay-bStgtZBV.js → gameplay-CRYw_Q-T.js} +6 -4
  14. package/dist/gameplay.d.ts +1 -1
  15. package/dist/gameplay.js +1 -1
  16. package/dist/index.d.ts +237 -5
  17. package/dist/index.js +9 -9
  18. package/dist/{loader-B2asghWa.js → loader-BTkHYrQn.js} +86 -50
  19. package/dist/{loader-Dkbn56KC.d.ts → loader-Cu_7kJDy.d.ts} +1 -1
  20. package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
  21. package/dist/net.d.ts +15 -410
  22. package/dist/net.js +1 -822
  23. package/dist/{pathfinding-Bz34pvQD.d.ts → pathfinding-HNGFqGUZ.d.ts} +24 -1
  24. package/dist/{physics-2d-Ccq2L9R0.js → physics-2d-BhZZ-HAp.js} +2 -2
  25. package/dist/{physics-3d-B2c5KNYh.js → physics-3d-DmyO2oaN.js} +3 -3
  26. package/dist/react.d.ts +1 -1
  27. package/dist/react.js +1 -1
  28. package/dist/{register-CGq-Hee8.js → register-BTomIiYG.js} +305 -25
  29. package/dist/{register-B_BaaGx-.js → register-DROK2l7J.js} +3 -3
  30. package/dist/{registry-IyWCGe4q.js → registry-C7u42TID.js} +23 -1
  31. package/dist/{replay-DIP2_as4.d.ts → replay-9Fy6C10F.d.ts} +1 -1
  32. package/dist/{replay-BgKxGcXH.js → replay-DYNUL4BU.js} +195 -2
  33. package/dist/split-screen-CaF9hO7g.js +1267 -0
  34. package/dist/split-screen-DNmcX1Pz.d.ts +441 -0
  35. package/dist/{src-C3UwzYXl.js → src-C9xyZW7M.js} +1 -1
  36. package/dist/{teardown-BKTCzLek.js → teardown-Bw2aeyGI.js} +32 -2
  37. package/dist/{test-D2gRpk5V.js → test-CJRsciFk.js} +119 -22
  38. package/dist/test.d.ts +60 -5
  39. package/dist/test.js +3 -3
  40. package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
  41. package/dist/vite.js +2 -2
  42. package/editor/assets/{agent8-DbX_msaO.js → agent8-PlFHzJsh.js} +1 -1
  43. package/editor/assets/{debug-C-kMxdl1.js → debug-CebV7CDW.js} +1 -1
  44. package/editor/assets/index-CdbsY31G.js +10958 -0
  45. package/editor/index.html +1 -1
  46. package/package.json +1 -1
  47. package/schemas/scene.schema.json +213 -0
  48. package/skills/incanto-assets.md +4 -1
  49. package/skills/incanto-audio.md +97 -10
  50. package/skills/incanto-building-2d-games.md +20 -3
  51. package/skills/incanto-editor.md +48 -11
  52. package/skills/incanto-gameplay-behaviors.md +14 -5
  53. package/skills/incanto-hud.md +7 -0
  54. package/skills/incanto-multiplayer.md +39 -3
  55. package/skills/incanto-node-reference.md +34 -0
  56. package/skills/incanto-physics-and-input.md +38 -0
  57. package/skills/incanto-verifying-your-game.md +75 -1
  58. package/skills/incanto-web-integration.md +1 -0
  59. package/templates/agent8-server.ts +79 -2
  60. package/templates-app/beacon-isle-3d/index.html +3 -3
  61. package/templates-app/beacon-isle-3d/package.json +1 -1
  62. package/templates-app/tps-3d/index.html +3 -3
  63. package/templates-app/tps-3d/package.json +1 -1
  64. package/templates-app/village-quest-3d/index.html +3 -3
  65. package/templates-app/village-quest-3d/package.json +1 -1
  66. package/dist/register-CLVhzWcI.js +0 -374
  67. package/editor/assets/index-CUc1U7wm.js +0 -10951
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-rZNfzVbH.js";
1
+ import { Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-Do0Da56m.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-Dkbn56KC.js";
4
+ import { t as LoadSceneOptions } from "./loader-Cu_7kJDy.js";
5
5
  import { n as AnimationEntry } from "./sprite-animation-CMr6f1K2.js";
6
6
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
7
7
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
@@ -36,6 +36,19 @@ declare class AssetStore2D {
36
36
  constructor(loader?: (url: string, callbacks?: TextureLoadCallbacks) => Texture, engine?: DiagnosticSink | null);
37
37
  /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
38
38
  load(assets: Record<string, JsonObject>): void;
39
+ /**
40
+ * Every declared asset that FAILED to load, `$ref` and url included.
41
+ *
42
+ * The 3D store has answered this since 0.32.0 — the 2D one only tracked a
43
+ * per-key status, which the console printed and nothing else could read. A
44
+ * 404'd texture is invisible, and "invisible" is the one symptom an author
45
+ * cannot tell apart from "I put it in the wrong place".
46
+ */
47
+ errors(): Array<{
48
+ ref: string;
49
+ url: string;
50
+ error: string;
51
+ }>;
39
52
  /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
40
53
  status(key: string): AssetStatus | undefined;
41
54
  getTexture(ref: string): Texture;
@@ -291,7 +304,21 @@ interface CreateGame2DOptions {
291
304
  * false disables. They overlay `touchContainer` (default: the canvas's
292
305
  * parent element — give it position: relative).
293
306
  */
307
+ /**
308
+ * Attach pointer input (mouse, finger or pen) to the canvas: positions for
309
+ * `input.pointerPosition()`, buttons for `input.mousePressed()`.
310
+ * `{ lockOnClick: true }` captures the cursor — rare in 2D, so it is off.
311
+ */
312
+ pointer?: boolean | {
313
+ lockOnClick?: boolean;
314
+ };
294
315
  touch?: "auto" | boolean;
316
+ /**
317
+ * Leave the browser's touch gestures (scroll/zoom/pull-to-refresh) on the
318
+ * canvas. Default false: the GAME owns them, or a drag on a phone scrolls
319
+ * the page instead of playing.
320
+ */
321
+ pageGestures?: boolean;
295
322
  touchContainer?: HTMLElement;
296
323
  /** @internal Test seam — replaces `document` for the touch overlay. */
297
324
  _touchDoc?: {
@@ -331,10 +358,32 @@ interface CreateGame2DOptions {
331
358
  _scheduler?: Scheduler;
332
359
  }
333
360
  /** What createGame needs from a renderer (stats is optional for test stubs). */
361
+ /** What a renderer must do for the engine. The test seam supplies this much. */
334
362
  interface GameRenderer {
335
363
  dispose(): void;
336
364
  stats?(): RendererStats;
337
365
  }
366
+ /**
367
+ * What a GAME can ask its renderer for.
368
+ *
369
+ * The two coordinate conversions live here rather than only on `Renderer2D`,
370
+ * because `game.renderer` is what a consumer holds: a tap needs screen→world
371
+ * and a pinned DOM element needs world→screen, both are documented in the
372
+ * skills, and neither typechecked through the narrow interface. Found by
373
+ * following my own documentation in a fresh project.
374
+ */
375
+ interface Game2DRenderer extends GameRenderer {
376
+ /** Canvas px → world px, through the design viewport and the camera. */
377
+ worldFromScreen(sx: number, sy: number): {
378
+ x: number;
379
+ y: number;
380
+ };
381
+ /** World px → canvas px (pin DOM to a node). */
382
+ screenFromWorld(wx: number, wy: number): {
383
+ x: number;
384
+ y: number;
385
+ };
386
+ }
338
387
  interface Game2D {
339
388
  engine: Engine;
340
389
  scene: Scene$1;
@@ -343,13 +392,25 @@ interface Game2D {
343
392
  * A running tree has moved; the editor gets what was written down.
344
393
  */
345
394
  sourceJson: SceneJson;
346
- renderer: GameRenderer;
395
+ renderer: Game2DRenderer;
347
396
  physics: Physics2D | null;
348
397
  /**
349
398
  * The node under a screen pixel, or null. The raycast the renderer already
350
399
  * does — reachable now without holding the renderer.
351
400
  */
352
401
  pick(x: number, y: number): Node | null;
402
+ /**
403
+ * Assets that did not arrive — a texture that 404'd, a sound that cannot be
404
+ * played — with the ref and the reason.
405
+ *
406
+ * The verify loop names this as one of its four signals, and 2D never had it:
407
+ * a 2D game could not ask the question at all.
408
+ */
409
+ assetErrors(): Array<{
410
+ ref: string;
411
+ url: string;
412
+ error: string;
413
+ }>;
353
414
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
354
415
  stats(): GameStats;
355
416
  /**
@@ -841,6 +902,8 @@ declare class Renderer2D {
841
902
  private syncSelectionOutline;
842
903
  private syncDebugLines;
843
904
  private render;
905
+ /** UI-pass extent from the last render — the space HUD nodes are posed in. */
906
+ private lastUi;
844
907
  private lastViewport;
845
908
  private lastView;
846
909
  /** The world view used by the LAST render (center/zoom/canvas size). */
@@ -863,6 +926,26 @@ declare class Renderer2D {
863
926
  * Hits resolve through `userData.incantoNode` and prefer higher renderOrder.
864
927
  */
865
928
  pick(sx: number, sy: number): Node | null;
929
+ private hit;
930
+ /** Does the UI pass draw this node (i.e. is it under a UILayer)? */
931
+ isUiNode(node: Node): boolean;
932
+ /**
933
+ * UI-space px → canvas px.
934
+ *
935
+ * A UILayer's children are posed in the UI pass's own space (origin top-left,
936
+ * `uiW × uiH`), NOT in world space — so mapping them with `screenFromWorld`
937
+ * puts them wherever the game camera happens to be looking.
938
+ */
939
+ screenFromUi(ux: number, uy: number): {
940
+ x: number;
941
+ y: number;
942
+ };
943
+ /** Canvas px → UI-space px (the inverse of `screenFromUi`). */
944
+ uiFromScreen(sx: number, sy: number): {
945
+ x: number;
946
+ y: number;
947
+ };
948
+ private uiView;
866
949
  /**
867
950
  * Canvas-pixel AABB of a node's rendered world-pass objects, or null when it
868
951
  * has no renderable extent (plain Nodes, empty containers).
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-CkzKZ4v5.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-B_BaaGx-.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-Ccq2L9R0.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-9D87XaiX.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-DROK2l7J.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-BhZZ-HAp.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * What a `CharacterController2D`/`3D` will ask a skin to play, and the clip in
package/dist/3d.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { D as QualityCaps, Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-rZNfzVbH.js";
1
+ import { D as QualityCaps, Dt as Node, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, n as BehaviorCtor, w as GameStats } from "./behavior-Do0Da56m.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-DAvWQeld.js";
3
3
  import { i as SceneJson$1, s as JsonObject } from "./schema-CFeioQRE.js";
4
- import { t as LoadSceneOptions } from "./loader-Dkbn56KC.js";
4
+ import { t as LoadSceneOptions } from "./loader-Cu_7kJDy.js";
5
5
  import { t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
6
  import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DZ70IY26.js";
7
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-Bz34pvQD.js";
7
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-HNGFqGUZ.js";
8
8
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
9
9
  import { VRM } from "@pixiv/three-vrm";
10
10
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -563,6 +563,12 @@ interface CreateGame3DOptions {
563
563
  * parent element — give it position: relative).
564
564
  */
565
565
  touch?: "auto" | boolean;
566
+ /**
567
+ * Leave the browser's touch gestures (scroll/zoom/pull-to-refresh) on the
568
+ * canvas. Default false: the GAME owns them, or a drag on a phone scrolls
569
+ * the page instead of playing.
570
+ */
571
+ pageGestures?: boolean;
566
572
  touchContainer?: HTMLElement;
567
573
  /** @internal Test seam — replaces `document` for the touch overlay. */
568
574
  _touchDoc?: {
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
- import { F as CharacterBody3D, H as WATER_MAX_RIPPLES, I as PhysicsBody3D, L as RigidBody3D, M as WATER_CUTOUT_MAX, N as WaterCutout3D, P as Area3D, R as StaticBody3D, j as Water3D, z as Node3D } from "./gameplay-bStgtZBV.js";
2
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-C08pOC6H.js";
3
- import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-BNaOC3Px.js";
1
+ import { F as CharacterBody3D, H as WATER_MAX_RIPPLES, I as PhysicsBody3D, L as RigidBody3D, M as WATER_CUTOUT_MAX, N as WaterCutout3D, P as Area3D, R as StaticBody3D, j as Water3D, z as Node3D } from "./gameplay-CRYw_Q-T.js";
2
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, 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, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ 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-SkGanr2s.js";
3
+ import { a as Environment3D, c as parseEnvironment3D, d as AssetStore3D, i as syncTree, l as sunDirectionFromElevationAzimuth, o as setEnvironment3D, r as Renderer3D, s as horizonColorFromSky, t as createGame3D, u as sunDirectionFromSky } from "./create-game-CDJ1lVqK.js";
4
4
  import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-njybhZon.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-B2c5KNYh.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DmyO2oaN.js";
7
7
  //#region src/3d/model-verdict.ts
8
8
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
9
9
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -325,9 +325,42 @@ type BusName = "sfx" | "music";
325
325
  * engine.audio.muted = true; // mute toggle
326
326
  * ```
327
327
  */
328
+ /** One thing that sounded (or would have, headless). */
329
+ interface AudioEvent {
330
+ /** `preset` (procedural one-shot), `src` (a clip), `music`, or `voice`. */
331
+ kind: "preset" | "src" | "music" | "voice";
332
+ /** The preset name, the clip url, or the voice preset. */
333
+ name: string;
334
+ /** Node path for a node-driven sound; `engine.music` / `engine.sfx` otherwise. */
335
+ from: string;
336
+ bus: BusName;
337
+ /** `engine.time` when it fired, in seconds. */
338
+ at: number;
339
+ }
328
340
  declare class AudioBuses {
329
341
  /** Fires whenever any value changes (drives live re-gain of playing sounds). */
330
342
  readonly changed: Signal<[]>;
343
+ /**
344
+ * What has sounded, most recent last.
345
+ *
346
+ * Audio is a no-op headless, which left "did the coin sound actually fire
347
+ * when the coin was collected?" unanswerable — the audio skill's own advice
348
+ * was to *"assert `play()` doesn't throw and check your gameplay logic
349
+ * instead"*, which checks something else. The CALL still happens with no
350
+ * backend, so recording it answers the wiring question everywhere: in the
351
+ * verify VM, in a playtest, in a test.
352
+ *
353
+ * Bounded (200) — a game running for an hour must not grow a log.
354
+ */
355
+ private readonly events;
356
+ /** @internal Called by the players; games read `recent()`. */
357
+ record(event: AudioEvent): void;
358
+ /** The last `limit` sounds, oldest first. */
359
+ recent(limit?: number): AudioEvent[];
360
+ /** How many times a preset/clip/track sounded — the assertion you want. */
361
+ countOf(name: string): number;
362
+ /** Forget what has sounded so far (between scenes, or between assertions). */
363
+ clearLog(): void;
331
364
  private _master;
332
365
  private _sfx;
333
366
  private _music;
@@ -377,6 +410,8 @@ interface MusicBackend {
377
410
  createTrack(src: string): MusicTrack;
378
411
  /** Resume a gesture-suspended context (wired to the first user gesture). */
379
412
  unlock(): void;
413
+ /** Release the context it owns (optional — teardown). */
414
+ dispose?(): void;
380
415
  }
381
416
  interface PlayMusicOptions {
382
417
  /** Loop the track (default true — music usually loops). */
@@ -420,6 +455,15 @@ declare class MusicManager {
420
455
  get current(): string | null;
421
456
  /** Per-manager volume (0..1), on top of the bus gain. */
422
457
  get volume(): number;
458
+ /**
459
+ * The backend, or an INERT one that mints silent tracks.
460
+ *
461
+ * Never null, on purpose: the manager used to return before touching its
462
+ * state when there was no backend, so `current` stayed null forever in every
463
+ * headless run — including the verify VM, which is the only place a game is
464
+ * checked automatically. "Did the boss music start?" had no answer anywhere.
465
+ * With an inert backend the state machine runs identically and nothing plays.
466
+ */
423
467
  private ensure;
424
468
  /** Resume a gesture-suspended context + (re)start any pending track. */
425
469
  unlock(): void;
@@ -432,6 +476,13 @@ declare class MusicManager {
432
476
  crossfadeTo(src: string, seconds?: number): void;
433
477
  /** Stop the current track, optionally fading out over `fadeOut` seconds. */
434
478
  stop(fadeOut?: number): void;
479
+ /**
480
+ * Stop instantly and release the backend — music must not outlive the game.
481
+ *
482
+ * `dispose()` is the one call that tears an SPA-mounted game down, and it used
483
+ * to leave the track playing with the engine that owned it gone.
484
+ */
485
+ dispose(): void;
435
486
  /** Set the per-manager volume (re-applied to live tracks on the next tick). */
436
487
  setVolume(v: number): void;
437
488
  /** Advance fades by `dt` seconds and (re)apply bus×fade gains. Frame-driven. */
@@ -588,6 +639,14 @@ interface Voice {
588
639
  //#endregion
589
640
  //#region src/core/audio/webaudio-sfx.d.ts
590
641
  /** A spatial emitter: where the sound is + where the listener is. */
642
+ /** The shape `AudioBuses.record` takes (structural — no import cycle). */
643
+ interface AudioRecord {
644
+ kind: "preset" | "src" | "music" | "voice";
645
+ name: string;
646
+ from: string;
647
+ bus: "sfx" | "music";
648
+ at: number;
649
+ }
591
650
  interface SpatialPlay {
592
651
  /** Emitter world position. */
593
652
  position: Vec3;
@@ -615,6 +674,19 @@ declare class SfxEngine {
615
674
  private ctx;
616
675
  private resolved;
617
676
  private readonly pcmCache;
677
+ /**
678
+ * The engine's buses, when this SfxEngine belongs to one — so a continuous
679
+ * voice lands in the same audio record as everything else. A record that
680
+ * covered only SOME sounds would answer "did the engine hum start?" with a
681
+ * confident no.
682
+ */
683
+ private buses;
684
+ /** Live continuous voices, so teardown can silence the ones nobody stopped. */
685
+ private readonly voices;
686
+ /** @internal Engine wiring. */
687
+ _setBuses(buses: {
688
+ record(event: AudioRecord): void;
689
+ }): void;
618
690
  /** True once a backend is present (lazily created on first use). */
619
691
  get available(): boolean;
620
692
  private ensure;
@@ -624,6 +696,18 @@ declare class SfxEngine {
624
696
  * done. Headless returns an inert handle (same call sites, no ifs).
625
697
  */
626
698
  startVoice(preset: VoicePreset, gain?: number): Voice;
699
+ /** How many continuous voices are still running (teardown/leak checks). */
700
+ get liveVoiceCount(): number;
701
+ /** Silence every continuous voice — the hum must not outlive the game. */
702
+ stopAll(seconds?: number): void;
703
+ /**
704
+ * Silence everything and release the AudioContext.
705
+ *
706
+ * A browser allows only a handful of contexts per page, so an SPA that mounts
707
+ * the game a few times must hand this one back — `dispose()` is documented as
708
+ * the call that tears everything down.
709
+ */
710
+ dispose(): void;
627
711
  /** Resume a gesture-suspended context (wired to the first user gesture). */
628
712
  unlock(): void;
629
713
  /**
@@ -760,6 +844,20 @@ declare class InputMap {
760
844
  connect(fn: () => void): () => void;
761
845
  };
762
846
  }): () => void;
847
+ /**
848
+ * Pointer input from a mouse, a FINGER or a pen — one code path.
849
+ *
850
+ * This used to bind `mousedown`/`mousemove`/`mouseup`, which on a phone only
851
+ * arrive as COMPATIBILITY events: delayed until the browser has decided the
852
+ * touch is not a scroll, never fired for drags or multi-touch, and suppressed
853
+ * outright once the surface claims its gestures with `touch-action: none`.
854
+ * So every genre the comment below lists — match-3, tower defense, card,
855
+ * point-and-click, RTS — was mouse-only on the device most of them are played
856
+ * on. Pointer events cover all three input kinds and fire immediately.
857
+ *
858
+ * Only the PRIMARY pointer drives it: a second finger (the one on the virtual
859
+ * stick) must not also swing the camera.
860
+ */
763
861
  attachPointer(target: HTMLElement, opts?: {
764
862
  lockOnClick?: boolean;
765
863
  }): () => void;
@@ -1581,6 +1679,16 @@ interface PropDef {
1581
1679
  */
1582
1680
  nodePath?: boolean;
1583
1681
  /**
1682
+ * The prop must be given a value — an empty string is not a configuration.
1683
+ *
1684
+ * `Chase.target` and `FollowCamera.target` threw for this in `onReady`, which
1685
+ * is the browser, at the moment the level starts. Declared here it is a LOAD
1686
+ * error instead: `incanto-check` reports it while you are still writing the
1687
+ * scene, and the editor knows not to "unlink" a prop whose emptiness the
1688
+ * engine refuses.
1689
+ */
1690
+ required?: boolean;
1691
+ /**
1584
1692
  * For an OBJECT-valued prop shaped as a tagged union — `collider`, whose
1585
1693
  * `shape` decides which other keys are required. One declaration gives an
1586
1694
  * inspector both the dropdown AND something valid to write when the tag
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport, w as diagnose } from "./loader-B2asghWa.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-CGq-Hee8.js";
2
+ import { A as registerBehavior, C as diagnose, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-BTkHYrQn.js";
3
+ import { b as AudioPlayer, x as Engine } from "./register-BTomIiYG.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
- import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-bStgtZBV.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-B_BaaGx-.js";
5
+ import { i as resolveRendering, n as attachTouchControls } from "./touch-BnMyy9tr.js";
6
+ import { a as openBundledEditor, c as audioErrors, i as devServerLibrary, n as pauseWhenHidden, r as crossFade, s as claimCanvasGestures, t as teardown } from "./teardown-Bw2aeyGI.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CRYw_Q-T.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-DROK2l7J.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-Ccq2L9R0.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-BhZZ-HAp.js";
11
11
  import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
12
  //#region src/2d/assets.ts
13
13
  /**
@@ -50,6 +50,7 @@ var AssetStore2D = class {
50
50
  },
51
51
  onError: (err) => {
52
52
  state.status = "error";
53
+ state.error = err instanceof Error ? err.message : `could not load ${decl.url}`;
53
54
  diagnose(this.engine, "error", `[incanto] failed to load texture '${key}' (${decl.url}):`, err);
54
55
  }
55
56
  });
@@ -66,10 +67,31 @@ var AssetStore2D = class {
66
67
  type,
67
68
  frameWidth,
68
69
  frameHeight,
70
+ url: decl.url,
69
71
  state
70
72
  });
71
73
  }
72
74
  }
75
+ /**
76
+ * Every declared asset that FAILED to load, `$ref` and url included.
77
+ *
78
+ * The 3D store has answered this since 0.32.0 — the 2D one only tracked a
79
+ * per-key status, which the console printed and nothing else could read. A
80
+ * 404'd texture is invisible, and "invisible" is the one symptom an author
81
+ * cannot tell apart from "I put it in the wrong place".
82
+ */
83
+ errors() {
84
+ const out = [];
85
+ for (const [key, entry] of this.entries) {
86
+ if (entry.state.status !== "error") continue;
87
+ out.push({
88
+ ref: `$${key}`,
89
+ url: entry.url,
90
+ error: entry.state.error ?? `could not load ${entry.url}`
91
+ });
92
+ }
93
+ return out;
94
+ }
73
95
  /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
74
96
  status(key) {
75
97
  return this.entries.get(key)?.state.status;
@@ -107,6 +129,18 @@ function worldFromScreen(view, sx, sy) {
107
129
  y: (sy - view.h / 2) / view.zoom + view.cy
108
130
  };
109
131
  }
132
+ function screenFromUi(v, ux, uy) {
133
+ return {
134
+ x: v.offsetX + ux / v.uiW * v.contentW,
135
+ y: v.offsetY + uy / v.uiH * v.contentH
136
+ };
137
+ }
138
+ function uiFromScreen(v, sx, sy) {
139
+ return {
140
+ x: (sx - v.offsetX) / v.contentW * v.uiW,
141
+ y: (sy - v.offsetY) / v.contentH * v.uiH
142
+ };
143
+ }
110
144
  //#endregion
111
145
  //#region src/2d/selection-outline.ts
112
146
  /** 2D twin of the 3D selection outline: a world-space (px, y-down) rectangle
@@ -355,6 +389,10 @@ var Renderer2D = class {
355
389
  const view = vp ? computeViewport(w, h, vp) : null;
356
390
  const uiW = view ? view.viewW : w;
357
391
  const uiH = view ? view.viewH : h;
392
+ this.lastUi = {
393
+ w: uiW,
394
+ h: uiH
395
+ };
358
396
  const { activeCamera } = syncTree2D(scene.root, this.worldScene, this.uiScene, this.assets, {
359
397
  width: uiW,
360
398
  height: uiH
@@ -406,6 +444,11 @@ var Renderer2D = class {
406
444
  this.webgl.render(this.uiScene, this.uiCam);
407
445
  this.webgl.autoClear = true;
408
446
  }
447
+ /** UI-pass extent from the last render — the space HUD nodes are posed in. */
448
+ lastUi = {
449
+ w: 1,
450
+ h: 1
451
+ };
409
452
  lastViewport = null;
410
453
  lastView = {
411
454
  cx: 0,
@@ -448,8 +491,13 @@ var Renderer2D = class {
448
491
  const cw = this.lastViewport?.contentW ?? w;
449
492
  const ch = this.lastViewport?.contentH ?? h;
450
493
  pickNdc.set((sx - ox) / cw * 2 - 1, -((sy - oy) / ch * 2 - 1));
451
- raycaster.setFromCamera(pickNdc, this.worldCam);
452
- const hits = raycaster.intersectObjects(this.worldScene.children, true);
494
+ const ui = this.hit(this.uiScene, this.uiCam);
495
+ if (ui) return ui;
496
+ return this.hit(this.worldScene, this.worldCam);
497
+ }
498
+ hit(scene, cam) {
499
+ raycaster.setFromCamera(pickNdc, cam);
500
+ const hits = raycaster.intersectObjects(scene.children, true);
453
501
  let best = null;
454
502
  for (const hit of hits) {
455
503
  let obj = hit.object;
@@ -464,6 +512,39 @@ var Renderer2D = class {
464
512
  }
465
513
  return best?.node ?? null;
466
514
  }
515
+ /** Does the UI pass draw this node (i.e. is it under a UILayer)? */
516
+ isUiNode(node) {
517
+ let obj = node._object2D ?? node._group ?? null;
518
+ while (obj) {
519
+ if (obj === this.uiScene) return true;
520
+ obj = obj.parent;
521
+ }
522
+ return false;
523
+ }
524
+ /**
525
+ * UI-space px → canvas px.
526
+ *
527
+ * A UILayer's children are posed in the UI pass's own space (origin top-left,
528
+ * `uiW × uiH`), NOT in world space — so mapping them with `screenFromWorld`
529
+ * puts them wherever the game camera happens to be looking.
530
+ */
531
+ screenFromUi(ux, uy) {
532
+ return screenFromUi(this.uiView(), ux, uy);
533
+ }
534
+ /** Canvas px → UI-space px (the inverse of `screenFromUi`). */
535
+ uiFromScreen(sx, sy) {
536
+ return uiFromScreen(this.uiView(), sx, sy);
537
+ }
538
+ uiView() {
539
+ return {
540
+ offsetX: this.lastViewport?.offsetX ?? 0,
541
+ offsetY: this.lastViewport?.offsetY ?? 0,
542
+ contentW: this.lastViewport?.contentW ?? this.lastView.w,
543
+ contentH: this.lastViewport?.contentH ?? this.lastView.h,
544
+ uiW: this.lastUi.w,
545
+ uiH: this.lastUi.h
546
+ };
547
+ }
467
548
  /**
468
549
  * Canvas-pixel AABB of a node's rendered world-pass objects, or null when it
469
550
  * has no renderable extent (plain Nodes, empty containers).
@@ -476,8 +557,9 @@ var Renderer2D = class {
476
557
  if (boundsScratch.isEmpty()) return null;
477
558
  boundsScratch.getSize(sizeScratch3);
478
559
  if (sizeScratch3.x === 0 && sizeScratch3.y === 0) return null;
479
- const a = this.screenFromWorld(boundsScratch.min.x, -boundsScratch.max.y);
480
- const b = this.screenFromWorld(boundsScratch.max.x, -boundsScratch.min.y);
560
+ const map = this.isUiNode(node) ? (x, y) => this.screenFromUi(x, y) : (x, y) => this.screenFromWorld(x, y);
561
+ const a = map(boundsScratch.min.x, -boundsScratch.max.y);
562
+ const b = map(boundsScratch.max.x, -boundsScratch.min.y);
481
563
  return {
482
564
  x: a.x,
483
565
  y: a.y,
@@ -543,10 +625,15 @@ async function createGame2D(opts) {
543
625
  const cleanups = [];
544
626
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
545
627
  if (keyboard) engine.input.attachKeyboard(keyboard);
628
+ if (opts.pointer) {
629
+ const lockOnClick = typeof opts.pointer === "object" ? opts.pointer.lockOnClick ?? false : false;
630
+ engine.input.attachPointer(opts.canvas, { lockOnClick });
631
+ }
546
632
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
547
633
  cleanups.push(engine.settings.bindAudio(engine.audio));
548
634
  cleanups.push(pauseWhenHidden(engine));
549
635
  cleanups.push(engine.settings.bindLocale(engine.locale));
636
+ if (!opts.pageGestures) claimCanvasGestures(opts.canvas);
550
637
  const touchMode = opts.touch ?? "auto";
551
638
  if (touchMode !== false) {
552
639
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -636,6 +723,9 @@ async function createGame2D(opts) {
636
723
  pick(x, y) {
637
724
  return engine.picker?.(x, y) ?? null;
638
725
  },
726
+ assetErrors() {
727
+ return [...renderer.assets?.errors() ?? [], ...audioErrors(engine.scene?.tree.root)];
728
+ },
639
729
  stats() {
640
730
  return {
641
731
  triangles: 0,
@@ -673,10 +763,17 @@ function wireAudioUnlock(engine, root, canvas) {
673
763
  target: canvasTarget,
674
764
  type: "pointerdown"
675
765
  });
676
- if (typeof window !== "undefined") sources.push({
677
- target: window,
678
- type: "keydown"
679
- });
766
+ if (typeof window !== "undefined") {
767
+ const win = window;
768
+ sources.push({
769
+ target: win,
770
+ type: "keydown"
771
+ });
772
+ sources.push({
773
+ target: win,
774
+ type: "pointerdown"
775
+ });
776
+ }
680
777
  if (sources.length === 0) return () => {};
681
778
  function detachAll() {
682
779
  for (const s of sources) s.target.removeEventListener?.(s.type, unlock);
@@ -1,15 +1,15 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as registerBehavior, n as loadScene, w as diagnose } from "./loader-B2asghWa.js";
3
- import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-CGq-Hee8.js";
2
+ import { A as registerBehavior, C as diagnose, n as loadScene } from "./loader-BTkHYrQn.js";
3
+ import { C as qualityCaps, T as qualityRendering, b as AudioPlayer, x as Engine } from "./register-BTomIiYG.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
- import { r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
6
- import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
7
- import { a as openBundledEditor, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, t as teardown } from "./teardown-BKTCzLek.js";
8
- import { I as PhysicsBody3D, V as createCausticsQuad, n as registerGameplayBehaviors, z as Node3D } from "./gameplay-bStgtZBV.js";
5
+ import { r as parseDrive, t as logReport } from "./log-report-CPFm4OXf.js";
6
+ import { i as resolveRendering, n as attachTouchControls } from "./touch-BnMyy9tr.js";
7
+ import { a as openBundledEditor, c as audioErrors, i as devServerLibrary, n as pauseWhenHidden, o as poseFromRenderer, r as crossFade, s as claimCanvasGestures, t as teardown } from "./teardown-Bw2aeyGI.js";
8
+ import { I as PhysicsBody3D, V as createCausticsQuad, n as registerGameplayBehaviors, z as Node3D } from "./gameplay-CRYw_Q-T.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-C08pOC6H.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-SkGanr2s.js";
11
11
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-njybhZon.js";
12
- import { n as enablePhysics3D } from "./physics-3d-B2c5KNYh.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-DmyO2oaN.js";
13
13
  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";
14
14
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
15
15
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -2349,6 +2349,7 @@ async function createGame3D(opts) {
2349
2349
  engine.input.attachPointer(opts.canvas, { lockOnClick });
2350
2350
  }
2351
2351
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
2352
+ if (!opts.pageGestures) claimCanvasGestures(opts.canvas);
2352
2353
  const touchMode = opts.touch ?? "auto";
2353
2354
  if (touchMode !== false) {
2354
2355
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -2575,7 +2576,7 @@ async function createGame3D(opts) {
2575
2576
  renderer,
2576
2577
  physics,
2577
2578
  assetErrors() {
2578
- return renderer.assets?.errors() ?? [];
2579
+ return [...renderer.assets?.errors() ?? [], ...audioErrors(engine.scene?.tree.root)];
2579
2580
  },
2580
2581
  async frame(frameOpts) {
2581
2582
  const capture = renderer.captureFrame;
@@ -2623,10 +2624,17 @@ function wireAudioUnlock(engine, root, canvas) {
2623
2624
  target: canvasTarget,
2624
2625
  type: "pointerdown"
2625
2626
  });
2626
- if (typeof window !== "undefined") sources.push({
2627
- target: window,
2628
- type: "keydown"
2629
- });
2627
+ if (typeof window !== "undefined") {
2628
+ const win = window;
2629
+ sources.push({
2630
+ target: win,
2631
+ type: "keydown"
2632
+ });
2633
+ sources.push({
2634
+ target: win,
2635
+ type: "pointerdown"
2636
+ });
2637
+ }
2630
2638
  if (sources.length === 0) return () => {};
2631
2639
  function detachAll() {
2632
2640
  for (const s of sources) s.target.removeEventListener?.(s.type, unlock);
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as RendererStats, b as Engine, jt as LogLevel } from "./behavior-rZNfzVbH.js";
1
+ import { T as RendererStats, b as Engine, jt as LogLevel } from "./behavior-Do0Da56m.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-BLk7H2Qa.js";
2
- import { s as mergeStaticProps } from "./registry-IyWCGe4q.js";
2
+ import { s as mergeStaticProps } from "./registry-C7u42TID.js";
3
3
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
4
4
  //#region src/debug/capture.ts
5
5
  /**