incanto 0.4.2 → 0.5.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 (43) hide show
  1. package/dist/2d.d.ts +74 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +174 -16
  4. package/dist/3d.js +4 -4
  5. package/dist/{audio-player-DqUR3XFs.d.ts → audio-player-DkBqRTs4.d.ts} +1 -1
  6. package/dist/{behavior-BAQq7HGM.d.ts → behavior-CWhW3oa6.d.ts} +54 -0
  7. package/dist/{create-game-CZHROKcT.js → create-game-BFESHv1X.js} +37 -25
  8. package/dist/{create-game-CMS7DiPi.js → create-game-D8UvwXme.js} +231 -8
  9. package/dist/debug.d.ts +1 -1
  10. package/dist/{duplicate-DP2WPYom.js → duplicate-BEvGBtb_.js} +1 -1
  11. package/dist/{gameplay-Ccruc3Wd.js → gameplay-CDFgSG6z.js} +293 -24
  12. package/dist/gameplay.d.ts +113 -2
  13. package/dist/gameplay.js +2 -2
  14. package/dist/index.d.ts +139 -5
  15. package/dist/index.js +59 -6
  16. package/dist/{loader-CGs_G-r0.js → loader-B4OEXDZ8.js} +60 -0
  17. package/dist/{loader-Mo0KghCv.d.ts → loader-D2u0fVW2.d.ts} +1 -1
  18. package/dist/net.d.ts +1 -1
  19. package/dist/net.js +1 -1
  20. package/dist/{particle-sim-DYuSUxvK.js → particle-sim-CFkILGwh.js} +84 -3
  21. package/dist/{particle-sim-CbN4YUuH.d.ts → particle-sim-CwJ5rI_P.d.ts} +3 -0
  22. package/dist/{physics-2d-KuMWPTf6.js → physics-2d-KXn1N1jF.js} +95 -10
  23. package/dist/{physics-3d-CXOBOUKG.js → physics-3d-C58oQzTX.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  27. package/dist/{register-DPEV9_9t.js → register-D71C4rDC.js} +76 -5
  28. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  29. package/dist/{register-02aSywx2.js → register-DvPHJdVj.js} +390 -59
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DUVZGcuO.js → agent8-DruKhR22.js} +1 -1
  33. package/editor/assets/index-D2qufqUo.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +847 -133
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-3d-games.md +35 -3
  39. package/skills/incanto-gameplay-behaviors.md +60 -0
  40. package/skills/incanto-hud.md +58 -0
  41. package/skills/incanto-node-reference.md +113 -0
  42. package/skills/incanto-physics-and-input.md +47 -0
  43. package/editor/assets/index-DsM2Yp9F.js +0 -7365
@@ -1,5 +1,5 @@
1
1
  import { c as JsonValue } from "./schema-CcoWb32N.js";
2
- import { T as Node, l as PropSchema, n as BehaviorCtor, t as Behavior } from "./behavior-BAQq7HGM.js";
2
+ import { T as Node, l as PropSchema, n as BehaviorCtor, t as Behavior, v as Engine } from "./behavior-CWhW3oa6.js";
3
3
 
4
4
  //#region src/gameplay/chase.d.ts
5
5
  /**
@@ -154,6 +154,11 @@ declare class FollowCamera extends Behavior {
154
154
  smoothing: number;
155
155
  deadzone: number;
156
156
  override onReady(): void;
157
+ private shakeMag;
158
+ private shakeT;
159
+ private shakeFalloff;
160
+ /** Kick the camera (impacts, explosions). Composes with the follow. */
161
+ shake(magnitude: number, seconds?: number): void;
157
162
  override update(dt: number): void;
158
163
  }
159
164
  //#endregion
@@ -528,6 +533,112 @@ declare class ZombieAI extends Behavior {
528
533
  private pickGoal;
529
534
  }
530
535
  //#endregion
536
+ //#region src/gameplay/game-flow.d.ts
537
+ /**
538
+ * Reload the CURRENT scene from its source JSON — fresh nodes, reset physics,
539
+ * rewired input. The restart primitive every game-over screen wants.
540
+ */
541
+ declare function restartScene(engine: Engine): void;
542
+ type GameFlowState = "playing" | "paused" | "gameover" | "won";
543
+ /**
544
+ * The win/lose/restart state machine 6+ examples hand-rolled as `over`/`win`
545
+ * booleans. Attach to any node (the scene root is natural):
546
+ *
547
+ * const flow = root.behavior as GameFlow; // script: { "name": "GameFlow" }
548
+ * flow.gameOver('YOU DIED'); // freezes time, sticky banner
549
+ * flow.win('AREA CLEAR');
550
+ * flow.pause(); flow.resume();
551
+ *
552
+ * While in `gameover`/`won`, pressing `restartAction` (default action name
553
+ * 'restart' — declare it in the scene input map, or leave undeclared and call
554
+ * `flow.restart()` yourself) reloads the scene from source. Banners render
555
+ * through a `%Banner` UiBanner when one exists; otherwise states are silent
556
+ * (drive your own UI off the `flowChanged` signal).
557
+ */
558
+ declare class GameFlow extends Behavior {
559
+ static readonly signals: readonly string[];
560
+ static readonly props: Record<string, {
561
+ default: string | boolean;
562
+ }>;
563
+ /** Input action that restarts from gameover/won (declare it in `input{}`). */
564
+ restartAction: string;
565
+ /** Freeze `engine.timeScale` on gameover/won (banner UI keeps rendering). */
566
+ freezeOnEnd: boolean;
567
+ /** Where the flow looks for a UiBanner ('' = never). */
568
+ bannerPath: string;
569
+ state: GameFlowState;
570
+ private frozeScale;
571
+ gameOver(text?: string, color?: string): void;
572
+ win(text?: string, color?: string): void;
573
+ pause(): void;
574
+ resume(): void;
575
+ restart(): void;
576
+ private transition;
577
+ private freeze;
578
+ private thaw;
579
+ override update(): void;
580
+ }
581
+ //#endregion
582
+ //#region src/gameplay/juice.d.ts
583
+ /**
584
+ * Game-feel primitives ("juice"): the weapon cooldown, camera shake, screen
585
+ * flash and hit-stop that action examples kept hand-rolling.
586
+ */
587
+ /**
588
+ * A fire-rate / ability cooldown — replaces the copy-pasted
589
+ * `this.clock += dt * 1000; if (now > nextFire)` pattern:
590
+ *
591
+ * private gun = new Cooldown(0.2);
592
+ * override update(dt: number): void {
593
+ * this.gun.tick(dt);
594
+ * if (this.engine.input.isPressed('fire') && this.gun.tryUse()) this.shoot();
595
+ * }
596
+ */
597
+ declare class Cooldown {
598
+ readonly seconds: number;
599
+ private remaining;
600
+ constructor(seconds: number);
601
+ /** Advance time. Call once per update with the frame dt. */
602
+ tick(dt: number): void;
603
+ get ready(): boolean;
604
+ /** 0..1 — how far through the cooldown we are (1 = ready). UiBar-friendly. */
605
+ get progress(): number;
606
+ /** Consume if ready. Returns whether the action should fire. */
607
+ tryUse(): boolean;
608
+ /** Force-ready (pickups that reset your reload). */
609
+ reset(): void;
610
+ }
611
+ /**
612
+ * Camera shake as a standalone behavior for cameras WITHOUT another script.
613
+ * (FollowCamera has this built in — one script per node.) Composes with any
614
+ * other position writer by applying only the DELTA of its own offset, so the
615
+ * camera returns exactly to where the other writer left it.
616
+ *
617
+ * (camera.behavior as CameraShake).shake(8); // pixels (2D) / meters·100 feel (3D: use ~0.2)
618
+ */
619
+ declare class CameraShake extends Behavior {
620
+ static readonly props: Record<string, {
621
+ default: number;
622
+ }>;
623
+ /** Seconds a shake takes to decay to zero. */
624
+ falloff: number;
625
+ private magnitude;
626
+ private t;
627
+ private prev;
628
+ shake(magnitude: number, seconds?: number): void;
629
+ override update(dt: number): void;
630
+ }
631
+ /**
632
+ * Full-screen color flash (damage red, pickup white). DOM overlay above
633
+ * everything; headless no-op. Repeated calls restart the fade.
634
+ */
635
+ declare function screenFlash(color?: string, opacity?: number, seconds?: number): void;
636
+ /**
637
+ * Hit-stop: freeze game time for `seconds` of REAL time, then restore the
638
+ * previous timeScale. Stacking calls extend the freeze instead of fighting.
639
+ */
640
+ declare function hitStop(engine: Engine, seconds?: number): void;
641
+ //#endregion
531
642
  //#region src/gameplay/index.d.ts
532
643
  /** Every built-in gameplay behavior, keyed by its registration name. */
533
644
  declare const GAMEPLAY_BEHAVIORS: Readonly<Record<string, BehaviorCtor>>;
@@ -540,4 +651,4 @@ declare function registerGameplayBehaviors(opts?: {
540
651
  replace?: boolean;
541
652
  }): void;
542
653
  //#endregion
543
- export { Chase, Collector, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, registerGameplayBehaviors };
654
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, type GameFlowState, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, hitStop, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as Health, a as Wander, c as Projectile, d as Oscillate, f as MoveTo, g as DamageOnContact, h as FollowCamera, i as WaveSpawner, l as Pickup, m as Interactable, n as registerGameplayBehaviors, o as Spawner, p as Lifetime, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as Collector, y as Chase } from "./gameplay-Ccruc3Wd.js";
2
- export { Chase, Collector, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, registerGameplayBehaviors };
1
+ import { C as Health, S as DamageOnContact, T as Chase, _ as screenFlash, a as Wander, b as restartScene, c as Projectile, d as Oscillate, f as MoveTo, g as hitStop, h as Cooldown, i as WaveSpawner, l as Pickup, m as CameraShake, n as registerGameplayBehaviors, o as Spawner, p as Lifetime, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Patrol, v as Interactable, w as Collector, x as FollowCamera, y as GameFlow } from "./gameplay-CDFgSG6z.js";
2
+ export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, Patrol, Pickup, Projectile, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, hitStop, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  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-CcoWb32N.js";
2
- import { $ as BusName, A as LogManager, B as spatialGain, C as RendererStats, D as SceneTree, E as NodeLifecycle, F as Listener, G as SfxWave, H as SFX_PRESETS, I as ROLLOFF_MODELS, J as MusicBackend, K as SynthOptions, L as RolloffModel, M as SfxEngine, N as SfxPlayOptions, O as LogEntry, P as isAudioContextAvailable, Q as AudioBuses, R as SpatialParams, S as GameStats, T as Node, U as SFX_PRESET_NAMES, V as spatialPan, W as SfxParams, X as MusicTrack, Y as MusicManager, Z as PlayMusicOptions, _ as registeredTypes, a as registerBehavior, b as Scheduler, c as PropDef, d as createNode, et as Signal, f as getNodeSchema, g as registerNode, h as mergeStaticSignals, i as getBehavior, j as InputMap, k as LogLevel, l as PropSchema, m as getNodeType, n as BehaviorCtor, o as registeredBehaviors, p as getNodeSignals, q as synthSfx, r as clearBehaviors, s as NodeCtor, t as Behavior, tt as SignalListener, u as clearRegistry, v as Engine, w as Scene, x as EngineStats, y as EngineOptions, z as Vec3 } from "./behavior-BAQq7HGM.js";
3
- import { n as loadScene, t as LoadSceneOptions } from "./loader-Mo0KghCv.js";
4
- import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-CbN4YUuH.js";
5
- import { n as AudioPlayer, t as AudioElementLike } from "./audio-player-DqUR3XFs.js";
2
+ import { $ as BusName, A as LogManager, B as spatialGain, C as RendererStats, D as SceneTree, E as NodeLifecycle, F as Listener, G as SfxWave, H as SFX_PRESETS, I as ROLLOFF_MODELS, J as MusicBackend, K as SynthOptions, L as RolloffModel, M as SfxEngine, N as SfxPlayOptions, O as LogEntry, P as isAudioContextAvailable, Q as AudioBuses, R as SpatialParams, S as GameStats, T as Node, U as SFX_PRESET_NAMES, V as spatialPan, W as SfxParams, X as MusicTrack, Y as MusicManager, Z as PlayMusicOptions, _ as registeredTypes, a as registerBehavior, b as Scheduler, c as PropDef, d as createNode, et as Signal, f as getNodeSchema, g as registerNode, h as mergeStaticSignals, i as getBehavior, j as InputMap, k as LogLevel, l as PropSchema, m as getNodeType, n as BehaviorCtor, o as registeredBehaviors, p as getNodeSignals, q as synthSfx, r as clearBehaviors, s as NodeCtor, t as Behavior, tt as SignalListener, u as clearRegistry, v as Engine, w as Scene, x as EngineStats, y as EngineOptions, z as Vec3 } from "./behavior-CWhW3oa6.js";
3
+ import { n as loadScene, t as LoadSceneOptions } from "./loader-D2u0fVW2.js";
4
+ import { n as ParticleSimConfig, r as ParticleView, t as ParticleSim } from "./particle-sim-CwJ5rI_P.js";
5
+ import { n as AudioPlayer, t as AudioElementLike } from "./audio-player-DkBqRTs4.js";
6
6
  import { n as IncantoErrorCode, r as IncantoErrorDetails, t as IncantoError } from "./errors-BMFaY68Q.js";
7
7
 
8
8
  //#region src/core/audio/crossfade.d.ts
@@ -72,6 +72,121 @@ type ParsedNodePath = {
72
72
  };
73
73
  declare function parseNodePath(path: string): ParsedNodePath;
74
74
  //#endregion
75
+ //#region src/core/nodes/hud.d.ts
76
+ /**
77
+ * DOM-backed HUD widgets — the layer 18 of 22 example games hand-rolled.
78
+ *
79
+ * These are CORE nodes (no three.js): they render into a fixed, pointer-
80
+ * transparent overlay ABOVE the canvas, so the same JSON works over the 2D
81
+ * and 3D renderers alike. Headless (no `document`) every widget is a silent
82
+ * no-op — scenes stay fully testable in plain node.
83
+ *
84
+ * { "name": "HUD", "type": "HudLayer", "children": [
85
+ * { "name": "Health", "type": "UiBar",
86
+ * "props": { "anchor": "topLeft", "value": 100, "max": 100, "label": "HP" } },
87
+ * { "name": "Score", "type": "UiText",
88
+ * "props": { "anchor": "topRight", "text": "0", "size": 22 } },
89
+ * { "name": "Banner", "type": "UiBanner" }
90
+ * ]}
91
+ *
92
+ * Behaviors talk to them like any node:
93
+ * (this.node.getNode('%Score') as UiText).text = `${score}`;
94
+ * (this.node.getNode('%Banner') as UiBanner).show('WAVE 2', { color: '#f66' });
95
+ */
96
+ type HudAnchor = "topLeft" | "top" | "topRight" | "left" | "center" | "right" | "bottomLeft" | "bottom" | "bottomRight";
97
+ /**
98
+ * The overlay container. One per scene is plenty; widgets mount into its
99
+ * anchor slots. `zIndex` lifts it above game canvases; the layer never eats
100
+ * pointer events (widgets that need clicks opt in individually).
101
+ */
102
+ declare class HudLayer extends Node {
103
+ static override readonly typeName: string;
104
+ static readonly props: PropSchema;
105
+ zIndex: number;
106
+ visible: boolean;
107
+ /** @internal root overlay element (null headless). */
108
+ _element: HTMLElement | null;
109
+ private readonly slots;
110
+ override onEnterTree(): void;
111
+ override onExitTree(): void;
112
+ override update(): void;
113
+ /** @internal Widgets mount into per-anchor flex columns. */
114
+ _slot(anchor: HudAnchor): HTMLElement | null;
115
+ }
116
+ /** Shared plumbing: mount into the parent HudLayer's anchor slot. */
117
+ declare abstract class HudWidget extends Node {
118
+ static readonly props: PropSchema;
119
+ anchor: HudAnchor;
120
+ visible: boolean;
121
+ /** @internal */
122
+ _element: HTMLElement | null;
123
+ protected abstract _build(): HTMLElement;
124
+ private layer;
125
+ override onReady(): void;
126
+ override onExitTree(): void;
127
+ override update(_dt: number): void;
128
+ protected _sync(): void;
129
+ }
130
+ /** A text line (score, timer, hints). Set `.text` from behaviors. */
131
+ declare class UiText extends HudWidget {
132
+ static override readonly typeName: string;
133
+ static override readonly props: PropSchema;
134
+ text: string;
135
+ size: number;
136
+ color: string;
137
+ shadow: boolean;
138
+ private last;
139
+ protected _build(): HTMLElement;
140
+ protected override _sync(): void;
141
+ }
142
+ /** A labeled progress bar (health, stamina, reload, boss HP). */
143
+ declare class UiBar extends HudWidget {
144
+ static override readonly typeName: string;
145
+ static override readonly props: PropSchema;
146
+ value: number;
147
+ max: number;
148
+ width: number;
149
+ height: number;
150
+ color: string;
151
+ lowColor: string;
152
+ lowThreshold: number;
153
+ background: string;
154
+ label: string;
155
+ private fill;
156
+ private lastRatio;
157
+ /** Current fill ratio 0..1 (what the bar shows). */
158
+ get ratio(): number;
159
+ protected _build(): HTMLElement;
160
+ protected override _sync(): void;
161
+ }
162
+ /**
163
+ * Center-screen announcements ("WAVE 2", "YOU DIED", "LEVEL UP") with fade
164
+ * in/out and a queue — call `.show(text, { color, seconds })`; `sticky:true`
165
+ * (seconds: 0) keeps it until the next show(). Emits `bannerShown(text)`.
166
+ */
167
+ declare class UiBanner extends HudWidget {
168
+ static override readonly typeName: string;
169
+ static readonly signals: string[];
170
+ static override readonly props: PropSchema;
171
+ override anchor: HudAnchor;
172
+ size: number;
173
+ /** Default display time (per-show override via options). */
174
+ seconds: number;
175
+ private queue;
176
+ private current;
177
+ private remaining;
178
+ /** What the banner is showing right now ('' when idle) — test-friendly. */
179
+ get showing(): string;
180
+ show(text: string, opts?: {
181
+ color?: string;
182
+ seconds?: number;
183
+ }): void;
184
+ /** Drop everything (scene transitions). */
185
+ clear(): void;
186
+ protected _build(): HTMLElement;
187
+ override update(dt: number): void;
188
+ }
189
+ //#endregion
75
190
  //#region src/core/nodes/timer.d.ts
76
191
  /**
77
192
  * The canonical serializable game clock — never `setTimeout` in game logic.
@@ -178,6 +293,25 @@ declare function resolveRendering(environment: JsonObject | undefined, fallback:
178
293
  pixelRatio?: number;
179
294
  }): ResolvedRendering;
180
295
  //#endregion
296
+ //#region src/core/save.d.ts
297
+ /**
298
+ * Namespaced game persistence — high scores, unlocks, settings. localStorage
299
+ * in the browser, in-memory headless (tests and SSR stay green). Values are
300
+ * JSON round-tripped, so what you get back is what you saved.
301
+ *
302
+ * const save = createSaveStore('my-game');
303
+ * save.set('highScore', 4200);
304
+ * const best = save.get('highScore', 0);
305
+ */
306
+ interface SaveStore {
307
+ get<T extends JsonValue>(key: string, fallback: T): T;
308
+ set(key: string, value: JsonValue): void;
309
+ remove(key: string): void;
310
+ /** Wipe THIS namespace only. */
311
+ clear(): void;
312
+ }
313
+ declare function createSaveStore(namespace: string): SaveStore;
314
+ //#endregion
181
315
  //#region src/core/scene/constants.d.ts
182
316
  /** The single reserved key marking a prop value as a constant reference. */
183
317
  declare const CONST_REF_KEY = "@const";
@@ -302,4 +436,4 @@ declare function computeViewport(canvasW: number, canvasH: number, viewport: {
302
436
  /** Engine version. Kept in sync with package.json by the release pipeline. */
303
437
  declare const VERSION: string;
304
438
  //#endregion
305
- export { AudioBuses, type AudioElementLike, AudioPlayer, Behavior, type BehaviorCtor, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, Engine, type EngineOptions, type EngineStats, type GameStats, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LoadSceneOptions, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, ROLLOFF_MODELS, type RendererStats, type ResolvedRendering, type ResolvedViewport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, type SceneJson, SceneTree, type Scheduler, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, Timer, TouchControls, type TouchControlsOptions, VERSION, type Vec3, type ViewportFit, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, crossfadeGains, duplicateNode, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, synthSfx };
439
+ export { AudioBuses, type AudioElementLike, AudioPlayer, Behavior, type BehaviorCtor, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, Engine, type EngineOptions, type EngineStats, type GameStats, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LoadSceneOptions, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, ROLLOFF_MODELS, type RendererStats, type ResolvedRendering, type ResolvedViewport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiText, VERSION, type Vec3, type ViewportFit, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, synthSfx };
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { _ as registerBehavior, a as SCENE_FORMAT, c as SceneTree, d as resolveConstants, f as Node, g as getBehavior, h as clearBehaviors, i as serializeNode, l as CONST_REF_KEY, m as Behavior, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, v as registeredBehaviors, y as Signal } from "./loader-CGs_G-r0.js";
2
- import { a as Engine, c as SfxEngine, d as WebAudioMusicBackend, f as crossfadeGains, i as applyParticlePreset, l as isAudioContextAvailable, m as AudioBuses, n as PARTICLE_PRESETS, o as LogManager, p as fadeGain, r as PARTICLE_PRESET_NAMES, s as InputMap, t as ParticleSim, u as MusicManager } from "./particle-sim-DYuSUxvK.js";
1
+ import { _ as registerBehavior, a as SCENE_FORMAT, c as SceneTree, d as resolveConstants, f as Node, g as getBehavior, h as clearBehaviors, i as serializeNode, l as CONST_REF_KEY, m as Behavior, n as loadScene, o as computeViewport, p as parseNodePath, r as Scene, s as resolveViewport, u as isConstRef, v as registeredBehaviors, y as Signal } from "./loader-B4OEXDZ8.js";
2
+ import { a as Engine, c as SfxEngine, d as WebAudioMusicBackend, f as crossfadeGains, i as applyParticlePreset, l as isAudioContextAvailable, m as AudioBuses, n as PARTICLE_PRESETS, o as LogManager, p as fadeGain, r as PARTICLE_PRESET_NAMES, s as InputMap, t as ParticleSim, u as MusicManager } from "./particle-sim-CFkILGwh.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { a as spatialGain, c as SFX_PRESET_NAMES, i as ROLLOFF_MODELS, l as synthSfx, n as Timer, o as spatialPan, r as AudioPlayer, s as SFX_PRESETS, t as registerCoreNodes } from "./register-BuUV1_KB.js";
5
+ import { a as UiBar, c as ROLLOFF_MODELS, d as SFX_PRESETS, f as SFX_PRESET_NAMES, i as UiBanner, l as spatialGain, n as Timer, o as UiText, p as synthSfx, r as HudLayer, s as AudioPlayer, t as registerCoreNodes, u as spatialPan } from "./register-Dl_ixIJe.js";
6
6
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-BLk7H2Qa.js";
7
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-BVJ2HbCn.js";
8
8
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
9
9
  import { i as resolveRendering, n as attachTouchControls, r as joystickVector, t as TouchControls } from "./touch-031PxtCR.js";
10
- import { t as duplicateNode } from "./duplicate-DP2WPYom.js";
10
+ import { t as duplicateNode } from "./duplicate-BEvGBtb_.js";
11
11
  //#region src/core/preload.ts
12
12
  /**
13
13
  * Warm the HTTP cache for a list of asset urls with a progress callback —
@@ -40,6 +40,59 @@ function assetUrls(assets) {
40
40
  return Object.values(assets ?? {}).map((a) => a.url).filter((u) => typeof u === "string" && !u.startsWith("data:"));
41
41
  }
42
42
  //#endregion
43
+ //#region src/core/save.ts
44
+ const memoryStores = /* @__PURE__ */ new Map();
45
+ function createSaveStore(namespace) {
46
+ const prefix = `incanto:${namespace}:`;
47
+ const local = storageOrNull();
48
+ if (!local) {
49
+ let mem = memoryStores.get(namespace);
50
+ if (!mem) {
51
+ mem = /* @__PURE__ */ new Map();
52
+ memoryStores.set(namespace, mem);
53
+ }
54
+ const backing = mem;
55
+ return {
56
+ get: (key, fallback) => parse(backing.get(key), fallback),
57
+ set: (key, value) => void backing.set(key, JSON.stringify(value)),
58
+ remove: (key) => void backing.delete(key),
59
+ clear: () => backing.clear()
60
+ };
61
+ }
62
+ return {
63
+ get: (key, fallback) => parse(local.getItem(prefix + key) ?? void 0, fallback),
64
+ set: (key, value) => local.setItem(prefix + key, JSON.stringify(value)),
65
+ remove: (key) => local.removeItem(prefix + key),
66
+ clear: () => {
67
+ const doomed = [];
68
+ for (let i = 0; i < local.length; i++) {
69
+ const k = local.key(i);
70
+ if (k?.startsWith(prefix)) doomed.push(k);
71
+ }
72
+ for (const k of doomed) local.removeItem(k);
73
+ }
74
+ };
75
+ }
76
+ function parse(raw, fallback) {
77
+ if (raw === void 0 || raw === null) return fallback;
78
+ try {
79
+ return JSON.parse(raw);
80
+ } catch {
81
+ return fallback;
82
+ }
83
+ }
84
+ function storageOrNull() {
85
+ try {
86
+ if (typeof localStorage === "undefined") return null;
87
+ const probe = "__incanto_probe__";
88
+ localStorage.setItem(probe, "1");
89
+ localStorage.removeItem(probe);
90
+ return localStorage;
91
+ } catch {
92
+ return null;
93
+ }
94
+ }
95
+ //#endregion
43
96
  //#region src/core/uid-gen.ts
44
97
  /**
45
98
  * The ONE way to mint a node uid: crypto-strength, `n_` + 16 base36 chars
@@ -57,6 +110,6 @@ function newUid() {
57
110
  //#endregion
58
111
  //#region src/index.ts
59
112
  /** Engine version. Kept in sync with package.json by the release pipeline. */
60
- const VERSION = "0.4.2";
113
+ const VERSION = "0.5.0";
61
114
  //#endregion
62
- export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, IncantoError, InputMap, LogManager, MusicManager, Node, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, crossfadeGains, duplicateNode, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, synthSfx };
115
+ export { AudioBuses, AudioPlayer, Behavior, CONST_REF_KEY, Engine, HudLayer, IncantoError, InputMap, LogManager, MusicManager, Node, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, Scene, SceneTree, SfxEngine, Signal, Timer, TouchControls, UiBanner, UiBar, UiText, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, duplicateNode, fadeGain, getBehavior, getNodeSchema, getNodeSignals, getNodeType, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, mergeStaticSignals, newUid, parseNodePath, preloadUrls, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, resolveConstants, resolveRendering, resolveViewport, serializeNode, spatialGain, spatialPan, synthSfx };
@@ -192,8 +192,13 @@ var Node = class {
192
192
  }
193
193
  set name(value) {
194
194
  validateName(value);
195
+ const old = this._name;
195
196
  if (this._parent) this._name = uniqueSiblingName(value, this._parent._children, this);
196
197
  else this._name = value;
198
+ if (this._tree && this._name !== old) {
199
+ this._tree._unindexName(this, old);
200
+ this._tree._indexName(this);
201
+ }
197
202
  }
198
203
  get parent() {
199
204
  return this._parent;
@@ -260,6 +265,10 @@ var Node = class {
260
265
  }
261
266
  /** Every node in THIS subtree named `name` — names repeat, so a list. */
262
267
  getNodesByName(name) {
268
+ if (this._tree && this._parent === null) {
269
+ const set = this._tree._nodesNamed(name);
270
+ return set ? [...set] : [];
271
+ }
263
272
  const out = [];
264
273
  const walk = (node) => {
265
274
  if (node.name === name) out.push(node);
@@ -284,6 +293,17 @@ var Node = class {
284
293
  resolve(path) {
285
294
  const parsed = parseNodePath(path);
286
295
  if (parsed.kind === "unique") {
296
+ const tree = this._tree;
297
+ if (tree) {
298
+ const set = tree._nodesNamed(parsed.name);
299
+ if (!set || set.size === 0) return null;
300
+ if (set.size > 1) {
301
+ const paths = [];
302
+ for (const m of set) paths.push(m.getPath());
303
+ throw new IncantoError("DUPLICATE_UNIQUE_NAME", `'%${parsed.name}' is ambiguous: ${set.size} nodes named '${parsed.name}' (${paths.join(", ")}).`);
304
+ }
305
+ for (const m of set) return m;
306
+ }
287
307
  const matches = [];
288
308
  collectByName(this.getRoot(), parsed.name, matches);
289
309
  if (matches.length > 1) throw new IncantoError("DUPLICATE_UNIQUE_NAME", `'%${parsed.name}' is ambiguous: ${matches.length} nodes named '${parsed.name}' (${matches.map((m) => m.getPath()).join(", ")}).`);
@@ -388,6 +408,7 @@ var Node = class {
388
408
  /** @internal */
389
409
  _propagateEnterTree(tree) {
390
410
  this._tree = tree;
411
+ tree._indexName(this);
391
412
  this.onEnterTree();
392
413
  this.behavior?.onEnterTree?.();
393
414
  for (const c of [...this._children]) if (c._tree !== tree) c._propagateEnterTree(tree);
@@ -406,6 +427,7 @@ var Node = class {
406
427
  for (const c of [...this._children]) c._propagateExitTree();
407
428
  this.onExitTree();
408
429
  this.behavior?.onExitTree?.();
430
+ this._tree?._unindexName(this);
409
431
  this._tree = null;
410
432
  }
411
433
  /** @internal */
@@ -507,6 +529,18 @@ var SceneTree = class {
507
529
  _root = null;
508
530
  _engine = null;
509
531
  freeQueue = /* @__PURE__ */ new Set();
532
+ /**
533
+ * name → attached nodes with that name. Maintained by Node enter/exit/rename
534
+ * so `%name` targeting and root-level getNodesByName are O(1) — behaviors
535
+ * resolve targets every frame and a full-tree walk per chaser melted horde
536
+ * scenes (N chasers × whole tree, 60×/s).
537
+ */
538
+ _nameIndex = /* @__PURE__ */ new Map();
539
+ /**
540
+ * Bumped whenever ANY node attaches or detaches — cheap "did the tree
541
+ * change shape?" check for per-step body gathers and similar caches.
542
+ */
543
+ _structureVersion = 0;
510
544
  _frameId = 0;
511
545
  /**
512
546
  * Monotonic update-frame counter — bumped once per `update()`. Per-frame
@@ -516,6 +550,29 @@ var SceneTree = class {
516
550
  get frameId() {
517
551
  return this._frameId;
518
552
  }
553
+ /** @internal Node attach hook. */
554
+ _indexName(node) {
555
+ this._structureVersion++;
556
+ let set = this._nameIndex.get(node.name);
557
+ if (!set) {
558
+ set = /* @__PURE__ */ new Set();
559
+ this._nameIndex.set(node.name, set);
560
+ }
561
+ set.add(node);
562
+ }
563
+ /** @internal Node detach/rename hook (`name` = the entry to remove). */
564
+ _unindexName(node, name = node.name) {
565
+ this._structureVersion++;
566
+ const set = this._nameIndex.get(name);
567
+ if (set) {
568
+ set.delete(node);
569
+ if (set.size === 0) this._nameIndex.delete(name);
570
+ }
571
+ }
572
+ /** @internal Every ATTACHED node named `name` (undefined = none). */
573
+ _nodesNamed(name) {
574
+ return this._nameIndex.get(name);
575
+ }
519
576
  /**
520
577
  * @internal Opaque per-frame scratch the 3d layer parks its shared moving-body
521
578
  * snapshot on (kept here so it is per-tree-instance, never a module global —
@@ -709,7 +766,10 @@ var Scene = class {
709
766
  /** Design-resolution viewport (consumed by renderers via resolveViewport). */
710
767
  viewport;
711
768
  connections;
769
+ /** The full source JSON this scene was loaded from — `restartScene` fuel. */
770
+ source;
712
771
  constructor(source, root, tree) {
772
+ this.source = jsonClone(source);
713
773
  this.name = source.name;
714
774
  this.dimension = source.dimension;
715
775
  this.root = root;
@@ -1,5 +1,5 @@
1
1
  import { i as SceneJson } from "./schema-CcoWb32N.js";
2
- import { v as Engine, w as Scene } from "./behavior-BAQq7HGM.js";
2
+ import { v as Engine, w as Scene } from "./behavior-CWhW3oa6.js";
3
3
 
4
4
  //#region src/core/scene/loader.d.ts
5
5
  interface LoadSceneOptions {
package/dist/net.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { c as JsonValue, i as SceneJson, s as JsonObject } from "./schema-CcoWb32N.js";
2
- import { T as Node, et as Signal, l as PropSchema, v as Engine } from "./behavior-BAQq7HGM.js";
2
+ import { T as Node, et as Signal, l as PropSchema, v as Engine } from "./behavior-CWhW3oa6.js";
3
3
 
4
4
  //#region src/net/types.d.ts
5
5
  type Unsubscribe = () => void;
package/dist/net.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
2
  import { t as jsonClone } from "./json-BLk7H2Qa.js";
3
3
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
4
- import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-B0gq63VW.js";
4
+ import { i as applySyncPatch, n as NetworkSpawner, r as NetworkManager, t as registerNodesNet } from "./register-C35HDZpm.js";
5
5
  //#region src/net/loopback.ts
6
6
  /**
7
7
  * In-memory multiplayer hub implementing the SAME kernel contract as
@@ -1,7 +1,7 @@
1
- import { y as Signal } from "./loader-CGs_G-r0.js";
1
+ import { y as Signal } from "./loader-B4OEXDZ8.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as Rng } from "./rng-DP-SR7eg.js";
4
- import { l as synthSfx } from "./register-BuUV1_KB.js";
4
+ import { p as synthSfx } from "./register-Dl_ixIJe.js";
5
5
  import { n as jsonEquals } from "./json-BLk7H2Qa.js";
6
6
  //#region src/core/audio/buses.ts
7
7
  /**
@@ -684,6 +684,75 @@ var InputMap = class {
684
684
  * wheel → wheelDelta. `lockOnClick` requests pointer lock on mousedown
685
685
  * (the FPS pattern).
686
686
  */
687
+ padAxesState = {
688
+ lx: 0,
689
+ ly: 0,
690
+ rx: 0,
691
+ ry: 0
692
+ };
693
+ padButtonsDown = 0;
694
+ /**
695
+ * Feed one polled gamepad snapshot (standard mapping). Buttons become
696
+ * codes `Pad0`..`Pad16` in the same space as keys — declare them in input
697
+ * actions like any key ("jump": ["Space", "Pad0"]). Axes land in
698
+ * `padAxes()` with a deadzone. Pass null when no pad is connected.
699
+ */
700
+ pollGamepad(pad) {
701
+ const dead = .15;
702
+ const shape = (v) => Math.abs(v) < dead ? 0 : v;
703
+ if (!pad) {
704
+ for (let i = 0; i < 17; i++) if (this.padButtonsDown & 1 << i) this.handleKey(`Pad${i}`, false);
705
+ this.padButtonsDown = 0;
706
+ this.padAxesState.lx = 0;
707
+ this.padAxesState.ly = 0;
708
+ this.padAxesState.rx = 0;
709
+ this.padAxesState.ry = 0;
710
+ return;
711
+ }
712
+ for (let i = 0; i < Math.min(17, pad.buttons.length); i++) {
713
+ const down = pad.buttons[i]?.pressed === true;
714
+ if (down !== ((this.padButtonsDown & 1 << i) !== 0)) {
715
+ this.handleKey(`Pad${i}`, down);
716
+ this.padButtonsDown = down ? this.padButtonsDown | 1 << i : this.padButtonsDown & ~(1 << i);
717
+ }
718
+ }
719
+ this.padAxesState.lx = shape(pad.axes[0] ?? 0);
720
+ this.padAxesState.ly = shape(pad.axes[1] ?? 0);
721
+ this.padAxesState.rx = shape(pad.axes[2] ?? 0);
722
+ this.padAxesState.ry = shape(pad.axes[3] ?? 0);
723
+ }
724
+ /** Deadzoned analog sticks: 0 = left {x,y}, 1 = right. */
725
+ padAxes(stick = 0) {
726
+ return stick === 0 ? {
727
+ x: this.padAxesState.lx,
728
+ y: this.padAxesState.ly
729
+ } : {
730
+ x: this.padAxesState.rx,
731
+ y: this.padAxesState.ry
732
+ };
733
+ }
734
+ /**
735
+ * Poll the browser Gamepad API every frame (the first connected pad).
736
+ * Wire once at boot: `engine.input.attachGamepad(engine)`. Headless no-op.
737
+ */
738
+ attachGamepad(engine) {
739
+ if (typeof navigator === "undefined" || typeof navigator.getGamepads !== "function") return () => {};
740
+ const disconnect = engine.updated.connect(() => {
741
+ const pads = navigator.getGamepads();
742
+ let pad = null;
743
+ for (const p of pads) if (p?.connected) {
744
+ pad = p;
745
+ break;
746
+ }
747
+ this.pollGamepad(pad);
748
+ });
749
+ const prev = this.detach;
750
+ this.detach = () => {
751
+ prev?.();
752
+ disconnect();
753
+ };
754
+ return disconnect;
755
+ }
687
756
  attachPointer(target, opts) {
688
757
  let buttonsHeld = 0;
689
758
  const onDown = (e) => {
@@ -936,6 +1005,12 @@ var Engine = class {
936
1005
  accumulator = 0;
937
1006
  disposeScheduler = null;
938
1007
  frameStats = new FrameStatsRing();
1008
+ /**
1009
+ * Game-time multiplier: 1 = realtime, 0.5 = slow motion, 0 = frozen
1010
+ * (hit-stop / pause). Scales BOTH variable and fixed updates — physics,
1011
+ * timers, behaviors all breathe together. See `gameplay` `hitStop()`.
1012
+ */
1013
+ timeScale = 1;
939
1014
  constructor(opts = {}) {
940
1015
  this.fixedStep = 1 / (opts.fixedHz ?? 60);
941
1016
  this.maxFixedSteps = opts.maxFixedStepsPerTick ?? 5;
@@ -1032,7 +1107,7 @@ var Engine = class {
1032
1107
  this.lastMs = nowMs;
1033
1108
  return;
1034
1109
  }
1035
- const dt = Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS);
1110
+ const dt = Math.min((nowMs - this.lastMs) / 1e3, MAX_DT_SECONDS) * this.timeScale;
1036
1111
  this.lastMs = nowMs;
1037
1112
  const scene = this._scene;
1038
1113
  if (!scene) return;
@@ -1228,6 +1303,12 @@ var ParticleSim = class {
1228
1303
  this.rng = rng;
1229
1304
  this.data = new Float64Array(Math.max(1, config.maxParticles) * FIELDS);
1230
1305
  }
1306
+ /** Change the emission rate LIVE (particles/sec; 0 pauses emission) — lets an
1307
+ * emitter toggle on/off at runtime (drift smoke, throttle flames). */
1308
+ setRate(rate) {
1309
+ this.config.rate = rate;
1310
+ if (rate <= 0) this.spawnAccumulator = 0;
1311
+ }
1231
1312
  get count() {
1232
1313
  return this.alive;
1233
1314
  }