incanto 0.52.0 → 0.54.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/bin/incanto-check.mjs +56 -1
  2. package/dist/2d.d.ts +95 -4
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +111 -6
  5. package/dist/3d.js +10 -6
  6. package/dist/{behavior-uPEuZrUB.d.ts → behavior-CyQoSu4n.d.ts} +212 -10
  7. package/dist/{create-game-B_e9hJf7.js → create-game-BRt6XKmP.js} +31 -12
  8. package/dist/{create-game-CGnoypjL.js → create-game-Czzp6ZuE.js} +26 -14
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/debug.js +1 -1
  11. package/dist/{duplicate-B-OtSRFL.js → duplicate-MNLMAcbz.js} +1 -1
  12. package/dist/editor.js +14 -0
  13. package/dist/{environment-presets-DSZwsKPs.js → environment-presets-BQ_QsIBY.js} +222 -11
  14. package/dist/{gameplay-B6jqvYeM.js → gameplay-CZ2yq37J.js} +32 -23
  15. package/dist/gameplay.d.ts +8 -3
  16. package/dist/gameplay.js +1 -1
  17. package/dist/index.d.ts +178 -6
  18. package/dist/index.js +9 -9
  19. package/dist/{loader-B-Gft32x.js → loader-BTkHYrQn.js} +54 -18
  20. package/dist/{loader-B9iTqs27.d.ts → loader-DhI1jFW_.d.ts} +1 -1
  21. package/dist/{log-report-lxrQY9cH.js → log-report-CPFm4OXf.js} +0 -0
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +1 -1
  24. package/dist/{particle-sim-BzJ1yxoE.d.ts → particle-sim-C5OfBbmU.d.ts} +11 -0
  25. package/dist/{pathfinding-CAR9DjQQ.d.ts → pathfinding-CXGCpRQe.d.ts} +24 -1
  26. package/dist/{physics-2d-Dns-oZlE.js → physics-2d-CfWAggJ1.js} +2 -2
  27. package/dist/{physics-3d-BhI0ehpe.js → physics-3d-Bes-GIuR.js} +3 -3
  28. package/dist/react.d.ts +1 -1
  29. package/dist/react.js +1 -1
  30. package/dist/{register-Trx7WHnD.js → register-en63AEZO.js} +163 -4
  31. package/dist/{register-DVwlnZAZ.js → register-p48lHE2o.js} +433 -36
  32. package/dist/{replay-BU1CCM15.d.ts → replay-ePMz26jw.d.ts} +1 -1
  33. package/dist/{replay-BicPOMX0.js → replay-t1pP0gQg.js} +2 -2
  34. package/dist/{split-screen-paxkQs_q.d.ts → split-screen-BsdOHbzP.d.ts} +1 -1
  35. package/dist/{split-screen-CZ9ccBBQ.js → split-screen-CSb_uZ6W.js} +2 -2
  36. package/dist/{sprite-animation-D_p28jwU.js → sprite-animation-7qvUxF6Z.js} +19 -0
  37. package/dist/{src-5gbZO47I.js → src-vNPQeQ1W.js} +1 -1
  38. package/dist/{teardown-ks3d5W9n.js → teardown-C7uVSJvx.js} +30 -1
  39. package/dist/{test-DAojuFdb.js → test-Tnf1nQl3.js} +25 -13
  40. package/dist/test.d.ts +4 -4
  41. package/dist/test.js +2 -2
  42. package/dist/{touch-BoNg_MnF.js → touch-BnMyy9tr.js} +4 -4
  43. package/dist/vite.js +2 -2
  44. package/editor/assets/{agent8-CCvckvbw.js → agent8-BWaW-D85.js} +1 -1
  45. package/editor/assets/{debug-CLNCOnbc.js → debug-DLF8rUtr.js} +2 -2
  46. package/editor/assets/{index-Cb5Brupb.js → index-BYfiwbQx.js} +91 -91
  47. package/editor/index.html +1 -1
  48. package/package.json +1 -1
  49. package/schemas/scene.schema.json +221 -0
  50. package/skills/incanto-assets.md +45 -2
  51. package/skills/incanto-audio.md +97 -10
  52. package/skills/incanto-building-2d-games.md +55 -3
  53. package/skills/incanto-building-3d-games.md +3 -1
  54. package/skills/incanto-environment.md +3 -1
  55. package/skills/incanto-gameplay-behaviors.md +51 -4
  56. package/skills/incanto-hud.md +7 -0
  57. package/skills/incanto-node-reference.md +36 -0
  58. package/skills/incanto-performance.md +47 -0
  59. package/skills/incanto-physics-and-input.md +38 -0
  60. package/skills/incanto-verifying-your-game.md +50 -1
  61. package/skills/incanto-web-integration.md +1 -0
  62. package/templates-app/beacon-isle-3d/index.html +3 -3
  63. package/templates-app/beacon-isle-3d/package.json +1 -1
  64. package/templates-app/tps-3d/index.html +3 -3
  65. package/templates-app/tps-3d/package.json +1 -1
  66. package/templates-app/village-quest-3d/index.html +3 -3
  67. package/templates-app/village-quest-3d/package.json +1 -1
@@ -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
  /**
@@ -634,6 +718,57 @@ declare class SfxEngine {
634
718
  play(params: SfxParams, gain: number, opts?: SfxPlayOptions): void;
635
719
  }
636
720
  //#endregion
721
+ //#region src/core/effects-log.d.ts
722
+ /**
723
+ * What SHOWED — the visual half of the audio record.
724
+ *
725
+ * A game's feedback is sound and vision, and only one of them could be checked.
726
+ * `engine.audio.countOf('coin')` answers "did the coin sound fire?"; nothing
727
+ * answered "did the explosion?". `framing()` says where an emitter IS, which is
728
+ * a different question — a particle system that never fired and one that fired
729
+ * a hundred times sit at the same coordinates.
730
+ *
731
+ * Nothing here renders, so nothing here is measured in pixels. It records that
732
+ * the effect WAS ASKED FOR, which is the wiring question a game actually has:
733
+ * the burst that never happened because the signal was never connected looks
734
+ * exactly like the burst that happened off-screen, and this tells them apart.
735
+ *
736
+ * ```ts
737
+ * session.engine.effects.clearLog();
738
+ * smashTheCrystal();
739
+ * session.step(200);
740
+ * session.engine.effects.countOf('explosion'); // 1
741
+ * session.engine.effects.countFrom('/Game/Crystal/Boom'); // 1
742
+ * ```
743
+ */
744
+ /** The kinds of thing a game does to say "that happened". */
745
+ type EffectKind = "burst" | "emit" | "trail" | "shake" | "flash" | "hitstop";
746
+ /** One effect that fired. */
747
+ interface EffectEvent {
748
+ kind: EffectKind;
749
+ /** The preset name, the flash colour, or `''` when the effect has no name. */
750
+ name: string;
751
+ /** Node path, or `engine` for the screen-wide ones. */
752
+ from: string;
753
+ /** Particles in the burst, shake magnitude, seconds frozen — the amount. */
754
+ amount: number;
755
+ /** `engine.time` when it fired, in seconds. */
756
+ at: number;
757
+ }
758
+ declare class EffectLog {
759
+ private readonly events;
760
+ /** @internal Called by the effects themselves; games read `recent()`. */
761
+ record(event: EffectEvent): void;
762
+ /** The last `limit` effects, oldest first. */
763
+ recent(limit?: number): EffectEvent[];
764
+ /** How many times an effect with this name fired (`explosion`, `#ff0000`). */
765
+ countOf(name: string): number;
766
+ /** How many effects this node fired — the question a wiring check has. */
767
+ countFrom(path: string): number;
768
+ /** Forget what has fired so far (between scenes, or between assertions). */
769
+ clearLog(): void;
770
+ }
771
+ //#endregion
637
772
  //#region src/core/input.d.ts
638
773
  /**
639
774
  * Declarative input from scene JSON `input{}` — DOM-free logic (feed key state
@@ -760,6 +895,20 @@ declare class InputMap {
760
895
  connect(fn: () => void): () => void;
761
896
  };
762
897
  }): () => void;
898
+ /**
899
+ * Pointer input from a mouse, a FINGER or a pen — one code path.
900
+ *
901
+ * This used to bind `mousedown`/`mousemove`/`mouseup`, which on a phone only
902
+ * arrive as COMPATIBILITY events: delayed until the browser has decided the
903
+ * touch is not a scroll, never fired for drags or multi-touch, and suppressed
904
+ * outright once the surface claims its gestures with `touch-action: none`.
905
+ * So every genre the comment below lists — match-3, tower defense, card,
906
+ * point-and-click, RTS — was mouse-only on the device most of them are played
907
+ * on. Pointer events cover all three input kinds and fire immediately.
908
+ *
909
+ * Only the PRIMARY pointer drives it: a second finger (the one on the virtual
910
+ * stick) must not also swing the camera.
911
+ */
763
912
  attachPointer(target: HTMLElement, opts?: {
764
913
  lockOnClick?: boolean;
765
914
  }): () => void;
@@ -1306,12 +1455,36 @@ interface QualityCaps {
1306
1455
  minReflectionIntervalMs: number;
1307
1456
  }
1308
1457
  //#endregion
1309
- //#region src/core/stats.d.ts
1458
+ //#region src/core/phase-timing.d.ts
1310
1459
  /**
1311
- * Performance-stats shapes — pure data, importable everywhere (core stays
1312
- * three-free; the renderer adapters fill RendererStats from three's
1313
- * `renderer.info`).
1460
+ * Where the frame went — the breakdown `stats()` could not give.
1461
+ *
1462
+ * fps and frameMs are a TOTAL, and a total has no next question. This engine
1463
+ * has already lost a day to that: a frame drop everyone attributed to the GPU
1464
+ * turned out to be garbage collection, and nothing on screen could have said
1465
+ * so. Three of a frame's four parts are the engine's own work, and the fourth
1466
+ * is what is left — which is exactly where GC and browser work hide.
1467
+ *
1468
+ * Means over the same rolling window as `frameMs`, so the numbers are
1469
+ * comparable to each other and to the whole.
1314
1470
  */
1471
+ /** One frame's own accounting, in milliseconds. */
1472
+ interface FramePhases {
1473
+ /** Fixed steps: physics, and anything on `fixedUpdate`. */
1474
+ fixedMs: number;
1475
+ /** The variable update: behaviors, node logic, tweens. */
1476
+ updateMs: number;
1477
+ /** Whatever renders, as reported by it. 0 when nothing does (headless). */
1478
+ renderMs: number;
1479
+ /**
1480
+ * The frame minus the three above. GC, browser layout, input handling,
1481
+ * anything outside the engine's own loop — and the one to look at when the
1482
+ * others are small and the frame is not.
1483
+ */
1484
+ otherMs: number;
1485
+ }
1486
+ //#endregion
1487
+ //#region src/core/stats.d.ts
1315
1488
  /** Loop-side counters from `engine.stats()` — queryable at any time. */
1316
1489
  interface EngineStats {
1317
1490
  /** Frames per second over the last ~60 REAL ticks (headless runs: 0). */
@@ -1329,6 +1502,11 @@ interface EngineStats {
1329
1502
  * this counter exists for.
1330
1503
  */
1331
1504
  errors: number;
1505
+ /**
1506
+ * Where the frame went: fixed (physics), update (behaviors), render, and
1507
+ * everything else. Means over the same window as `frameMs`.
1508
+ */
1509
+ phases: FramePhases;
1332
1510
  }
1333
1511
  /** GPU-side counters from `renderer.stats()` — the LAST rendered frame. */
1334
1512
  interface RendererStats {
@@ -1393,6 +1571,12 @@ declare class Engine {
1393
1571
  * routes its volume through this; games set it for global volume/mute. */
1394
1572
  readonly audio: AudioBuses;
1395
1573
  /**
1574
+ * What SHOWED — the visual half of the audio record. Particle bursts, camera
1575
+ * shake, screen flash and freeze frames land here, so "did the explosion
1576
+ * fire?" is answerable in a run with no renderer.
1577
+ */
1578
+ readonly effects: EffectLog;
1579
+ /**
1396
1580
  * Persisted player settings — volume, quality, sensitivity.
1397
1581
  *
1398
1582
  * `createSaveStore` was good and had zero callers, so every game's volume
@@ -1435,6 +1619,10 @@ declare class Engine {
1435
1619
  private accumulator;
1436
1620
  private disposeScheduler;
1437
1621
  private readonly frameStats;
1622
+ /** Where each frame's time went — the breakdown `frameMs` alone cannot give. */
1623
+ private readonly phaseTimings;
1624
+ /** This frame's render cost, as reported by whoever renders. */
1625
+ private renderMs;
1438
1626
  /**
1439
1627
  * Game-time multiplier: 1 = realtime, 0.5 = slow motion, 0 = frozen
1440
1628
  * (hit-stop / pause). Scales BOTH variable and fixed updates — physics,
@@ -1528,6 +1716,15 @@ declare class Engine {
1528
1716
  erroredNodes(): Node[];
1529
1717
  /** Un-skip every quarantined node — the "try again" after you fix the code. */
1530
1718
  resumeErroredNodes(): number;
1719
+ /**
1720
+ * How long the last render took, in ms — called by whoever renders.
1721
+ *
1722
+ * The engine does not render, so this is the one slice it cannot measure for
1723
+ * itself; without it the cost lands in `otherMs` beside the GC and the two
1724
+ * are indistinguishable, which is the confusion this whole breakdown exists
1725
+ * to end.
1726
+ */
1727
+ noteRenderMs(ms: number): void;
1531
1728
  stop(): void;
1532
1729
  /**
1533
1730
  * Live performance counters, queryable at ANY time. fps/frameMs come from a
@@ -1553,12 +1750,17 @@ declare class Engine {
1553
1750
  dispose(): void;
1554
1751
  /**
1555
1752
  * Advance exactly ONE fixed step and one variable update (both dt = the
1556
- * fixed step), bypassing the wall-clock accumulator entirely. Drift-free
1557
- * by construction — the unit of time for headless harnesses (incanto/test
1558
- * runScript): every input edge is visible to BOTH fixedUpdate and update
1559
- * exactly once.
1753
+ * fixed step scaled by `timeScale`), bypassing the wall-clock accumulator
1754
+ * entirely. Drift-free by construction — the unit of time for headless
1755
+ * harnesses (incanto/test runScript): every input edge is visible to BOTH
1756
+ * fixedUpdate and update exactly once.
1757
+ *
1758
+ * `ignorePause: true` steps a frame at full scale even while the game is
1759
+ * paused — the debug overlay's single-step button, and nothing else.
1560
1760
  */
1561
- step(): void;
1761
+ step(opts?: {
1762
+ ignorePause?: boolean;
1763
+ }): void;
1562
1764
  /** Advance the loop manually. First call after (re)start only primes the clock. */
1563
1765
  tick(nowMs: number): void;
1564
1766
  }
@@ -1726,4 +1928,4 @@ declare function registeredBehaviors(): string[];
1726
1928
  /** Test isolation helper. */
1727
1929
  declare function clearBehaviors(): void;
1728
1930
  //#endregion
1729
- export { T_PREFIX as $, SettingsValues as A, LogEntry as At, captureBehaviors as B, EngineStats as C, MusicTrack as Ct, QualityCaps as D, Node as Dt, DeviceHints as E, BusName as Et, BehaviorState as F, ORDER_GROUP_BASE as G, savesWithoutUid as H, RestoreReport as I, effectiveOrder as J, OrderGroup as K, SaveSlot as L, readDeviceHints as M, LogManager as Mt, suggestQuality as N, Signal as Nt, QualityTier as O, NodeLifecycle as Ot, Scene as P, SignalListener as Pt, Localization as Q, SaveSlots as R, Scheduler as S, MusicManager as St, RendererStats as T, AudioBuses as Tt, SaveStore as U, restoreBehaviors as V, createSaveStore as W, BASE_LOCALE as X, resolveOrderGroups as Y, LocaleTables as Z, mergeStaticSignals as _, SfxParams as _t, clearBehaviors as a, isAudioContextAvailable as at, Engine as b, synthSfx as bt, registeredBehaviors as c, Listener as ct, PropSchema as d, SpatialParams as dt, suggestLocale as et, clearRegistry as f, Vec3 as ft, getNodeType as g, SFX_PRESET_NAMES as gt, getNodeSignals as h, SFX_PRESETS as ht, behaviorSignals as i, SfxPlayOptions as it, qualityEnvironment as j, LogLevel as jt, Settings as k, SceneTree as kt, NodeCtor as l, ROLLOFF_MODELS as lt, getNodeSchema as m, spatialPan as mt, BehaviorCtor as n, InputMap as nt, getBehavior as o, Voice as ot, createNode as p, spatialGain as pt, OrderGroupTable as q, behaviorSchema as r, SfxEngine as rt, registerBehavior as s, VoicePreset as st, Behavior as t, translationKey as tt, PropDef as u, RolloffModel as ut, registerNode as v, SfxWave as vt, GameStats as w, PlayMusicOptions as wt, EngineOptions as x, MusicBackend as xt, registeredTypes as y, SynthOptions as yt, behaviorsWithoutSave as z };
1931
+ 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 };
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { A as registerBehavior, C as diagnose, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-B-Gft32x.js";
3
- import { h as Engine, m as AudioPlayer } from "./register-DVwlnZAZ.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-p48lHE2o.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-ks3d5W9n.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-B6jqvYeM.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-Trx7WHnD.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-C7uVSJvx.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-CZ2yq37J.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-en63AEZO.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-Dns-oZlE.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-CfWAggJ1.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
  /**
@@ -333,7 +333,11 @@ var Renderer2D = class {
333
333
  this.selectionLines.renderOrder = 1e4;
334
334
  this.selectionLines.visible = false;
335
335
  this.worldScene.add(this.selectionLines);
336
- this.disconnect = this.engine.updated.connect(() => this.render());
336
+ this.disconnect = this.engine.updated.connect(() => {
337
+ const started = performance.now();
338
+ this.render();
339
+ this.engine.noteRenderMs(performance.now() - started);
340
+ });
337
341
  }
338
342
  debugLines;
339
343
  selectionLines;
@@ -625,10 +629,15 @@ async function createGame2D(opts) {
625
629
  const cleanups = [];
626
630
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
627
631
  if (keyboard) engine.input.attachKeyboard(keyboard);
632
+ if (opts.pointer) {
633
+ const lockOnClick = typeof opts.pointer === "object" ? opts.pointer.lockOnClick ?? false : false;
634
+ engine.input.attachPointer(opts.canvas, { lockOnClick });
635
+ }
628
636
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
629
637
  cleanups.push(engine.settings.bindAudio(engine.audio));
630
638
  cleanups.push(pauseWhenHidden(engine));
631
639
  cleanups.push(engine.settings.bindLocale(engine.locale));
640
+ if (!opts.pageGestures) claimCanvasGestures(opts.canvas);
632
641
  const touchMode = opts.touch ?? "auto";
633
642
  if (touchMode !== false) {
634
643
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -718,6 +727,9 @@ async function createGame2D(opts) {
718
727
  pick(x, y) {
719
728
  return engine.picker?.(x, y) ?? null;
720
729
  },
730
+ assetErrors() {
731
+ return [...renderer.assets?.errors() ?? [], ...audioErrors(engine.scene?.tree.root)];
732
+ },
721
733
  stats() {
722
734
  return {
723
735
  triangles: 0,
@@ -755,10 +767,17 @@ function wireAudioUnlock(engine, root, canvas) {
755
767
  target: canvasTarget,
756
768
  type: "pointerdown"
757
769
  });
758
- if (typeof window !== "undefined") sources.push({
759
- target: window,
760
- type: "keydown"
761
- });
770
+ if (typeof window !== "undefined") {
771
+ const win = window;
772
+ sources.push({
773
+ target: win,
774
+ type: "keydown"
775
+ });
776
+ sources.push({
777
+ target: win,
778
+ type: "pointerdown"
779
+ });
780
+ }
762
781
  if (sources.length === 0) return () => {};
763
782
  function detachAll() {
764
783
  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, C as diagnose, n as loadScene } from "./loader-B-Gft32x.js";
3
- import { _ as qualityCaps, h as Engine, m as AudioPlayer, y as qualityRendering } from "./register-DVwlnZAZ.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-p48lHE2o.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-ks3d5W9n.js";
8
- import { I as PhysicsBody3D, V as createCausticsQuad, n as registerGameplayBehaviors, z as Node3D } from "./gameplay-B6jqvYeM.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-C7uVSJvx.js";
8
+ import { I as PhysicsBody3D, V as createCausticsQuad, n as registerGameplayBehaviors, z as Node3D } from "./gameplay-CZ2yq37J.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-DSZwsKPs.js";
10
+ import { E as Camera3D, U as TextureCache3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-BQ_QsIBY.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-BhI0ehpe.js";
12
+ import { n as enablePhysics3D } from "./physics-3d-Bes-GIuR.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";
@@ -1670,7 +1670,11 @@ var Renderer3D = class {
1670
1670
  this.selectionLines.renderOrder = 1e4;
1671
1671
  this.selectionLines.visible = false;
1672
1672
  this.threeScene.add(this.selectionLines);
1673
- this.disconnect = this.engine.updated.connect(() => this.render());
1673
+ this.disconnect = this.engine.updated.connect(() => {
1674
+ const started = performance.now();
1675
+ this.render();
1676
+ this.engine.noteRenderMs(performance.now() - started);
1677
+ });
1674
1678
  }
1675
1679
  /** The pixel ratio the scene ASKED for — the governor scales this, never replaces it. */
1676
1680
  basePixelRatio;
@@ -2349,6 +2353,7 @@ async function createGame3D(opts) {
2349
2353
  engine.input.attachPointer(opts.canvas, { lockOnClick });
2350
2354
  }
2351
2355
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
2356
+ if (!opts.pageGestures) claimCanvasGestures(opts.canvas);
2352
2357
  const touchMode = opts.touch ?? "auto";
2353
2358
  if (touchMode !== false) {
2354
2359
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -2575,7 +2580,7 @@ async function createGame3D(opts) {
2575
2580
  renderer,
2576
2581
  physics,
2577
2582
  assetErrors() {
2578
- return renderer.assets?.errors() ?? [];
2583
+ return [...renderer.assets?.errors() ?? [], ...audioErrors(engine.scene?.tree.root)];
2579
2584
  },
2580
2585
  async frame(frameOpts) {
2581
2586
  const capture = renderer.captureFrame;
@@ -2623,10 +2628,17 @@ function wireAudioUnlock(engine, root, canvas) {
2623
2628
  target: canvasTarget,
2624
2629
  type: "pointerdown"
2625
2630
  });
2626
- if (typeof window !== "undefined") sources.push({
2627
- target: window,
2628
- type: "keydown"
2629
- });
2631
+ if (typeof window !== "undefined") {
2632
+ const win = window;
2633
+ sources.push({
2634
+ target: win,
2635
+ type: "keydown"
2636
+ });
2637
+ sources.push({
2638
+ target: win,
2639
+ type: "pointerdown"
2640
+ });
2641
+ }
2630
2642
  if (sources.length === 0) return () => {};
2631
2643
  function detachAll() {
2632
2644
  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-uPEuZrUB.js";
1
+ import { Pt as LogLevel, T as RendererStats, b as Engine } from "./behavior-CyQoSu4n.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
@@ -1600,7 +1600,7 @@ var DebugOverlay = class {
1600
1600
  */
1601
1601
  nextFrame() {
1602
1602
  if (!this.paused) this.setPaused(true);
1603
- this.engine.step();
1603
+ this.engine.step({ ignorePause: true });
1604
1604
  this.syncTime();
1605
1605
  }
1606
1606
  /** Push the engine's current time state into the panel, without rebuilding it. */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-B-Gft32x.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-BTkHYrQn.js";
2
2
  //#region src/core/scene/duplicate.ts
3
3
  function stripUids(json) {
4
4
  const { uid: _uid, ...rest } = json;
package/dist/editor.js CHANGED
@@ -1181,6 +1181,20 @@ var kt = {
1181
1181
  en: "Options are the locales the scene declares in \"strings\", each labeled with its own endonym. Picking one switches the game live and persists it. English is the base: any key a locale omits falls back to it, on purpose.",
1182
1182
  ko: "옵션은 씬의 \"strings\"가 선언한 로케일이며, 각 언어의 자기 이름으로 표시됩니다. 선택하면 즉시 바뀌고 저장됩니다. 영어가 기준이라 로케일이 빠뜨린 키는 의도적으로 영어로 표시됩니다."
1183
1183
  }),
1184
+ L("UiVolumeSlider", {
1185
+ en: "A volume slider, already wired to a bus.",
1186
+ ko: "볼륨 슬라이더 — 버스에 이미 연결되어 있습니다."
1187
+ }, {
1188
+ en: "bus picks which one it owns: master (everything), sfx or music. It writes engine.audio, which persists itself, so nothing needs saving. Left unlabelled it names its own bus. Two of them (music + sfx) is the audio menu most games ship.",
1189
+ ko: "bus로 어느 볼륨을 맡을지 고릅니다 — master(전체), sfx, music. engine.audio에 쓰면 자동 저장되므로 따로 저장할 게 없습니다. label을 비워두면 버스 이름이 표시됩니다. 대부분의 게임은 music + sfx 두 개면 충분합니다."
1190
+ }),
1191
+ L("UiMuteToggle", {
1192
+ en: "The mute switch — checked means silent.",
1193
+ ko: "음소거 스위치 — 체크하면 소리가 꺼집니다."
1194
+ }, {
1195
+ en: "Writes engine.audio.muted and persists with the volumes. Follows a mute made anywhere else, so two menus can never disagree.",
1196
+ ko: "engine.audio.muted에 쓰고 볼륨과 함께 저장됩니다. 다른 곳에서 음소거해도 따라가므로 두 메뉴가 어긋나지 않습니다."
1197
+ }),
1184
1198
  L("UiQualitySelect", {
1185
1199
  en: "Graphics quality — Low, Medium, High. Already wired.",
1186
1200
  ko: "그래픽 품질 — Low, Medium, High. 이미 연결되어 있습니다."