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
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-CGq-Hee8.js";
1
+ import { t as registerCoreNodes } from "./register-BTomIiYG.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
- import { i as getNodeSchema, l as registerNode } from "./registry-IyWCGe4q.js";
4
+ import { i as getNodeSchema, l as registerNode } from "./registry-C7u42TID.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-D_p28jwU.js";
7
- import { F as CharacterBody3D, I as PhysicsBody3D, L as RigidBody3D, N as WaterCutout3D, P as Area3D, R as StaticBody3D, U as colliderFootDrop, j as Water3D, z as Node3D } from "./gameplay-bStgtZBV.js";
7
+ import { F as CharacterBody3D, I as PhysicsBody3D, L as RigidBody3D, N as WaterCutout3D, P as Area3D, R as StaticBody3D, U as colliderFootDrop, j as Water3D, z as Node3D } from "./gameplay-CRYw_Q-T.js";
8
8
  import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
9
  import { AdditiveBlending, AnimationClip, AnimationMixer, Box3, BoxGeometry, BufferAttribute, BufferGeometry, CanvasTexture, CapsuleGeometry, ClampToEdgeWrapping, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LinearMipmapLinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, NoColorSpace, NormalBlending, PerspectiveCamera, PlaneGeometry, PointLight, Points, PointsMaterial, Quaternion, QuaternionKeyframeTrack, RGBADepthPacking, RGBAFormat, RepeatWrapping, SRGBColorSpace, ShaderChunk, ShaderMaterial, SphereGeometry, Texture, TextureLoader, UniformsLib, UniformsUtils, Vector3, Vector4, VectorKeyframeTrack } from "three";
10
10
  import { clone } from "three/addons/utils/SkeletonUtils.js";
@@ -1,7 +1,7 @@
1
- import { A as registerBehavior, T as Behavior, f as Node, m as SaveSlots, n as loadScene, x as effectiveOrder } from "./loader-B2asghWa.js";
1
+ import { A as registerBehavior, T as Behavior, b as effectiveOrder, f as Node, n as loadScene, p as SaveSlots } from "./loader-BTkHYrQn.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
- import { t as duplicateNode } from "./duplicate-EybyYeyL.js";
4
+ import { t as duplicateNode } from "./duplicate-MNLMAcbz.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -4374,7 +4374,8 @@ var Chase = class extends Behavior {
4374
4374
  /** NodePath of the node to chase (resolved from THIS behavior's node). */
4375
4375
  target: {
4376
4376
  default: "",
4377
- nodePath: true
4377
+ nodePath: true,
4378
+ required: true
4378
4379
  },
4379
4380
  /** Units per second toward the target. */
4380
4381
  speed: { default: 60 },
@@ -4909,7 +4910,8 @@ var FollowCamera = class extends Behavior {
4909
4910
  /** NodePath of the node to follow (relative to this node, or absolute). */
4910
4911
  target: {
4911
4912
  default: "",
4912
- nodePath: true
4913
+ nodePath: true,
4914
+ required: true
4913
4915
  },
4914
4916
  /** Constant offset added to the target's position ([x,y] or [x,y,z]). */
4915
4917
  offset: { default: [] },
@@ -1,4 +1,4 @@
1
- import { Dt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-rZNfzVbH.js";
1
+ import { Dt as Node, b as Engine, d as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-Do0Da56m.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Chase, C as restartScene, D as DamageOnContact, E as phaseOf, O as Health, S as goToScene, T as DayNight, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, k as Collector, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, w as FollowCamera, x as GameFlow, y as screenFlash } from "./gameplay-bStgtZBV.js";
1
+ import { A as Chase, C as restartScene, D as DamageOnContact, E as phaseOf, O as Health, S as goToScene, T as DayNight, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, k as Collector, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, w as FollowCamera, x as GameFlow, y as screenFlash } from "./gameplay-CRYw_Q-T.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/index.d.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { $ as T_PREFIX, A as SettingsValues, At as LogEntry, B as captureBehaviors, C as EngineStats, Ct as MusicTrack, Dt as Node, E as DeviceHints, Et as BusName, F as BehaviorState, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, J as effectiveOrder, K as OrderGroup, L as SaveSlot, M as readDeviceHints, Mt as LogManager, N as suggestQuality, Nt as Signal, O as QualityTier, Ot as NodeLifecycle, P as Scene, Pt as SignalListener, Q as Localization, R as SaveSlots, S as Scheduler, St as MusicManager, T as RendererStats, Tt as AudioBuses, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as SfxParams, a as clearBehaviors, at as isAudioContextAvailable, b as Engine, bt as synthSfx, c as registeredBehaviors, ct as Listener, d as PropSchema, dt as SpatialParams, et as suggestLocale, f as clearRegistry, ft as Vec3, g as getNodeType, gt as SFX_PRESET_NAMES, h as getNodeSignals, ht as SFX_PRESETS, i as behaviorSignals, it as SfxPlayOptions, j as qualityEnvironment, jt as LogLevel, k as Settings, kt as SceneTree, l as NodeCtor, lt as ROLLOFF_MODELS, m as getNodeSchema, mt as spatialPan, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as Voice, p as createNode, pt as spatialGain, q as OrderGroupTable, r as behaviorSchema, rt as SfxEngine, s as registerBehavior, st as VoicePreset, t as Behavior, tt as translationKey, u as PropDef, ut as RolloffModel, v as registerNode, vt as SfxWave, w as GameStats, wt as PlayMusicOptions, x as EngineOptions, xt as MusicBackend, y as registeredTypes, yt as SynthOptions, z as behaviorsWithoutSave } from "./behavior-rZNfzVbH.js";
1
+ import { $ as T_PREFIX, A as SettingsValues, At as LogEntry, B as captureBehaviors, C as EngineStats, Ct as MusicTrack, Dt as Node, E as DeviceHints, Et as BusName, F as BehaviorState, G as ORDER_GROUP_BASE, H as savesWithoutUid, I as RestoreReport, J as effectiveOrder, K as OrderGroup, L as SaveSlot, M as readDeviceHints, Mt as LogManager, N as suggestQuality, Nt as Signal, O as QualityTier, Ot as NodeLifecycle, P as Scene, Pt as SignalListener, Q as Localization, R as SaveSlots, S as Scheduler, St as MusicManager, T as RendererStats, Tt as AudioBuses, U as SaveStore, V as restoreBehaviors, W as createSaveStore, X as BASE_LOCALE, Y as resolveOrderGroups, Z as LocaleTables, _ as mergeStaticSignals, _t as SfxParams, a as clearBehaviors, at as isAudioContextAvailable, b as Engine, bt as synthSfx, c as registeredBehaviors, ct as Listener, d as PropSchema, dt as SpatialParams, et as suggestLocale, f as clearRegistry, ft as Vec3, g as getNodeType, gt as SFX_PRESET_NAMES, h as getNodeSignals, ht as SFX_PRESETS, i as behaviorSignals, it as SfxPlayOptions, j as qualityEnvironment, jt as LogLevel, k as Settings, kt as SceneTree, l as NodeCtor, lt as ROLLOFF_MODELS, m as getNodeSchema, mt as spatialPan, n as BehaviorCtor, nt as InputMap, o as getBehavior, ot as Voice, p as createNode, pt as spatialGain, q as OrderGroupTable, r as behaviorSchema, rt as SfxEngine, s as registerBehavior, st as VoicePreset, t as Behavior, tt as translationKey, u as PropDef, ut as RolloffModel, v as registerNode, vt as SfxWave, w as GameStats, wt as PlayMusicOptions, x as EngineOptions, xt as MusicBackend, y as registeredTypes, yt as SynthOptions, z as behaviorsWithoutSave } from "./behavior-Do0Da56m.js";
2
2
  import { a as Rng, c as JsonValue, d as jsonKind, i as SceneJson, l as jsonClone, n as NodeJson, o as JsonKind, r as SCENE_FORMAT, s as JsonObject, t as ConnectionJson, u as jsonEquals } from "./schema-CFeioQRE.js";
3
- import { n as loadScene, t as LoadSceneOptions } from "./loader-Dkbn56KC.js";
3
+ import { n as loadScene, t as LoadSceneOptions } from "./loader-Cu_7kJDy.js";
4
4
  import { i as resolveFrames, n as AnimationEntry, r as resolveAnimation, t as AnimationDef } from "./sprite-animation-CMr6f1K2.js";
5
5
  import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-BzJ1yxoE.js";
6
- import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-Bz34pvQD.js";
7
- import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-DIP2_as4.js";
6
+ import { a as AudioElementLike, i as gridFromRows, n as PathGrid, o as AudioPlayer, r as findPath, t as FindPathOptions } from "./pathfinding-HNGFqGUZ.js";
7
+ import { a as startRecording, c as IncantoErrorDetails, i as replay, l as auditScene, n as ReplayEvent, o as IncantoError, r as ReplayJson, s as IncantoErrorCode, t as Recorder } from "./replay-9Fy6C10F.js";
8
8
 
9
9
  //#region src/core/audio/crossfade.d.ts
10
10
  /**
@@ -38,6 +38,7 @@ declare class WebAudioMusicBackend implements MusicBackend {
38
38
  private ensure;
39
39
  createTrack(src: string): MusicTrack;
40
40
  unlock(): void;
41
+ dispose(): void;
41
42
  }
42
43
  //#endregion
43
44
  //#region src/core/debug-draw.d.ts
@@ -189,6 +190,66 @@ type ParsedNodePath = {
189
190
  };
190
191
  declare function parseNodePath(path: string): ParsedNodePath;
191
192
  //#endregion
193
+ //#region src/core/node-refs.d.ts
194
+ /** The scene-JSON shape this walks. */
195
+ interface RefNode {
196
+ name?: string;
197
+ type?: string;
198
+ props?: JsonObject;
199
+ script?: {
200
+ name?: string;
201
+ props?: JsonObject;
202
+ };
203
+ children?: RefNode[];
204
+ }
205
+ /** Why a written path leads nowhere. Structured so callers can localize it. */
206
+ type RefProblem = {
207
+ kind: "grammar";
208
+ message: string;
209
+ } | {
210
+ kind: "noMatch";
211
+ name: string;
212
+ } | {
213
+ kind: "ambiguous";
214
+ name: string;
215
+ count: number;
216
+ } | {
217
+ kind: "wrongRoot";
218
+ first: string;
219
+ root: string;
220
+ } | {
221
+ kind: "noChild";
222
+ parent: string;
223
+ segment: string;
224
+ } | {
225
+ kind: "aboveRoot";
226
+ };
227
+ type RefResult = {
228
+ ok: true;
229
+ path: number[];
230
+ } | {
231
+ ok: false;
232
+ problem: RefProblem;
233
+ };
234
+ /**
235
+ * Resolve `value` from the node at `owner`, the way the engine would.
236
+ *
237
+ * Note the asymmetry with connections, whose paths resolve from the SCENE ROOT:
238
+ * a prop's path resolves from the node holding it, which is why `'../Skin'` is
239
+ * the documented default of `CharacterController3D.skinPath` and would mean
240
+ * nothing in a connection.
241
+ */
242
+ declare function resolveRefInJson(root: RefNode | undefined, owner: readonly number[], value: string): RefResult;
243
+ /** The problem in English, for `incanto-check` and as the editor's base text. */
244
+ declare function describeRefProblem(problem: RefProblem, value: string): string;
245
+ /**
246
+ * Scene lints for node-path props that point at nothing.
247
+ *
248
+ * An EMPTY value is not reported: `''` is the default of most of these props and
249
+ * means "not set", which is a choice rather than a typo.
250
+ */
251
+ declare function nodeRefWarnings(scene: JsonObject): string[];
252
+ //#endregion
192
253
  //#region src/core/nodes/hud.d.ts
193
254
  /**
194
255
  * DOM-backed HUD widgets — the layer 18 of 22 example games hand-rolled.
@@ -637,6 +698,177 @@ declare class UiSelect extends HudWidgetBase {
637
698
  private fillOptions;
638
699
  protected override _sync(): void;
639
700
  }
701
+ /**
702
+ * The language picker, done — because every game needs the same one.
703
+ *
704
+ * A plain `UiSelect` bound to the locale would take a behavior, a signal
705
+ * connection, and the knowledge that `Settings` persists it: three things an
706
+ * agent has to get right to ship a feature the engine already has. This is one
707
+ * node.
708
+ *
709
+ * ```json
710
+ * { "name": "Language", "type": "UiLanguageSelect",
711
+ * "props": { "label": "@t:settings.language" } }
712
+ * ```
713
+ *
714
+ * Options are the locales the SCENE declares, so a game shipping only English
715
+ * shows only English and the widget quietly costs nothing. Each is labeled with
716
+ * its own endonym, because a player who cannot read the language currently on
717
+ * screen still has to be able to find theirs.
718
+ */
719
+ declare class UiLanguageSelect extends UiSelect {
720
+ static override readonly typeName: string;
721
+ static override readonly props: PropSchema;
722
+ override label: string;
723
+ /** Last locale we PUSHED into `value`, to tell an engine change from a player one. */
724
+ private lastLocale;
725
+ protected override _sync(): void;
726
+ protected override _optionLabel(value: string): string;
727
+ }
728
+ /**
729
+ * A select bound to one persisted setting — the shape all three graphics
730
+ * controls share.
731
+ *
732
+ * The engine owns quality tiers, the frame cap and the resolution scale, and
733
+ * every one of them was reachable only from TypeScript. A vibe-coded game whose
734
+ * whole world is JSON could not put a graphics menu on screen without a
735
+ * behavior, a signal connection, and the knowledge that `Settings` persists —
736
+ * which is how "supports low-end devices" quietly becomes "does not".
737
+ *
738
+ * Subclasses say which setting, what the options are, and what they read as.
739
+ */
740
+ declare abstract class UiSettingSelect extends UiSelect {
741
+ /** Last value we PUSHED into the select, to tell an engine change from a player one. */
742
+ private lastPushed;
743
+ /** The settings key this control owns. */
744
+ protected abstract _key(): string;
745
+ /** The stored value, as the string the select carries. */
746
+ protected abstract _read(engine: NonNullable<ReturnType<UiSettingSelect["_engine"]>>): string;
747
+ /** Apply a player's pick. */
748
+ protected abstract _write(engine: NonNullable<ReturnType<UiSettingSelect["_engine"]>>, value: string): void;
749
+ protected _engine(): Engine | null;
750
+ /**
751
+ * The translated option label, or the English one.
752
+ *
753
+ * `locale.t` returns the KEY when nothing declares it, and a menu reading
754
+ * `settings.quality.high` is worse than one reading `High`. English is the
755
+ * base language and the fallback, always.
756
+ */
757
+ protected _tOr(key: string, fallback: string): string;
758
+ protected override _sync(): void;
759
+ }
760
+ /**
761
+ * The graphics quality picker.
762
+ *
763
+ * ```json
764
+ * { "name": "Quality", "type": "UiQualitySelect" }
765
+ * ```
766
+ *
767
+ * Writes through `chooseQuality`, NOT `set('quality')`: a tier a person picked
768
+ * has to survive the next launch's device detection, and the two are
769
+ * indistinguishable afterwards unless the choice is recorded as one.
770
+ *
771
+ * Shadows, bloom, post and clouds follow immediately. Antialiasing follows on
772
+ * the next launch — it is a WebGL context attribute and cannot change while a
773
+ * context exists.
774
+ */
775
+ declare class UiQualitySelect extends UiSettingSelect {
776
+ static override readonly typeName: string;
777
+ static override readonly props: PropSchema;
778
+ override label: string;
779
+ override options: string;
780
+ protected override _key(): string;
781
+ protected override _read(engine: Engine): string;
782
+ protected override _write(engine: Engine, value: string): void;
783
+ protected override _optionLabel(value: string): string;
784
+ }
785
+ /**
786
+ * The frame-rate cap: fewer frames, a cooler phone, a longer battery.
787
+ *
788
+ * ```json
789
+ * { "name": "FrameCap", "type": "UiFrameCapSelect" }
790
+ * ```
791
+ *
792
+ * `0` is uncapped and is the default — a cap nobody asked for is a downgrade.
793
+ * The cap gates the whole frame, so it saves the CPU as well as the GPU.
794
+ */
795
+ declare class UiFrameCapSelect extends UiSettingSelect {
796
+ static override readonly typeName: string;
797
+ static override readonly props: PropSchema;
798
+ override label: string;
799
+ override options: string;
800
+ protected override _key(): string;
801
+ protected override _read(engine: Engine): string;
802
+ protected override _write(engine: Engine, value: string): void;
803
+ protected override _optionLabel(value: string): string;
804
+ }
805
+ /**
806
+ * The resolution scale — the cheapest frames on a weak GPU.
807
+ *
808
+ * ```json
809
+ * { "name": "Resolution", "type": "UiRenderScaleSelect" }
810
+ * ```
811
+ *
812
+ * At 75% the fragment shaders run over 56% of the pixels for a slightly softer
813
+ * image and nothing else, where dropping a quality tier costs the shadows
814
+ * outright. Reads as a percentage, because `0.75` means nothing to a player.
815
+ */
816
+ declare class UiRenderScaleSelect extends UiSettingSelect {
817
+ static override readonly typeName: string;
818
+ static override readonly props: PropSchema;
819
+ override label: string;
820
+ override options: string;
821
+ protected override _key(): string;
822
+ protected override _read(engine: Engine): string;
823
+ protected override _write(engine: Engine, value: string): void;
824
+ protected override _optionLabel(value: string): string;
825
+ }
826
+ /** The three buses a player is ever offered a slider for. */
827
+ declare const VOLUME_BUSES: readonly ["master", "sfx", "music"];
828
+ type VolumeBus = (typeof VOLUME_BUSES)[number];
829
+ /**
830
+ * The volume slider — the settings row every game has.
831
+ *
832
+ * ```json
833
+ * { "name": "Music", "type": "UiVolumeSlider", "props": { "bus": "music" } }
834
+ * ```
835
+ *
836
+ * The graphics controls above became nodes because a game whose whole world is
837
+ * JSON could not put a menu on screen without a behavior, a signal connection
838
+ * and the knowledge that `Settings` persists. All of that was just as true of
839
+ * volume, which is the control a player looks for FIRST and the only one some
840
+ * games need at all — so an audio menu meant hand-rolled DOM, and hand-rolled
841
+ * DOM is out of scene JSON, out of the editor, and out of every headless check.
842
+ *
843
+ * It writes the bus; the bus persists itself (`bindAudio`, which
844
+ * `createGame2D/3D` wire), so there is nothing to save.
845
+ */
846
+ declare class UiVolumeSlider extends UiSlider {
847
+ static override readonly typeName: string;
848
+ static override readonly props: PropSchema;
849
+ bus: VolumeBus;
850
+ /** Last value we PUSHED in, to tell an engine-side change from a player drag. */
851
+ private lastPushed;
852
+ protected override _build(): HTMLElement;
853
+ protected override _sync(): void;
854
+ }
855
+ /**
856
+ * The mute switch.
857
+ *
858
+ * ```json
859
+ * { "name": "Mute", "type": "UiMuteToggle" }
860
+ * ```
861
+ *
862
+ * Checked means silent. Like the slider it writes `engine.audio`, which
863
+ * persists itself.
864
+ */
865
+ declare class UiMuteToggle extends UiToggle {
866
+ static override readonly typeName: string;
867
+ static override readonly props: PropSchema;
868
+ private lastPushed;
869
+ protected override _build(): HTMLElement;
870
+ protected override _sync(): void;
871
+ }
640
872
  //#endregion
641
873
  //#region src/core/noise.d.ts
642
874
  /**
@@ -857,4 +1089,4 @@ declare function computeViewport(canvasW: number, canvasH: number, viewport: {
857
1089
  /** Engine version. Kept in sync with package.json by the release pipeline. */
858
1090
  declare const VERSION: string;
859
1091
  //#endregion
860
- export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
1092
+ export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RefNode, type RefProblem, type RefResult, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
package/dist/index.js CHANGED
@@ -1,14 +1,14 @@
1
- import { A as registerBehavior, C as InputMap, D as behaviorSignals, E as behaviorSchema, M as Signal, O as clearBehaviors, S as resolveOrderGroups, T as Behavior, _ as restoreBehaviors, a as SCENE_FORMAT, b as ORDER_GROUP_BASE, c as SceneTree, d as resolveConstants, f as Node, g as captureBehaviors, h as behaviorsWithoutSave, i as serializeNode, j as registeredBehaviors, k as getBehavior, l as CONST_REF_KEY, m as SaveSlots, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, v as savesWithoutUid, x as effectiveOrder, y as createSaveStore } from "./loader-B2asghWa.js";
2
- import { A as ROLLOFF_MODELS, B as AudioBuses, C as BASE_LOCALE, D as translationKey, E as suggestLocale, F as synthSfx, I as MusicManager, L as WebAudioMusicBackend, M as spatialPan, N as SFX_PRESETS, O as SfxEngine, P as SFX_PRESET_NAMES, R as crossfadeGains, S as LogManager, T as T_PREFIX, a as UiSlider, b as readDeviceHints, c as UiButton, d as UiBanner, f as UiBar, g as Settings, h as Engine, i as UiSelect, j as spatialGain, k as isAudioContextAvailable, l as UiDialogue, m as AudioPlayer, n as UiImage, o as UiToggle, p as UiText, r as UiPanel, s as Timer, t as registerCoreNodes, u as HudLayer, v as qualityEnvironment, w as Localization, x as suggestQuality, z as fadeGain } from "./register-CGq-Hee8.js";
1
+ import { A as registerBehavior, D as behaviorSignals, E as behaviorSchema, M as Signal, O as clearBehaviors, S as InputMap, T as Behavior, _ as savesWithoutUid, a as SCENE_FORMAT, b as effectiveOrder, c as SceneTree, d as resolveConstants, f as Node, g as restoreBehaviors, h as captureBehaviors, i as serializeNode, j as registeredBehaviors, k as getBehavior, l as CONST_REF_KEY, m as behaviorsWithoutSave, n as loadScene, o as computeViewport, p as SaveSlots, r as Scene, s as resolveViewport, u as isConstRef, v as createSaveStore, w as parseNodePath, x as resolveOrderGroups, y as ORDER_GROUP_BASE } from "./loader-BTkHYrQn.js";
2
+ import { A as Localization, B as SFX_PRESET_NAMES, D as suggestQuality, E as readDeviceHints, F as isAudioContextAvailable, G as fadeGain, H as MusicManager, I as ROLLOFF_MODELS, K as AudioBuses, L as spatialGain, M as suggestLocale, N as translationKey, O as LogManager, P as SfxEngine, R as spatialPan, S as Settings, U as WebAudioMusicBackend, V as synthSfx, W as crossfadeGains, _ as UiBanner, a as UiMuteToggle, b as AudioPlayer, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as HudLayer, h as UiDialogue, i as UiLanguageSelect, j as T_PREFIX, k as BASE_LOCALE, l as UiSelect, m as UiButton, n as UiFrameCapSelect, o as UiPanel, p as Timer, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as UiBar, w as qualityEnvironment, x as Engine, y as UiText, z as SFX_PRESETS } from "./register-BTomIiYG.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { n as startRecording, r as auditScene, t as replay } from "./replay-BgKxGcXH.js";
6
5
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.js";
7
- import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-IyWCGe4q.js";
8
- import { n as logText, r as parseDrive, t as logReport } from "./log-report-lxrQY9cH.js";
6
+ import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-C7u42TID.js";
7
+ import { a as nodeRefWarnings, i as describeRefProblem, n as startRecording, o as resolveRefInJson, r as auditScene, t as replay } from "./replay-DYNUL4BU.js";
8
+ import { n as logText, r as parseDrive, t as logReport } from "./log-report-CPFm4OXf.js";
9
9
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
10
10
  import { a as PARTICLE_PRESETS, i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-D_p28jwU.js";
11
- import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-C3UwzYXl.js";
12
- import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BoNg_MnF.js";
13
- import { t as duplicateNode } from "./duplicate-EybyYeyL.js";
14
- export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiImage, UiPanel, UiSelect, UiSlider, UiText, UiToggle, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
11
+ import { a as findPath, i as preloadUrls, n as newUid, o as gridFromRows, r as assetUrls, t as VERSION } from "./src-C9xyZW7M.js";
12
+ import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-BnMyy9tr.js";
13
+ import { t as duplicateNode } from "./duplicate-MNLMAcbz.js";
14
+ export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -1,6 +1,6 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
3
- import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-IyWCGe4q.js";
3
+ import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-C7u42TID.js";
4
4
  //#region src/core/signal.ts
5
5
  var Signal = class {
6
6
  connections = [];
@@ -135,6 +135,36 @@ function createBehavior(name, props) {
135
135
  return behavior;
136
136
  }
137
137
  //#endregion
138
+ //#region src/core/node-path.ts
139
+ function parseNodePath(path) {
140
+ if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
141
+ if (path.startsWith("%")) {
142
+ const name = path.slice(1);
143
+ if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid unique-name path '${path}'. Expected '%NodeName'.`);
144
+ return {
145
+ kind: "unique",
146
+ name
147
+ };
148
+ }
149
+ if (path === ".") return {
150
+ kind: "relative",
151
+ segments: []
152
+ };
153
+ const absolute = path.startsWith("/");
154
+ const body = absolute ? path.slice(1) : path;
155
+ if (body === "" || body.split("/").some((s) => s === "")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Segments must be non-empty (no leading/trailing/double '/').`);
156
+ const segments = body.split("/");
157
+ for (const seg of segments) {
158
+ if (seg.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '%' is only allowed as a whole-path prefix ('%Name').`);
159
+ if (seg === ".") throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '.' is only valid as the whole path — drop the './' prefix.`);
160
+ }
161
+ if (absolute && segments.includes("..")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Absolute paths must not contain '..'.`);
162
+ return {
163
+ kind: absolute ? "absolute" : "relative",
164
+ segments
165
+ };
166
+ }
167
+ //#endregion
138
168
  //#region src/core/diagnostics.ts
139
169
  function diagnose(engine, level, ...parts) {
140
170
  engine?.log[level](...parts);
@@ -191,12 +221,9 @@ var InputMap = class {
191
221
  };
192
222
  return;
193
223
  }
194
- const el = target;
195
- const scaleX = typeof el.width === "number" && el.width > 0 ? el.width / rect.width : 1;
196
- const scaleY = typeof el.height === "number" && el.height > 0 ? el.height / rect.height : 1;
197
224
  this.pointerPos = {
198
- x: (clientX - rect.left) * scaleX,
199
- y: (clientY - rect.top) * scaleY
225
+ x: clientX - rect.left,
226
+ y: clientY - rect.top
200
227
  };
201
228
  }
202
229
  pointerPos = null;
@@ -388,36 +415,69 @@ var InputMap = class {
388
415
  };
389
416
  return disconnect;
390
417
  }
418
+ /**
419
+ * Pointer input from a mouse, a FINGER or a pen — one code path.
420
+ *
421
+ * This used to bind `mousedown`/`mousemove`/`mouseup`, which on a phone only
422
+ * arrive as COMPATIBILITY events: delayed until the browser has decided the
423
+ * touch is not a scroll, never fired for drags or multi-touch, and suppressed
424
+ * outright once the surface claims its gestures with `touch-action: none`.
425
+ * So every genre the comment below lists — match-3, tower defense, card,
426
+ * point-and-click, RTS — was mouse-only on the device most of them are played
427
+ * on. Pointer events cover all three input kinds and fire immediately.
428
+ *
429
+ * Only the PRIMARY pointer drives it: a second finger (the one on the virtual
430
+ * stick) must not also swing the camera.
431
+ */
391
432
  attachPointer(target, opts) {
392
433
  let buttonsHeld = 0;
434
+ /** The pointer that pressed on THIS target; others are somebody else's. */
435
+ let activeId = null;
393
436
  const onDown = (e) => {
437
+ if (!e.isPrimary) return;
438
+ activeId = e.pointerId;
394
439
  buttonsHeld++;
395
440
  this.setPointerPosition(e.clientX, e.clientY, target);
396
441
  this.handleMouseButton(e.button, true);
397
442
  if (opts?.lockOnClick && document.pointerLockElement !== target) target.requestPointerLock?.();
398
443
  };
399
444
  const onUp = (e) => {
445
+ if (activeId !== null && e.pointerId !== activeId) return;
446
+ activeId = null;
400
447
  buttonsHeld = Math.max(0, buttonsHeld - 1);
401
448
  this.handleMouseButton(e.button, false);
402
449
  };
403
450
  const onMove = (e) => {
451
+ if (activeId !== null && e.pointerId !== activeId) return;
404
452
  this.setPointerPosition(e.clientX, e.clientY, target);
405
- if (document.pointerLockElement === target || buttonsHeld > 0) this.handlePointerMove(e.movementX, e.movementY);
453
+ if (document.pointerLockElement === target || buttonsHeld > 0) {
454
+ const dx = e.movementX || this.pointerDelta().x;
455
+ const dy = e.movementY || this.pointerDelta().y;
456
+ this.handlePointerMove(dx, dy);
457
+ }
458
+ };
459
+ const onCancel = (e) => {
460
+ if (activeId !== null && e.pointerId !== activeId) return;
461
+ activeId = null;
462
+ buttonsHeld = 0;
463
+ this.handleMouseButton(0, false);
406
464
  };
407
465
  const onWheel = (e) => {
408
466
  e.preventDefault();
409
467
  this.handleWheel(e.deltaY);
410
468
  };
411
469
  const onContext = (e) => e.preventDefault();
412
- target.addEventListener("mousedown", onDown);
413
- window.addEventListener("mouseup", onUp);
414
- window.addEventListener("mousemove", onMove);
470
+ target.addEventListener("pointerdown", onDown);
471
+ window.addEventListener("pointerup", onUp);
472
+ window.addEventListener("pointermove", onMove);
473
+ window.addEventListener("pointercancel", onCancel);
415
474
  target.addEventListener("wheel", onWheel, { passive: false });
416
475
  target.addEventListener("contextmenu", onContext);
417
476
  const detach = () => {
418
- target.removeEventListener("mousedown", onDown);
419
- window.removeEventListener("mouseup", onUp);
420
- window.removeEventListener("mousemove", onMove);
477
+ target.removeEventListener("pointerdown", onDown);
478
+ window.removeEventListener("pointerup", onUp);
479
+ window.removeEventListener("pointermove", onMove);
480
+ window.removeEventListener("pointercancel", onCancel);
421
481
  target.removeEventListener("wheel", onWheel);
422
482
  target.removeEventListener("contextmenu", onContext);
423
483
  };
@@ -574,6 +634,12 @@ var InputMap = class {
574
634
  * never looked at `input` at all. A typo there ("keys": "KeyW" instead of
575
635
  * ["KeyW"]) passed the check and threw in the browser at boot.
576
636
  */
637
+ const DIRECTIONS = [
638
+ "up",
639
+ "down",
640
+ "left",
641
+ "right"
642
+ ];
577
643
  function parseInputDecls(decls) {
578
644
  const out = /* @__PURE__ */ new Map();
579
645
  for (const [name, raw] of Object.entries(decls)) {
@@ -589,12 +655,12 @@ function parseInputDecls(decls) {
589
655
  });
590
656
  } else if (decl.type === "vector2") {
591
657
  const dirs = decl.keys;
592
- for (const dir of [
593
- "up",
594
- "down",
595
- "left",
596
- "right"
597
- ]) if (!Array.isArray(dirs?.[dir])) throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "keys" needs arrays for up/down/left/right.`);
658
+ const missing = DIRECTIONS.filter((dir) => !Array.isArray(dirs?.[dir]));
659
+ if (missing.length > 0) {
660
+ const atTopLevel = DIRECTIONS.filter((dir) => Array.isArray(decl[dir]));
661
+ const where = atTopLevel.length > 0 ? ` — ${atTopLevel.join(", ")} ${atTopLevel.length === 1 ? "is" : "are"} at the top level, not under "keys"` : "";
662
+ throw new IncantoError("BAD_FORMAT", `Input action '${name}': a vector2 needs ${missing.join(", ")} as arrays of KeyboardEvent.code strings under "keys"${where}. Like this: { "type": "vector2", "keys": { "up": ["KeyW"], "down": ["KeyS"], "left": ["KeyA"], "right": ["KeyD"] } }`);
663
+ }
598
664
  if (decl.touch !== void 0 && decl.touch !== "joystick") throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "touch" must be 'joystick', got ${JSON.stringify(decl.touch)}.`);
599
665
  const d = dirs;
600
666
  out.set(name, {
@@ -979,36 +1045,6 @@ function savesWithoutUid(root) {
979
1045
  return out;
980
1046
  }
981
1047
  //#endregion
982
- //#region src/core/node-path.ts
983
- function parseNodePath(path) {
984
- if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
985
- if (path.startsWith("%")) {
986
- const name = path.slice(1);
987
- if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid unique-name path '${path}'. Expected '%NodeName'.`);
988
- return {
989
- kind: "unique",
990
- name
991
- };
992
- }
993
- if (path === ".") return {
994
- kind: "relative",
995
- segments: []
996
- };
997
- const absolute = path.startsWith("/");
998
- const body = absolute ? path.slice(1) : path;
999
- if (body === "" || body.split("/").some((s) => s === "")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Segments must be non-empty (no leading/trailing/double '/').`);
1000
- const segments = body.split("/");
1001
- for (const seg of segments) {
1002
- if (seg.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '%' is only allowed as a whole-path prefix ('%Name').`);
1003
- if (seg === ".") throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '.' is only valid as the whole path — drop the './' prefix.`);
1004
- }
1005
- if (absolute && segments.includes("..")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Absolute paths must not contain '..'.`);
1006
- return {
1007
- kind: absolute ? "absolute" : "relative",
1008
- segments
1009
- };
1010
- }
1011
- //#endregion
1012
1048
  //#region src/core/node.ts
1013
1049
  function validateName(name) {
1014
1050
  if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("TREE_VIOLATION", `Invalid node name '${name}'. Names must be non-empty and must not contain '/' or '%'.`);
@@ -2030,4 +2066,4 @@ function validateUniqueUids(root) {
2030
2066
  walk(root);
2031
2067
  }
2032
2068
  //#endregion
2033
- export { registerBehavior as A, InputMap as C, behaviorSignals as D, behaviorSchema as E, Signal as M, clearBehaviors as O, resolveOrderGroups as S, Behavior as T, restoreBehaviors as _, SCENE_FORMAT as a, ORDER_GROUP_BASE as b, SceneTree as c, resolveConstants as d, Node as f, captureBehaviors as g, behaviorsWithoutSave as h, serializeNode as i, registeredBehaviors as j, getBehavior as k, CONST_REF_KEY as l, SaveSlots as m, loadScene as n, computeViewport as o, parseNodePath as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, savesWithoutUid as v, diagnose as w, effectiveOrder as x, createSaveStore as y };
2069
+ export { registerBehavior as A, diagnose as C, behaviorSignals as D, behaviorSchema as E, Signal as M, clearBehaviors as O, InputMap as S, Behavior as T, savesWithoutUid as _, SCENE_FORMAT as a, effectiveOrder as b, SceneTree as c, resolveConstants as d, Node as f, restoreBehaviors as g, captureBehaviors as h, serializeNode as i, registeredBehaviors as j, getBehavior as k, CONST_REF_KEY as l, behaviorsWithoutSave as m, loadScene as n, computeViewport as o, SaveSlots as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, createSaveStore as v, parseNodePath as w, resolveOrderGroups as x, ORDER_GROUP_BASE as y };
@@ -1,4 +1,4 @@
1
- import { P as Scene, b as Engine } from "./behavior-rZNfzVbH.js";
1
+ import { P as Scene, b as Engine } from "./behavior-Do0Da56m.js";
2
2
  import { i as SceneJson } from "./schema-CFeioQRE.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts