incanto 0.4.3 → 0.6.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 (44) hide show
  1. package/dist/2d.d.ts +80 -6
  2. package/dist/2d.js +4 -4
  3. package/dist/3d.d.ts +200 -15
  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-B_sW_eiE.js} +59 -27
  8. package/dist/{create-game-DiTq3-fQ.js → create-game-DkbZCNaV.js} +292 -14
  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-Cgli1aju.js} +95 -10
  23. package/dist/{physics-3d-DmNCeh58.js → physics-3d-CBAQ12LY.js} +121 -43
  24. package/dist/react.d.ts +1 -1
  25. package/dist/react.js +1 -1
  26. package/dist/{register-DPEV9_9t.js → register-BJCfuuZ2.js} +79 -5
  27. package/dist/{register-B0gq63VW.js → register-C35HDZpm.js} +2 -2
  28. package/dist/{register-BRy8FNox.js → register-Dd7Juujf.js} +389 -49
  29. package/dist/{register-BuUV1_KB.js → register-Dl_ixIJe.js} +275 -2
  30. package/dist/test.d.ts +2 -2
  31. package/dist/test.js +10 -10
  32. package/editor/assets/{agent8-DryTXVd4.js → agent8-MciFwn0v.js} +1 -1
  33. package/editor/assets/index-mofVSmuA.js +7417 -0
  34. package/editor/index.html +1 -1
  35. package/package.json +1 -1
  36. package/schemas/scene.schema.json +980 -126
  37. package/skills/incanto-3d-models.md +24 -1
  38. package/skills/incanto-building-2d-games.md +6 -0
  39. package/skills/incanto-building-3d-games.md +46 -1
  40. package/skills/incanto-gameplay-behaviors.md +60 -0
  41. package/skills/incanto-hud.md +58 -0
  42. package/skills/incanto-node-reference.md +148 -0
  43. package/skills/incanto-physics-and-input.md +47 -0
  44. package/editor/assets/index-N8se-PhP.js +0 -7365
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.3";
113
+ const VERSION = "0.6.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
  }
@@ -49,6 +49,9 @@ declare class ParticleSim {
49
49
  private spawnAccumulator;
50
50
  private everSpawned;
51
51
  constructor(config: ParticleSimConfig, rng: Rng);
52
+ /** Change the emission rate LIVE (particles/sec; 0 pauses emission) — lets an
53
+ * emitter toggle on/off at runtime (drift smoke, throttle flames). */
54
+ setRate(rate: number): void;
52
55
  get count(): number;
53
56
  /** True when nothing is alive and at least one particle has ever spawned. */
54
57
  get done(): boolean;