incanto 0.32.0 → 0.33.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 (49) hide show
  1. package/dist/2d.d.ts +59 -2
  2. package/dist/2d.js +3 -3
  3. package/dist/3d.d.ts +8 -3
  4. package/dist/3d.js +4 -4
  5. package/dist/{behavior-D_jMpFh8.d.ts → behavior-CKwTCjfR.d.ts} +140 -1
  6. package/dist/{create-game-bKHgHcsZ.js → create-game-D2QU5x7S.js} +13 -6
  7. package/dist/{create-game-gXI7PYl0.js → create-game-sFuTLqjD.js} +11 -5
  8. package/dist/debug.d.ts +1 -1
  9. package/dist/{duplicate-KRPtUtzl.js → duplicate-BgnG1Lqz.js} +1 -1
  10. package/dist/editor.js +35 -0
  11. package/dist/{environment-presets--DigHNg4.js → environment-presets-CQtEGogB.js} +2 -2
  12. package/dist/{gameplay-BftxM_It.js → gameplay-BpQCbABv.js} +90 -2
  13. package/dist/gameplay.d.ts +1 -1
  14. package/dist/gameplay.js +1 -1
  15. package/dist/index.d.ts +125 -23
  16. package/dist/index.js +5 -58
  17. package/dist/{loader-BlaRQGaA.js → loader-Buk8Bu1h.js} +53 -0
  18. package/dist/{loader-BYBrqTxP.d.ts → loader-COn5fS0o.d.ts} +1 -1
  19. package/dist/net.d.ts +1 -1
  20. package/dist/net.js +3 -3
  21. package/dist/{pathfinding-BwhqPD3i.d.ts → pathfinding-DUw9mir9.d.ts} +1 -1
  22. package/dist/{physics-2d-D9wquBvK.js → physics-2d-DjXR5DMu.js} +12 -2
  23. package/dist/{physics-3d-CnPygVGo.js → physics-3d-DF8npb1O.js} +55 -9
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-Dzkd6-os.js → register-CscIzJEO.js} +551 -13
  27. package/dist/{register-CUY284Is.js → register-CtI-itec.js} +2 -2
  28. package/dist/{register-tkR_8tWg.js → register-FIJtNbub.js} +145 -8
  29. package/dist/{test-WwRIlXsK.js → test-BRxLd2jH.js} +10 -10
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +1 -1
  32. package/dist/vite.js +1 -1
  33. package/editor/assets/{agent8-C5k1nTCH.js → agent8-BdDP3xKW.js} +1 -1
  34. package/editor/assets/{debug-BT_0mjk-.js → debug-DbjTyTlC.js} +1 -1
  35. package/editor/assets/{index-gfyrByWw.js → index-BWCudoz1.js} +90 -90
  36. package/editor/index.html +1 -1
  37. package/package.json +1 -1
  38. package/schemas/scene.schema.json +520 -0
  39. package/skills/incanto-audio.md +43 -0
  40. package/skills/incanto-building-2d-games.md +36 -0
  41. package/skills/incanto-hud.md +41 -0
  42. package/skills/incanto-node-reference.md +89 -0
  43. package/skills/incanto-physics-and-input.md +50 -0
  44. package/templates-app/beacon-isle-3d/docs/project-3d-rules.md +40 -19
  45. package/templates-app/beacon-isle-3d/package.json +1 -1
  46. package/templates-app/tps-3d/docs/project-3d-rules.md +41 -19
  47. package/templates-app/tps-3d/package.json +1 -1
  48. package/templates-app/village-quest-3d/docs/project-3d-rules.md +40 -19
  49. package/templates-app/village-quest-3d/package.json +1 -1
package/dist/2d.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
1
+ import { C as RendererStats, N as Scene$1, S as GameStats, at as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine } from "./behavior-CKwTCjfR.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
4
- import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
4
+ import { t as LoadSceneOptions } from "./loader-COn5fS0o.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
6
  import { Group, Mesh, Object3D, Scene, Texture } from "three";
7
7
  import * as RapierNs from "@dimforge/rapier2d-compat";
@@ -144,6 +144,22 @@ declare class CharacterBody2D extends PhysicsBody2D {
144
144
  /** @internal Updated by Physics2D.moveAndSlide. */
145
145
  _grounded: boolean;
146
146
  moveAndSlide(): void;
147
+ /** @internal Set by the physics step from the KCC's resolved collisions. */
148
+ _onWall: number;
149
+ /** @internal */
150
+ _onCeiling: boolean;
151
+ /**
152
+ * Touching a wall, and which side (-1 left, +1 right, 0 neither).
153
+ *
154
+ * Wall jump and wall slide are unbuildable without it — not awkward to build,
155
+ * impossible: `isOnFloor()` was the only surface a character could report, so
156
+ * even a hand-written behavior had nothing to ask.
157
+ */
158
+ isOnWall(): boolean;
159
+ /** -1 the wall is to the left, +1 to the right, 0 no wall. */
160
+ wallSide(): number;
161
+ /** Head hit something — cut the jump short, the way every platformer does. */
162
+ isOnCeiling(): boolean;
147
163
  isOnFloor(): boolean;
148
164
  }
149
165
  //#endregion
@@ -321,6 +337,11 @@ interface Game2D {
321
337
  sourceJson: SceneJson;
322
338
  renderer: GameRenderer;
323
339
  physics: Physics2D | null;
340
+ /**
341
+ * The node under a screen pixel, or null. The raycast the renderer already
342
+ * does — reachable now without holding the renderer.
343
+ */
344
+ pick(x: number, y: number): Node | null;
324
345
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
325
346
  stats(): GameStats;
326
347
  /**
@@ -463,18 +484,54 @@ declare class Camera2D extends Node2D {
463
484
  * - `platformer`: x movement + gravity (from the scene `physics.gravity[1]`,
464
485
  * default 980) + jump (`jumpHeight` px → impulse √(2·g·h))
465
486
  * - `topDown`: full-axis movement, no gravity
487
+ *
488
+ * ## Why the extra platformer props exist
489
+ *
490
+ * A jump that only works while `isOnFloor()` is true feels BROKEN, and players
491
+ * do not report it as "the coyote time is missing" — they report the game as
492
+ * unresponsive. The whole schema used to be five props, so a JSON-only game got
493
+ * a character that felt wrong and could not drive `AnimatedSprite2D.play('run')`
494
+ * either. `examples/platformer-2d` hand-rolls all of this in a 541-line
495
+ * behavior; these are those constants, as props.
496
+ *
497
+ * Every one defaults to OFF (0 / 1 / false), so an existing scene behaves
498
+ * exactly as before until it asks for something.
466
499
  */
467
500
  declare class CharacterController2D extends Node {
468
501
  static override readonly typeName: string;
469
502
  static readonly props: PropSchema;
503
+ static readonly signals: readonly string[];
470
504
  mode: string;
471
505
  maxSpeed: number;
472
506
  /** Pixels (platformer mode). */
473
507
  jumpHeight: number;
474
508
  moveAction: string;
475
509
  jumpAction: string;
510
+ coyoteSeconds: number;
511
+ jumpBufferSeconds: number;
512
+ jumpCutMultiplier: number;
513
+ maxJumps: number;
514
+ dashSpeed: number;
515
+ dashSeconds: number;
516
+ dashAction: string;
517
+ wallSlideSpeed: number;
518
+ wallJumpImpulse: number[];
519
+ /** What the character is doing — drive `AnimatedSprite2D.play(state)` off it. */
520
+ state: "idle" | "run" | "jump" | "fall" | "wallSlide" | "dash";
521
+ private coyoteLeft;
522
+ private bufferLeft;
523
+ private jumpsUsed;
524
+ private dashLeft;
525
+ private dashDir;
526
+ private rising;
476
527
  override onReady(): void;
477
528
  override fixedUpdate(dt: number): void;
529
+ /**
530
+ * Emits `movementStateChanged(state)` on a change — the 3D sibling has had
531
+ * this since it shipped, and it is what drives `AnimatedSprite2D.play(...)`
532
+ * from scene JSON instead of a behavior polling velocities.
533
+ */
534
+ private setState;
478
535
  }
479
536
  //#endregion
480
537
  //#region src/2d/nodes/color-rect-2d.d.ts
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-bKHgHcsZ.js";
3
- import { _ as RigidBody2D, a as parseCells, c as ColorRect2D, d as AnimatedSprite2D, f as Sprite2D, g as PhysicsBody2D, h as CharacterBody2D, i as mergeSolidRects, l as CharacterController2D, m as Area2D, n as UILayer, o as Particles2D, p as Joint2D, r as TileMap2D, s as Label, t as registerNodes2D, u as Camera2D, v as StaticBody2D, y as Node2D } from "./register-tkR_8tWg.js";
4
- import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-D9wquBvK.js";
2
+ import { a as AssetStore2D, i as syncTree2D, r as Renderer2D, t as createGame2D } from "./create-game-D2QU5x7S.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-FIJtNbub.js";
4
+ import { n as enablePhysics2D, t as Physics2D } from "./physics-2d-DjXR5DMu.js";
5
5
  //#region src/2d/library-sprite.ts
6
6
  /**
7
7
  * Turn a library sprite-animation JSON into a scene-ready spritesheet asset
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,9 @@
1
- import { C as RendererStats, S as GameStats, X as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine, w as Scene$1 } from "./behavior-D_jMpFh8.js";
1
+ import { C as RendererStats, N as Scene$1, S as GameStats, at as Node, b as Scheduler, l as PropSchema, n as BehaviorCtor, v as Engine } from "./behavior-CKwTCjfR.js";
2
2
  import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-BJb-CWfA.js";
3
3
  import { i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
4
- import { t as LoadSceneOptions } from "./loader-BYBrqTxP.js";
4
+ import { t as LoadSceneOptions } from "./loader-COn5fS0o.js";
5
5
  import { t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
6
- import { n as PathGrid, s as SpatialPose } from "./pathfinding-BwhqPD3i.js";
6
+ import { n as PathGrid, s as SpatialPose } from "./pathfinding-DUw9mir9.js";
7
7
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Vector3, WebGLRenderer } from "three";
8
8
  import { VRM } from "@pixiv/three-vrm";
9
9
  import { Sky } from "three/examples/jsm/objects/Sky.js";
@@ -544,6 +544,11 @@ interface Game3D {
544
544
  sourceJson: SceneJson;
545
545
  renderer: GameRenderer;
546
546
  physics: Physics3D | null;
547
+ /**
548
+ * The node under a screen pixel, or null. The raycast the renderer already
549
+ * does — reachable now without holding the renderer.
550
+ */
551
+ pick(x: number, y: number): Node | null;
547
552
  /** Engine + renderer perf counters in one read (fps/nodes/triangles/…). */
548
553
  stats(): GameStats;
549
554
  /**
package/dist/3d.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
- import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BftxM_It.js";
3
- import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-gXI7PYl0.js";
4
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, V as movementState, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets--DigHNg4.js";
2
+ import { A as Water3D, F as PhysicsBody3D, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D, V as WATER_MAX_RIPPLES, j as WATER_CUTOUT_MAX } from "./gameplay-BpQCbABv.js";
3
+ import { a as Environment3D, c as sunDirectionFromElevationAzimuth, i as syncTree, l as sunDirectionFromSky, o as horizonColorFromSky, r as Renderer3D, s as parseEnvironment3D, t as createGame3D, u as AssetStore3D } from "./create-game-sFuTLqjD.js";
4
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, V as movementState, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-CQtEGogB.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-CnPygVGo.js";
6
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DF8npb1O.js";
7
7
  //#region src/3d/environment-runtime.ts
8
8
  /**
9
9
  * Live environment editing — the renderer re-applies `scene.environment`
@@ -298,6 +298,14 @@ declare class AudioBuses {
298
298
  /** Final gain for a sound on `bus` with its own `sourceVolume` (all clamped). */
299
299
  effectiveVolume(bus: BusName, sourceVolume: number): number;
300
300
  /** Clamp + finite-guard a new value; emit only on a real change. */
301
+ /**
302
+ * Write THEN emit.
303
+ *
304
+ * This used to emit from inside the expression assigning the field, so every
305
+ * listener of `changed` read the OLD value — including the thing the signal
306
+ * exists for ("live re-gain of playing sounds"), which therefore re-gained to
307
+ * the volume you just left.
308
+ */
301
309
  private assign;
302
310
  }
303
311
  //#endregion
@@ -595,6 +603,25 @@ declare class InputMap {
595
603
  private detach;
596
604
  /** Load (or extend with) scene-JSON action declarations. Hard-validates shape. */
597
605
  declare(decls: Record<string, unknown>): void;
606
+ /**
607
+ * Where the pointer is, in CANVAS pixels (top-left origin), or null when it
608
+ * has never been over the canvas.
609
+ *
610
+ * This is what a click-driven game asks for. `pointerDelta()` answers "how far
611
+ * did the mouse move" — the mouse-look question — and for a whole class of
612
+ * genres that is the wrong question and the only one the engine could answer.
613
+ * Pair it with `renderer.pick(x, y)` to get the node under the cursor.
614
+ */
615
+ pointerPosition(): {
616
+ x: number;
617
+ y: number;
618
+ } | null;
619
+ /**
620
+ * @internal Feed a pointer position. `target` scales client → canvas pixels
621
+ * so a CSS-stretched canvas still reports coordinates its renderer can use.
622
+ */
623
+ setPointerPosition(clientX: number, clientY: number, target?: HTMLElement): void;
624
+ private pointerPos;
598
625
  /** Feed a key state change (code = KeyboardEvent.code, e.g. 'Space', 'KeyW'). */
599
626
  handleKey(code: string, isDown: boolean): void;
600
627
  private readonly injectedDown;
@@ -699,6 +726,17 @@ declare class InputMap {
699
726
  isPressed(action: string): boolean;
700
727
  justPressed(action: string): boolean;
701
728
  justReleased(action: string): boolean;
729
+ /**
730
+ * Mouse button state WITHOUT declaring an action for it.
731
+ *
732
+ * A click on a tile is not a bindable action — there is no key to remap, and
733
+ * asking a match-3 to declare `"click": { "keys": ["Mouse0"] }` in every scene
734
+ * is ceremony. Buttons already live in the key set as `Mouse0`/`Mouse1`/…;
735
+ * these read them directly. 0 left, 1 middle, 2 right.
736
+ */
737
+ mousePressed(button?: number): boolean;
738
+ mouseJustPressed(button?: number): boolean;
739
+ mouseJustReleased(button?: number): boolean;
702
740
  /** Normalized direction for a vector2 action (y-down: up = -y). */
703
741
  getVector(action: string): {
704
742
  x: number;
@@ -753,6 +791,89 @@ declare class Scene {
753
791
  toJSON(): SceneJson;
754
792
  }
755
793
  //#endregion
794
+ //#region src/core/save.d.ts
795
+ /**
796
+ * Namespaced game persistence — high scores, unlocks, settings. localStorage
797
+ * in the browser, in-memory headless (tests and SSR stay green). Values are
798
+ * JSON round-tripped, so what you get back is what you saved.
799
+ *
800
+ * const save = createSaveStore('my-game');
801
+ * save.set('highScore', 4200);
802
+ * const best = save.get('highScore', 0);
803
+ */
804
+ interface SaveStore {
805
+ get<T extends JsonValue>(key: string, fallback: T): T;
806
+ set(key: string, value: JsonValue): void;
807
+ remove(key: string): void;
808
+ /** Wipe THIS namespace only. */
809
+ clear(): void;
810
+ }
811
+ declare function createSaveStore(namespace: string): SaveStore;
812
+ //#endregion
813
+ //#region src/core/settings.d.ts
814
+ /** One of three tiers, resolved once at boot and adjustable in an options menu. */
815
+ type QualityTier = "low" | "medium" | "high";
816
+ interface SettingsValues {
817
+ master: number;
818
+ sfx: number;
819
+ music: number;
820
+ muted: boolean;
821
+ quality: QualityTier;
822
+ /** Mouse/stick look sensitivity multiplier. */
823
+ sensitivity: number;
824
+ invertY: boolean;
825
+ /** Reduce camera shake, flashes and other motion. */
826
+ reduceMotion: boolean;
827
+ }
828
+ /** What a device probe can see without a GPU context. */
829
+ interface DeviceHints {
830
+ hardwareConcurrency?: number;
831
+ deviceMemory?: number;
832
+ coarsePointer?: boolean;
833
+ maxTextureSize?: number;
834
+ }
835
+ /**
836
+ * A default tier from what the browser will admit to.
837
+ *
838
+ * Deliberately crude: this picks a STARTING point, and the options menu is the
839
+ * real answer. A phone that reports four cores and a coarse pointer should not
840
+ * open on the setting tuned for a desktop GPU.
841
+ */
842
+ declare function suggestQuality(hints: DeviceHints): QualityTier;
843
+ /** Read what this environment will tell us, safely, without touching WebGL. */
844
+ declare function readDeviceHints(): DeviceHints;
845
+ declare class Settings {
846
+ /** Fires after any change, with the key that changed. */
847
+ readonly changed: Signal<[keyof SettingsValues]>;
848
+ private readonly store;
849
+ private readonly values;
850
+ constructor(namespace?: string, store?: SaveStore);
851
+ get<K extends keyof SettingsValues>(key: K): SettingsValues[K];
852
+ set<K extends keyof SettingsValues>(key: K, value: SettingsValues[K]): void;
853
+ /** Every value at once — for an options screen, or a save-file dump. */
854
+ all(): SettingsValues;
855
+ /** Back to defaults, on disk too. */
856
+ reset(): void;
857
+ /**
858
+ * Push the saved volumes into the engine's buses, and keep writing them back
859
+ * whenever anything changes them.
860
+ *
861
+ * The subscription is the point: a game that sets `engine.audio.music = 0.4`
862
+ * from a slider has already persisted it, without knowing this class exists.
863
+ */
864
+ bindAudio(audio: AudioBuses): () => void;
865
+ }
866
+ /**
867
+ * What a quality tier MEANS, as an environment patch.
868
+ *
869
+ * Only the levers that actually cost: shadows, post, clouds, and the resolution
870
+ * the renderer draws at. Deliberately an `environment` patch, because
871
+ * `setEnvironment3D(engine, patch)` already applies one live and validated —
872
+ * this is not a second rendering pipeline, it is three presets for the one that
873
+ * exists.
874
+ */
875
+ declare function qualityEnvironment(tier: QualityTier): Record<string, JsonValue>;
876
+ //#endregion
756
877
  //#region src/core/stats.d.ts
757
878
  /**
758
879
  * Performance-stats shapes — pure data, importable everywhere (core stays
@@ -824,9 +945,27 @@ declare class Engine {
824
945
  readonly rng: Rng;
825
946
  /** The engine log channel (debug overlay + headless harness tail this). */
826
947
  readonly log: LogManager;
948
+ /**
949
+ * Screen pixel → the node under it, when a renderer has offered one.
950
+ *
951
+ * The raycast has always existed (`Renderer3D.pick` / `Renderer2D.pick`) and
952
+ * was reachable only from whoever held the renderer — so a Behavior, which
953
+ * holds a node, could not use it. Every mouse-driven genre needs exactly this
954
+ * and had no way to ask. Instance-scoped, set by the renderer on construction
955
+ * and cleared on dispose; null when nothing renders (headless).
956
+ */
957
+ picker: ((x: number, y: number) => Node | null) | null;
827
958
  /** Global volume buses — `master` × `sfx`/`music` gain + `muted`. AudioPlayer
828
959
  * routes its volume through this; games set it for global volume/mute. */
829
960
  readonly audio: AudioBuses;
961
+ /**
962
+ * Persisted player settings — volume, quality, sensitivity.
963
+ *
964
+ * `createSaveStore` was good and had zero callers, so every game's volume
965
+ * reset on reload. Reading it here costs nothing (localStorage, once) and
966
+ * `bindAudio` makes the volumes write themselves.
967
+ */
968
+ readonly settings: Settings;
830
969
  /** Low-latency procedural-SFX player (WebAudio, headless-safe). AudioPlayer
831
970
  * presets play through it; games may call `engine.sfx.play(...)` directly. */
832
971
  readonly sfx: SfxEngine;
@@ -1035,4 +1174,4 @@ declare function registeredBehaviors(): string[];
1035
1174
  /** Test isolation helper. */
1036
1175
  declare function clearBehaviors(): void;
1037
1176
  //#endregion
1038
- export { LogEntry as $, VoicePreset as A, SfxParams as B, RendererStats as C, SfxPlayOptions as D, SfxEngine as E, Vec3 as F, MusicManager as G, SynthOptions as H, spatialGain as I, AudioBuses as J, MusicTrack as K, spatialPan as L, ROLLOFF_MODELS as M, RolloffModel as N, isAudioContextAvailable as O, SpatialParams as P, SceneTree as Q, SFX_PRESETS as R, GameStats as S, InputMap as T, synthSfx as U, SfxWave as V, MusicBackend as W, Node as X, BusName as Y, NodeLifecycle as Z, registeredTypes as _, registerBehavior as a, Scheduler as b, PropDef as c, createNode as d, LogLevel as et, getNodeSchema as f, registerNode as g, mergeStaticSignals as h, getBehavior as i, Listener as j, Voice as k, PropSchema as l, getNodeType as m, BehaviorCtor as n, Signal as nt, registeredBehaviors as o, getNodeSignals as p, PlayMusicOptions as q, clearBehaviors as r, SignalListener as rt, NodeCtor as s, Behavior as t, LogManager as tt, clearRegistry as u, Engine as v, Scene as w, EngineStats as x, EngineOptions as y, SFX_PRESET_NAMES as z };
1177
+ export { MusicBackend as $, suggestQuality as A, Listener as B, RendererStats as C, SettingsValues as D, Settings as E, SfxEngine as F, spatialGain as G, RolloffModel as H, SfxPlayOptions as I, SFX_PRESET_NAMES as J, spatialPan as K, isAudioContextAvailable as L, createSaveStore as M, Scene as N, qualityEnvironment as O, InputMap as P, synthSfx as Q, Voice as R, GameStats as S, QualityTier as T, SpatialParams as U, ROLLOFF_MODELS as V, Vec3 as W, SfxWave as X, SfxParams as Y, SynthOptions as Z, registeredTypes as _, registerBehavior as a, Node as at, Scheduler as b, PropDef as c, LogEntry as ct, createNode as d, Signal as dt, MusicManager as et, getNodeSchema as f, SignalListener as ft, registerNode as g, mergeStaticSignals as h, getBehavior as i, BusName as it, SaveStore as j, readDeviceHints as k, PropSchema as l, LogLevel as lt, getNodeType as m, BehaviorCtor as n, PlayMusicOptions as nt, registeredBehaviors as o, NodeLifecycle as ot, getNodeSignals as p, SFX_PRESETS as q, clearBehaviors as r, AudioBuses as rt, NodeCtor as s, SceneTree as st, Behavior as t, MusicTrack as tt, clearRegistry as u, LogManager as ut, Engine as v, DeviceHints as w, EngineStats as x, EngineOptions as y, VoicePreset as z };
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { m as diagnose, n as loadScene, o as computeViewport, s as resolveViewport, y as registerBehavior } from "./loader-BlaRQGaA.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-Dzkd6-os.js";
2
+ import { m as diagnose, n as loadScene, o as computeViewport, s as resolveViewport, y as registerBehavior } from "./loader-Buk8Bu1h.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-CscIzJEO.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { n as registerGameplayBehaviors } from "./gameplay-BftxM_It.js";
8
- import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-tkR_8tWg.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-BpQCbABv.js";
8
+ import { g as PhysicsBody2D, n as UILayer, t as registerNodes2D, u as Camera2D, y as Node2D } from "./register-FIJtNbub.js";
9
9
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
10
- import { n as enablePhysics2D } from "./physics-2d-D9wquBvK.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-DjXR5DMu.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
  /**
@@ -274,9 +274,11 @@ var Renderer2D = class {
274
274
  }, globalThis.devicePixelRatio ?? 1, opts);
275
275
  this.webgl = new WebGLRenderer({
276
276
  canvas: opts.canvas,
277
- antialias: rendering.antialias
277
+ antialias: rendering.antialias,
278
+ preserveDrawingBuffer: rendering.preserveDrawingBuffer
278
279
  });
279
280
  this.webgl.info.autoReset = false;
281
+ this.engine.picker = (x, y) => this.pick(x, y);
280
282
  this.webgl.setPixelRatio(rendering.pixelRatio);
281
283
  this.webgl.info.autoReset = false;
282
284
  this.debugLines = new LineSegments(new BufferGeometry(), new LineBasicMaterial({
@@ -484,6 +486,7 @@ var Renderer2D = class {
484
486
  };
485
487
  }
486
488
  dispose() {
489
+ if (this.engine.picker) this.engine.picker = null;
487
490
  this.disconnect();
488
491
  for (const scene of [this.worldScene, this.uiScene]) scene.traverse((obj) => {
489
492
  const mesh = obj;
@@ -541,6 +544,7 @@ async function createGame2D(opts) {
541
544
  const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
542
545
  if (keyboard) engine.input.attachKeyboard(keyboard);
543
546
  cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
547
+ cleanups.push(engine.settings.bindAudio(engine.audio));
544
548
  const touchMode = opts.touch ?? "auto";
545
549
  if (touchMode !== false) {
546
550
  const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
@@ -627,6 +631,9 @@ async function createGame2D(opts) {
627
631
  sourceJson,
628
632
  renderer,
629
633
  physics,
634
+ pick(x, y) {
635
+ return engine.picker?.(x, y) ?? null;
636
+ },
630
637
  stats() {
631
638
  return {
632
639
  triangles: 0,
@@ -1,13 +1,13 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { m as diagnose, n as loadScene, y as registerBehavior } from "./loader-BlaRQGaA.js";
3
- import { l as AudioPlayer, u as Engine } from "./register-Dzkd6-os.js";
2
+ import { m as diagnose, n as loadScene, y as registerBehavior } from "./loader-Buk8Bu1h.js";
3
+ import { h as Engine, m as AudioPlayer } from "./register-CscIzJEO.js";
4
4
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
5
  import { i as resolveRendering, n as attachTouchControls } from "./touch-BoNg_MnF.js";
6
6
  import { a as poseFromRenderer, i as openBundledEditor, n as crossFade, r as devServerLibrary, t as teardown } from "./teardown-ByzfDPyu.js";
7
- import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BftxM_It.js";
7
+ import { B as createCausticsQuad, F as PhysicsBody3D, R as Node3D, n as registerGameplayBehaviors } from "./gameplay-BpQCbABv.js";
8
8
  import { t as debugSources } from "./debug-draw-BM3DsvtT.js";
9
- import { E as Camera3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets--DigHNg4.js";
10
- import { n as enablePhysics3D } from "./physics-3d-CnPygVGo.js";
9
+ import { E as Camera3D, g as ModelInstance3D, n as registerNodes3D, t as resolveEnvironmentHdri, v as DirectionalLight3D } from "./environment-presets-CQtEGogB.js";
10
+ import { n as enablePhysics3D } from "./physics-3d-DF8npb1O.js";
11
11
  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";
12
12
  import { VRMLoaderPlugin, VRMUtils } from "@pixiv/three-vrm";
13
13
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
@@ -1566,6 +1566,7 @@ var Renderer3D = class {
1566
1566
  preserveDrawingBuffer: rendering.preserveDrawingBuffer
1567
1567
  });
1568
1568
  this.webgl.info.autoReset = false;
1569
+ this.engine.picker = (x, y) => this.pick(x, y);
1569
1570
  this.basePixelRatio = rendering.pixelRatio;
1570
1571
  this.webgl.setPixelRatio(rendering.pixelRatio);
1571
1572
  this.adaptive = opts.adaptiveResolution === false ? null : new AdaptiveResolution();
@@ -2082,6 +2083,7 @@ var Renderer3D = class {
2082
2083
  this.webgl.shadowMap.needsUpdate = true;
2083
2084
  }
2084
2085
  dispose() {
2086
+ if (this.engine.picker) this.engine.picker = null;
2085
2087
  this.disconnect();
2086
2088
  this.threeScene.traverse((obj) => {
2087
2089
  const mesh = obj;
@@ -2260,6 +2262,7 @@ async function createGame3D(opts) {
2260
2262
  opts.onProgress?.(.16 + f * .82, "shaders");
2261
2263
  });
2262
2264
  } catch {}
2265
+ cleanups.push(engine.settings.bindAudio(engine.audio));
2263
2266
  await report(1, "ready");
2264
2267
  function disposeGame() {
2265
2268
  teardown([
@@ -2280,6 +2283,9 @@ async function createGame3D(opts) {
2280
2283
  assetErrors() {
2281
2284
  return renderer.assets?.errors() ?? [];
2282
2285
  },
2286
+ pick(x, y) {
2287
+ return engine.picker?.(x, y) ?? null;
2288
+ },
2283
2289
  stats() {
2284
2290
  return {
2285
2291
  triangles: 0,
package/dist/debug.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { C as RendererStats, et as LogLevel, v as Engine } from "./behavior-D_jMpFh8.js";
1
+ import { C as RendererStats, lt as LogLevel, v as Engine } from "./behavior-CKwTCjfR.js";
2
2
 
3
3
  //#region src/debug/panel.d.ts
4
4
  /** Minimal document surface the overlay needs (injectable for tests). */
@@ -1,4 +1,4 @@
1
- import { i as serializeNode, t as buildNodeJson } from "./loader-BlaRQGaA.js";
1
+ import { i as serializeNode, t as buildNodeJson } from "./loader-Buk8Bu1h.js";
2
2
  //#region src/core/order-groups.ts
3
3
  const ORDER_GROUP_BASE = {
4
4
  background: -2e3,
package/dist/editor.js CHANGED
@@ -1075,6 +1075,41 @@ var dt = {
1075
1075
  en: "Child of HudLayer. Set text/size/color/background; disabled greys it out. Emits the 'pressed' signal (behaviors: node.on('pressed', ...)); press() triggers it programmatically (gamepad menus, tests).",
1076
1076
  ko: "HudLayer의 자식. text/size/color/background를 설정하고 disabled로 비활성화합니다. 'pressed' 시그널을 내며(비헤이비어에서 node.on('pressed', ...)), press()로 코드에서도 누를 수 있습니다(게임패드 메뉴, 테스트)."
1077
1077
  }),
1078
+ F("UiPanel", {
1079
+ en: "A box that holds other widgets — the thing a menu, options screen or inventory actually is.",
1080
+ ko: "다른 위젯을 담는 상자 — 메뉴·설정 화면·인벤토리가 실제로 이것입니다."
1081
+ }, {
1082
+ en: "Child of HudLayer. Widgets under it mount INSIDE it, so tree structure becomes screen structure; panels nest. layout column|row|grid with columns (an inventory is a grid panel, one child per slot), plus gap/padding/background/radius/width/height/border. width or height 0 = size to the contents.",
1083
+ ko: "HudLayer의 자식. 그 아래 위젯들이 이 안에 마운트되어 트리 구조가 화면 구조가 됩니다(중첩 가능). layout은 column|row|grid이고 columns와 함께 씁니다(인벤토리 = grid 패널, 슬롯당 자식 하나). gap/padding/background/radius/width/height/border 지원. width·height가 0이면 내용에 맞춰집니다."
1084
+ }),
1085
+ F("UiImage", {
1086
+ en: "A picture in the HUD — item icons, portraits, a logo.",
1087
+ ko: "HUD 안의 그림 — 아이템 아이콘, 초상화, 로고."
1088
+ }, {
1089
+ en: "src takes a url or a $assetKey (declare the icon once like any other asset). fit contain|cover|fill; tint multiplies through the picture, which is how an unaffordable item greys out in one prop; opacity for disabled states.",
1090
+ ko: "src는 url 또는 $assetKey(아이콘도 다른 에셋처럼 한 번만 선언). fit은 contain|cover|fill, tint는 그림에 곱해져서 살 수 없는 아이템을 프롭 하나로 회색 처리합니다. opacity로 비활성 표현."
1091
+ }),
1092
+ F("UiSlider", {
1093
+ en: "A value the player sets — volume, sensitivity, difficulty.",
1094
+ ko: "플레이어가 조절하는 값 — 볼륨, 감도, 난이도."
1095
+ }, {
1096
+ en: "Emits changed(value). label/value/min/max/step/width/color. Setting .value from a behavior updates the control WITHOUT re-emitting, so restoring a saved setting does not fire the handler that saved it.",
1097
+ ko: "changed(value) 시그널. label/value/min/max/step/width/color. 비헤이비어에서 .value를 설정하면 컨트롤만 갱신되고 changed는 다시 발생하지 않습니다 — 저장된 설정을 복원할 때 저장 핸들러가 다시 도는 것을 막습니다."
1098
+ }),
1099
+ F("UiToggle", {
1100
+ en: "An on/off switch — mute, invert-Y, fullscreen.",
1101
+ ko: "켜기/끄기 스위치 — 음소거, Y축 반전, 전체화면."
1102
+ }, {
1103
+ en: "Emits changed(boolean). label/value. Same no-re-emit rule as UiSlider when set from code.",
1104
+ ko: "changed(boolean) 시그널. label/value. 코드에서 설정할 때 재발생하지 않는 규칙은 UiSlider와 같습니다."
1105
+ }),
1106
+ F("UiSelect", {
1107
+ en: "One of several — quality, resolution, language.",
1108
+ ko: "여러 개 중 하나 — 품질, 해상도, 언어."
1109
+ }, {
1110
+ en: "options is a comma-separated list (\"low,medium,high\"); value is the current one. Emits changed(value).",
1111
+ ko: "options는 쉼표로 구분된 목록(\"low,medium,high\"), value가 현재 선택입니다. changed(value) 시그널."
1112
+ }),
1078
1113
  F("UiDialogue", {
1079
1114
  en: "A typewriter dialogue box with a queue and choice buttons — the RPG conversation layer.",
1080
1115
  ko: "타자기 효과·큐·선택지 버튼을 갖춘 대화창 — RPG 대화 레이어."
@@ -1,10 +1,10 @@
1
- import { t as registerCoreNodes } from "./register-Dzkd6-os.js";
1
+ import { t as registerCoreNodes } from "./register-CscIzJEO.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
4
  import { i as getNodeSchema, l as registerNode } from "./registry-CJdGpT2V.js";
5
5
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
6
6
  import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-Bw7hB93B.js";
7
- import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BftxM_It.js";
7
+ import { A as Water3D, F as PhysicsBody3D, H as colliderFootDrop, I as RigidBody3D, L as StaticBody3D, M as WaterCutout3D, N as Area3D, P as CharacterBody3D, R as Node3D } from "./gameplay-BpQCbABv.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, Color, ConeGeometry, CylinderGeometry, DataTexture, DirectionalLight, DoubleSide, DynamicDrawUsage, Euler, Group, IcosahedronGeometry, ImageBitmapLoader, InstancedBufferAttribute, InstancedMesh, LinearFilter, LoopOnce, LoopRepeat, Matrix4, Mesh, MeshBasicMaterial, MeshDepthMaterial, MeshPhysicalMaterial, MeshStandardMaterial, NearestFilter, NoBlending, 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 { f as Node, g as Behavior, n as loadScene, y as registerBehavior } from "./loader-BlaRQGaA.js";
1
+ import { f as Node, g as Behavior, n as loadScene, y as registerBehavior } from "./loader-Buk8Bu1h.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
4
- import { r as effectiveOrder, t as duplicateNode } from "./duplicate-KRPtUtzl.js";
4
+ import { r as effectiveOrder, t as duplicateNode } from "./duplicate-BgnG1Lqz.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();
@@ -4248,6 +4248,93 @@ var Chase = class extends Behavior {
4248
4248
  }
4249
4249
  };
4250
4250
  //#endregion
4251
+ //#region src/gameplay/clickable.ts
4252
+ /**
4253
+ * Click and hover THIS node — the mouse as gameplay input.
4254
+ *
4255
+ * The raycast has always existed (`Renderer3D.pick` / `Renderer2D.pick`) and was
4256
+ * reachable only from whoever held the renderer, which a Behavior never does.
4257
+ * So every mouse-driven genre — match-3, tower defense, card and deckbuilder,
4258
+ * point-and-click, RTS, idle/clicker — had NO scene JSON to write. Not an
4259
+ * awkward API: none.
4260
+ *
4261
+ * ```json
4262
+ * { "name": "Tile", "type": "MeshInstance3D",
4263
+ * "script": { "name": "Clickable" } }
4264
+ * ```
4265
+ * ```json
4266
+ * "connections": [
4267
+ * { "from": "/Board/Tile", "signal": "clicked", "to": "/Board", "handler": "onTileClicked" }
4268
+ * ]
4269
+ * ```
4270
+ *
4271
+ * `clicked` fires on RELEASE over the same node the press started on — the
4272
+ * convention every button on every platform follows, so a drag that ends
4273
+ * elsewhere is not a click. `hovered`/`unhovered` bracket the cursor being over
4274
+ * it.
4275
+ */
4276
+ var Clickable = class extends Behavior {
4277
+ static props = {
4278
+ /** Mouse button: 0 left, 1 middle, 2 right. */
4279
+ button: { default: 0 },
4280
+ /** Ignore clicks further away than this (0 = any distance). */
4281
+ maxDistance: { default: 0 },
4282
+ /** Stop responding without removing the behavior. */
4283
+ enabled: { default: true }
4284
+ };
4285
+ static signals = [
4286
+ "clicked",
4287
+ "hovered",
4288
+ "unhovered"
4289
+ ];
4290
+ button = 0;
4291
+ maxDistance = 0;
4292
+ enabled = true;
4293
+ /** True while the cursor is over this node — read it to tint a hover state. */
4294
+ hovering = false;
4295
+ pressedHere = false;
4296
+ update() {
4297
+ const engine = this.node.tree?.engine;
4298
+ if (!engine || !this.enabled) return;
4299
+ const picker = engine.picker;
4300
+ const pointer = engine.input.pointerPosition();
4301
+ if (!picker || !pointer) {
4302
+ if (this.hovering) this.setHover(false);
4303
+ return;
4304
+ }
4305
+ const hit = picker(pointer.x, pointer.y);
4306
+ const over = hit !== null && this.isSelfOrDescendant(hit) && this.withinRange(hit);
4307
+ if (over !== this.hovering) this.setHover(over);
4308
+ if (engine.input.mouseJustPressed(this.button)) this.pressedHere = over;
4309
+ if (engine.input.mouseJustReleased(this.button)) {
4310
+ if (this.pressedHere && over) this.emit("clicked", this.node);
4311
+ this.pressedHere = false;
4312
+ }
4313
+ }
4314
+ setHover(next) {
4315
+ this.hovering = next;
4316
+ this.emit(next ? "hovered" : "unhovered", this.node);
4317
+ }
4318
+ /** A pick lands on the visual MESH, which is usually a child of the node. */
4319
+ isSelfOrDescendant(hit) {
4320
+ for (let n = hit; n; n = n.parent) if (n === this.node) return true;
4321
+ return false;
4322
+ }
4323
+ withinRange(hit) {
4324
+ if (this.maxDistance <= 0) return true;
4325
+ if (!this.node.tree?.engine?.scene?.root) return true;
4326
+ const a = this.node.position;
4327
+ const b = hit.position;
4328
+ if (!Array.isArray(a) || !Array.isArray(b)) return true;
4329
+ let sum = 0;
4330
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
4331
+ const d = (a[i] ?? 0) - (b[i] ?? 0);
4332
+ sum += d * d;
4333
+ }
4334
+ return Math.sqrt(sum) <= this.maxDistance;
4335
+ }
4336
+ };
4337
+ //#endregion
4251
4338
  //#region src/gameplay/collector.ts
4252
4339
  /**
4253
4340
  * A tally for the "collect N things" pattern, living on the collector itself
@@ -6037,6 +6124,7 @@ const GAMEPLAY_BEHAVIORS = {
6037
6124
  Collector,
6038
6125
  DamageOnContact,
6039
6126
  Interactable,
6127
+ Clickable,
6040
6128
  FollowCamera,
6041
6129
  Patrol,
6042
6130
  Chase,
@@ -1,4 +1,4 @@
1
- import { X as Node, l as PropSchema, n as BehaviorCtor, t as Behavior, v as Engine } from "./behavior-D_jMpFh8.js";
1
+ import { at as Node, l as PropSchema, n as BehaviorCtor, t as Behavior, v as Engine } from "./behavior-CKwTCjfR.js";
2
2
  import { c as JsonValue, s as JsonObject } from "./schema-CcoWb32N.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-BftxM_It.js";
1
+ import { C as FollowCamera, D as Health, E as DamageOnContact, O as Collector, S as restartScene, T as phaseOf, _ as hitStop, a as Wander, b as GameFlow, c as Projectile, d as PathFollow, f as Oscillate, g as Cooldown, h as CameraShake, i as WaveSpawner, k as Chase, l as Pickup, m as Lifetime, n as registerGameplayBehaviors, o as Spawner, p as MoveTo, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as screenFlash, w as DayNight, x as goToScene, y as Interactable } from "./gameplay-BpQCbABv.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };