incanto 0.1.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 (138) hide show
  1. package/LICENSE +30 -0
  2. package/README.md +36 -0
  3. package/THIRD-PARTY-NOTICES.md +88 -0
  4. package/assets/audio/attacked.mp3 +0 -0
  5. package/assets/audio/explosion.mp3 +0 -0
  6. package/assets/audio/gold_loot.mp3 +0 -0
  7. package/assets/audio/heal.mp3 +0 -0
  8. package/assets/audio/hit_metal_bang.mp3 +0 -0
  9. package/assets/audio/ice_spear.mp3 +0 -0
  10. package/assets/audio/monster_died.mp3 +0 -0
  11. package/assets/audio/slash.mp3 +0 -0
  12. package/assets/audio/smite.mp3 +0 -0
  13. package/assets/audio/spells_cast.mp3 +0 -0
  14. package/assets/audio/ui_click.wav +0 -0
  15. package/assets/audio/walk.mp3 +0 -0
  16. package/assets/catalog.json +390 -0
  17. package/assets/characters/2dbasic.json +41 -0
  18. package/assets/characters/2dbasic.png +0 -0
  19. package/assets/characters/ghost.json +46 -0
  20. package/assets/characters/ghost.png +0 -0
  21. package/assets/characters/goblin.json +40 -0
  22. package/assets/characters/goblin.png +0 -0
  23. package/assets/characters/medieval-knight.json +41 -0
  24. package/assets/characters/medieval-knight.png +0 -0
  25. package/assets/effects/swoosh.png +0 -0
  26. package/assets/items/box.png +0 -0
  27. package/assets/items/buff_potion.png +0 -0
  28. package/assets/items/coin.png +0 -0
  29. package/assets/items/gem.png +0 -0
  30. package/assets/items/gold.png +0 -0
  31. package/assets/items/hp_potion.png +0 -0
  32. package/assets/items/locked_item_box.png +0 -0
  33. package/assets/items/map.png +0 -0
  34. package/assets/items/resurrection_potion.png +0 -0
  35. package/assets/items/super_box.png +0 -0
  36. package/assets/items/trap.png +0 -0
  37. package/assets/tiles/floor00.jpg +0 -0
  38. package/assets/tiles/minecraft-tiles.png +0 -0
  39. package/assets/tiles/wall00.jpg +0 -0
  40. package/assets/vegetation/ash_color.png +0 -0
  41. package/assets/vegetation/aspen_color.png +0 -0
  42. package/assets/vegetation/bark/birch_color_1k.jpg +0 -0
  43. package/assets/vegetation/bark/birch_normal_1k.jpg +0 -0
  44. package/assets/vegetation/bark/birch_roughness_1k.jpg +0 -0
  45. package/assets/vegetation/bark/oak_color_1k.jpg +0 -0
  46. package/assets/vegetation/bark/oak_normal_1k.jpg +0 -0
  47. package/assets/vegetation/bark/oak_roughness_1k.jpg +0 -0
  48. package/assets/vegetation/bark/pine_color_1k.jpg +0 -0
  49. package/assets/vegetation/bark/pine_normal_1k.jpg +0 -0
  50. package/assets/vegetation/bark/pine_roughness_1k.jpg +0 -0
  51. package/assets/vegetation/ground/dirt_color.jpg +0 -0
  52. package/assets/vegetation/ground/dirt_normal.jpg +0 -0
  53. package/assets/vegetation/ground/grass.jpg +0 -0
  54. package/assets/vegetation/oak_color.png +0 -0
  55. package/assets/vegetation/pine_color.png +0 -0
  56. package/bin/incanto-assets.mjs +107 -0
  57. package/bin/incanto-check.mjs +107 -0
  58. package/bin/incanto-editor.mjs +343 -0
  59. package/bin/incanto-env.mjs +144 -0
  60. package/bin/incanto-model.mjs +296 -0
  61. package/bin/incanto-play.mjs +219 -0
  62. package/bin/incanto-skills.mjs +71 -0
  63. package/dist/2d.d.ts +642 -0
  64. package/dist/2d.js +44 -0
  65. package/dist/3d.d.ts +1860 -0
  66. package/dist/3d.js +5 -0
  67. package/dist/agent8-DzU2fFyH.js +129 -0
  68. package/dist/audio-player-DqUR3XFs.d.ts +110 -0
  69. package/dist/behavior-BAQq7HGM.d.ts +851 -0
  70. package/dist/create-game-BdjpTHrW.js +1725 -0
  71. package/dist/create-game-CZHROKcT.js +527 -0
  72. package/dist/debug-draw-CZmOYjL2.js +13 -0
  73. package/dist/debug.d.ts +66 -0
  74. package/dist/debug.js +658 -0
  75. package/dist/duplicate-DP2WPYom.js +22 -0
  76. package/dist/env.d.ts +430 -0
  77. package/dist/env.js +3152 -0
  78. package/dist/errors-BMFaY68Q.d.ts +33 -0
  79. package/dist/errors-BpWbnbb_.js +13 -0
  80. package/dist/gameplay-Ccruc3Wd.js +1501 -0
  81. package/dist/gameplay.d.ts +543 -0
  82. package/dist/gameplay.js +2 -0
  83. package/dist/heightmap-CroQPEER.js +185 -0
  84. package/dist/index.d.ts +305 -0
  85. package/dist/index.js +62 -0
  86. package/dist/json-BLk7H2Qa.js +30 -0
  87. package/dist/loader-CGs_G-r0.js +919 -0
  88. package/dist/loader-Mo0KghCv.d.ts +41 -0
  89. package/dist/net.d.ts +427 -0
  90. package/dist/net.js +772 -0
  91. package/dist/noise-CGUMx44x.js +82 -0
  92. package/dist/particle-sim-CbN4YUuH.d.ts +63 -0
  93. package/dist/particle-sim-DYuSUxvK.js +1319 -0
  94. package/dist/physics-2d-KuMWPTf6.js +288 -0
  95. package/dist/physics-3d-Dl67vOLT.js +434 -0
  96. package/dist/react.d.ts +65 -0
  97. package/dist/react.js +209 -0
  98. package/dist/register-BuUV1_KB.js +561 -0
  99. package/dist/register-CNlYAS1_.js +10634 -0
  100. package/dist/register-DPEV9_9t.js +851 -0
  101. package/dist/register-Dasmnurl.js +374 -0
  102. package/dist/registry-BVJ2HbCn.js +132 -0
  103. package/dist/rng-DP-SR7eg.js +38 -0
  104. package/dist/rolldown-runtime-D7D4PA-g.js +13 -0
  105. package/dist/schema-CcoWb32N.d.ts +104 -0
  106. package/dist/test.d.ts +158 -0
  107. package/dist/test.js +275 -0
  108. package/dist/touch-031PxtCR.js +208 -0
  109. package/dist/vite.d.ts +26 -0
  110. package/dist/vite.js +57 -0
  111. package/editor/assets/GameServer-C56iOUgF.js +1 -0
  112. package/editor/assets/agent8-Bp7QFI7v.js +1 -0
  113. package/editor/assets/index-DF3tMeKJ.css +1 -0
  114. package/editor/assets/index-Dl2pjA8e.js +7365 -0
  115. package/editor/assets/rapier-CEuLKeCu.js +1 -0
  116. package/editor/assets/rapier-DE6a0vmv.js +1 -0
  117. package/editor/index.html +169 -0
  118. package/package.json +97 -0
  119. package/schemas/scene.schema.json +4254 -0
  120. package/skills/README.md +9 -0
  121. package/skills/incanto-3d-character.md +229 -0
  122. package/skills/incanto-3d-models.md +151 -0
  123. package/skills/incanto-assets.md +118 -0
  124. package/skills/incanto-audio.md +309 -0
  125. package/skills/incanto-behaviors-and-scripts.md +169 -0
  126. package/skills/incanto-building-2d-games.md +242 -0
  127. package/skills/incanto-building-3d-games.md +245 -0
  128. package/skills/incanto-editor.md +163 -0
  129. package/skills/incanto-environment.md +743 -0
  130. package/skills/incanto-gameplay-behaviors.md +707 -0
  131. package/skills/incanto-multiplayer.md +264 -0
  132. package/skills/incanto-node-reference.md +797 -0
  133. package/skills/incanto-physics-and-input.md +164 -0
  134. package/skills/incanto-scene-json-authoring.md +325 -0
  135. package/skills/incanto-verifying-your-game.md +191 -0
  136. package/skills/incanto-web-integration.md +96 -0
  137. package/templates/agent8-server.js +84 -0
  138. package/templates/agent8-server.ts +138 -0
@@ -0,0 +1,919 @@
1
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
2
+ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
3
+ import { c as mergeStaticSignals, i as getNodeSchema, r as createNode, s as mergeStaticProps, t as applySchemaProps } from "./registry-BVJ2HbCn.js";
4
+ //#region src/core/signal.ts
5
+ var Signal = class {
6
+ connections = [];
7
+ /** Connect a listener. Returns a disposer. Connecting the same fn twice is a no-op. */
8
+ connect(fn, opts) {
9
+ if (!this.connections.some((c) => c.fn === fn)) this.connections.push({
10
+ fn,
11
+ once: opts?.once ?? false
12
+ });
13
+ return () => this.disconnect(fn);
14
+ }
15
+ disconnect(fn) {
16
+ const i = this.connections.findIndex((c) => c.fn === fn);
17
+ if (i !== -1) this.connections.splice(i, 1);
18
+ }
19
+ disconnectAll() {
20
+ this.connections.length = 0;
21
+ }
22
+ emit(...args) {
23
+ const snapshot = [...this.connections];
24
+ for (const conn of snapshot) {
25
+ if (!this.connections.includes(conn)) continue;
26
+ if (conn.once) this.disconnect(conn.fn);
27
+ conn.fn(...args);
28
+ }
29
+ }
30
+ get connectionCount() {
31
+ return this.connections.length;
32
+ }
33
+ };
34
+ //#endregion
35
+ //#region src/core/behavior.ts
36
+ /**
37
+ * Vibe-coded logic attached to a JSON-declared node — the ONE script a node
38
+ * may carry (`"script": {"name": "PlayerController", "props": {...}}`).
39
+ *
40
+ * Lifecycle hooks run AFTER the node's own. Props follow the same
41
+ * schema/defaults/delta model as node props.
42
+ */
43
+ var Behavior = class {
44
+ static props;
45
+ /**
46
+ * Custom signals this behavior emits via `this.emit(...)` — declared onto
47
+ * its node at load. Undeclared emits are hard errors (typo safety).
48
+ */
49
+ static signals;
50
+ /** The node this behavior is attached to (assigned by the loader). */
51
+ node;
52
+ /** The running engine, reached through the scene tree. */
53
+ get engine() {
54
+ const engine = this.node?.tree?.engine;
55
+ if (!engine) throw new IncantoError("TREE_VIOLATION", `Behavior on '${this.node?.name ?? "?"}' is not attached to a running engine (node must be in a scene set on an Engine).`);
56
+ return engine;
57
+ }
58
+ /** Shortcut for `this.engine.input`. */
59
+ get input() {
60
+ return this.engine.input;
61
+ }
62
+ /** Seeded engine randomness — use this, not Math.random(), for replayability. */
63
+ get rng() {
64
+ return this.engine.rng;
65
+ }
66
+ /** The engine log channel (visible in the debug overlay and test harness). */
67
+ get log() {
68
+ return this.engine.log;
69
+ }
70
+ getNode(path) {
71
+ return this.node.getNode(path);
72
+ }
73
+ emit(signal, ...args) {
74
+ this.node.emit(signal, ...args);
75
+ }
76
+ on(signal, fn, opts) {
77
+ return this.node.on(signal, fn, opts);
78
+ }
79
+ };
80
+ /**
81
+ * @internal No-op stand-in attached when a scene's script names an
82
+ * unregistered behavior under `stubMissingBehaviors` (validation/tool mode):
83
+ * props skip validation, connection handlers resolve to nothing, emits are
84
+ * harmless. Games never see this — the strict default still hard-fails.
85
+ */
86
+ var StubBehavior = class extends Behavior {};
87
+ const behaviors = /* @__PURE__ */ new Map();
88
+ /** Whether a behavior name is registered (no throw). */
89
+ function hasBehavior(name) {
90
+ return behaviors.has(name);
91
+ }
92
+ /** Explicit registration — never an import side effect (tree-shaking safety). */
93
+ function registerBehavior(name, ctor, opts) {
94
+ const existing = behaviors.get(name);
95
+ if (existing && existing !== ctor && !opts?.replace) throw new IncantoError("DUPLICATE_BEHAVIOR", `Behavior '${name}' is already registered by a different class. Replacing an implementation (e.g. under hot reload) needs registerBehavior(name, ctor, { replace: true }).`);
96
+ behaviors.set(name, ctor);
97
+ }
98
+ function getBehavior(name) {
99
+ const ctor = behaviors.get(name);
100
+ if (!ctor) {
101
+ const registered = [...behaviors.keys()];
102
+ throw new IncantoError("UNKNOWN_BEHAVIOR", `Unknown behavior '${name}'. Registered behaviors: [${registered.join(", ")}]. Did you forget registerBehavior('${name}', ${name})?`, { validOptions: registered });
103
+ }
104
+ return ctor;
105
+ }
106
+ function registeredBehaviors() {
107
+ return [...behaviors.keys()];
108
+ }
109
+ /** Test isolation helper. */
110
+ function clearBehaviors() {
111
+ behaviors.clear();
112
+ }
113
+ /** @internal Instantiate + validate props (used by the scene loader). */
114
+ function createBehavior(name, props) {
115
+ const ctor = getBehavior(name);
116
+ const behavior = new ctor();
117
+ applySchemaProps(behavior, mergeStaticProps(ctor), props, `script:${name}`);
118
+ return behavior;
119
+ }
120
+ //#endregion
121
+ //#region src/core/node-path.ts
122
+ function parseNodePath(path) {
123
+ if (path === "") throw new IncantoError("BAD_NODE_PATH", "Node path must not be empty.");
124
+ if (path.startsWith("%")) {
125
+ const name = path.slice(1);
126
+ if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid unique-name path '${path}'. Expected '%NodeName'.`);
127
+ return {
128
+ kind: "unique",
129
+ name
130
+ };
131
+ }
132
+ if (path === ".") return {
133
+ kind: "relative",
134
+ segments: []
135
+ };
136
+ const absolute = path.startsWith("/");
137
+ const body = absolute ? path.slice(1) : path;
138
+ if (body === "" || body.split("/").some((s) => s === "")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Segments must be non-empty (no leading/trailing/double '/').`);
139
+ const segments = body.split("/");
140
+ for (const seg of segments) {
141
+ if (seg.includes("%")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '%' is only allowed as a whole-path prefix ('%Name').`);
142
+ if (seg === ".") throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. '.' is only valid as the whole path — drop the './' prefix.`);
143
+ }
144
+ if (absolute && segments.includes("..")) throw new IncantoError("BAD_NODE_PATH", `Invalid node path '${path}'. Absolute paths must not contain '..'.`);
145
+ return {
146
+ kind: absolute ? "absolute" : "relative",
147
+ segments
148
+ };
149
+ }
150
+ //#endregion
151
+ //#region src/core/node.ts
152
+ function validateName(name) {
153
+ if (name === "" || name.includes("/") || name.includes("%")) throw new IncantoError("TREE_VIOLATION", `Invalid node name '${name}'. Names must be non-empty and must not contain '/' or '%'.`);
154
+ }
155
+ /**
156
+ * Base class of everything in an Incanto scene (Godot's Node model).
157
+ *
158
+ * Pure data + tree structure: no rendering, no DOM, no three.js — renderer
159
+ * adapters subscribe from the outside.
160
+ */
161
+ var Node = class {
162
+ static typeName = "Node";
163
+ _name;
164
+ _parent = null;
165
+ _children = [];
166
+ _groups = /* @__PURE__ */ new Set();
167
+ _signals = /* @__PURE__ */ new Map();
168
+ _declared = null;
169
+ _tree = null;
170
+ _ready = false;
171
+ /**
172
+ * Optional STABLE identifier (scene JSON `uid`) — unlike names (unique only
173
+ * among siblings) a uid is unique across the whole scene, so scripts and
174
+ * tools can address a node no matter where it moves.
175
+ */
176
+ uid = null;
177
+ /** Free-form JSON identity for game logic (e.g. `{kind: 'ITEM', value: 10}`). */
178
+ tags = {};
179
+ /** Behavior attachment blob from scene JSON (resolved by the loader). */
180
+ script = null;
181
+ /** The resolved behavior instance (set by the loader from `script`). */
182
+ behavior = null;
183
+ /** Replication config blob from scene JSON (interpreted in M6; preserved until then). */
184
+ network = null;
185
+ constructor(name) {
186
+ const ctor = this.constructor;
187
+ this._name = name ?? ctor.typeName;
188
+ validateName(this._name);
189
+ }
190
+ get name() {
191
+ return this._name;
192
+ }
193
+ set name(value) {
194
+ validateName(value);
195
+ if (this._parent) this._name = uniqueSiblingName(value, this._parent._children, this);
196
+ else this._name = value;
197
+ }
198
+ get parent() {
199
+ return this._parent;
200
+ }
201
+ get children() {
202
+ return this._children;
203
+ }
204
+ get groups() {
205
+ return this._groups;
206
+ }
207
+ /** The SceneTree this node is attached to, or null while detached. */
208
+ get tree() {
209
+ return this._tree;
210
+ }
211
+ /** Whether onReady has run (it runs at most once per instance). */
212
+ get isReady() {
213
+ return this._ready;
214
+ }
215
+ addChild(child) {
216
+ if (child._parent !== null) throw new IncantoError("TREE_VIOLATION", `Cannot add '${child.name}': it already has parent '${child._parent.name}'. removeChild/reparent first.`);
217
+ for (let n = this; n; n = n._parent) if (n === child) throw new IncantoError("TREE_VIOLATION", `Cannot add '${child.name}' to '${this.name}': it is this node or an ancestor (cycle).`);
218
+ child._name = uniqueSiblingName(child._name, this._children, child);
219
+ child._parent = this;
220
+ this._children.push(child);
221
+ if (this._tree) {
222
+ child._propagateEnterTree(this._tree);
223
+ child._propagateReady();
224
+ }
225
+ return child;
226
+ }
227
+ removeChild(child) {
228
+ const i = this._children.indexOf(child);
229
+ if (i === -1) throw new IncantoError("TREE_VIOLATION", `'${child.name}' is not a child of '${this.name}'.`);
230
+ if (child._tree) child._propagateExitTree();
231
+ this._children.splice(i, 1);
232
+ child._parent = null;
233
+ }
234
+ reparent(newParent) {
235
+ this._parent?.removeChild(this);
236
+ newParent.addChild(this);
237
+ }
238
+ findChild(name, recursive = true) {
239
+ for (const c of this._children) if (c._name === name) return c;
240
+ if (recursive) for (const c of this._children) {
241
+ const found = c.findChild(name, true);
242
+ if (found) return found;
243
+ }
244
+ return null;
245
+ }
246
+ /** Topmost ancestor (the node itself when detached). */
247
+ getRoot() {
248
+ let n = this;
249
+ while (n._parent) n = n._parent;
250
+ return n;
251
+ }
252
+ /** Depth-first search of THIS subtree for the node carrying `uid`. */
253
+ getNodeByUid(uid) {
254
+ if (this.uid === uid) return this;
255
+ for (const child of this.children) {
256
+ const found = child.getNodeByUid(uid);
257
+ if (found) return found;
258
+ }
259
+ return null;
260
+ }
261
+ /** Every node in THIS subtree named `name` — names repeat, so a list. */
262
+ getNodesByName(name) {
263
+ const out = [];
264
+ const walk = (node) => {
265
+ if (node.name === name) out.push(node);
266
+ for (const child of node.children) walk(child);
267
+ };
268
+ walk(this);
269
+ return out;
270
+ }
271
+ getPath() {
272
+ const parts = [];
273
+ for (let n = this; n; n = n._parent) parts.unshift(n._name);
274
+ return `/${parts.join("/")}`;
275
+ }
276
+ getNode(path) {
277
+ const found = this.resolve(path);
278
+ if (found) return found;
279
+ throw new IncantoError("NODE_NOT_FOUND", `No node at '${path}' from '${this.getPath()}'. Children here: [${this._children.map((c) => c._name).join(", ")}].`);
280
+ }
281
+ getNodeOrNull(path) {
282
+ return this.resolve(path);
283
+ }
284
+ resolve(path) {
285
+ const parsed = parseNodePath(path);
286
+ if (parsed.kind === "unique") {
287
+ const matches = [];
288
+ collectByName(this.getRoot(), parsed.name, matches);
289
+ 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(", ")}).`);
290
+ return matches[0] ?? null;
291
+ }
292
+ let current;
293
+ let segments = parsed.segments;
294
+ if (parsed.kind === "absolute") {
295
+ const root = this.getRoot();
296
+ if (segments[0] !== root._name && segments[0] !== "root") return null;
297
+ current = root;
298
+ segments = segments.slice(1);
299
+ } else current = this;
300
+ for (const seg of segments) {
301
+ if (!current) return null;
302
+ if (seg === "..") {
303
+ current = current._parent;
304
+ continue;
305
+ }
306
+ current = current._children.find((c) => c._name === seg) ?? null;
307
+ }
308
+ return current;
309
+ }
310
+ addToGroup(group) {
311
+ this._groups.add(group);
312
+ }
313
+ removeFromGroup(group) {
314
+ this._groups.delete(group);
315
+ }
316
+ isInGroup(group) {
317
+ return this._groups.has(group);
318
+ }
319
+ declaredSignals() {
320
+ if (!this._declared) {
321
+ this._declared = /* @__PURE__ */ new Set();
322
+ let ctor = this.constructor;
323
+ while (typeof ctor === "function") {
324
+ const own = ctor;
325
+ if (Object.hasOwn(ctor, "signals") && own.signals) for (const s of own.signals) this._declared.add(s);
326
+ ctor = Object.getPrototypeOf(ctor);
327
+ }
328
+ }
329
+ return this._declared;
330
+ }
331
+ /** Declare an ad-hoc signal on THIS instance (static `signals` covers types). */
332
+ declareSignal(name) {
333
+ this.declaredSignals().add(name);
334
+ }
335
+ /** Every signal this instance may emit (static + behavior + ad-hoc). */
336
+ declaredSignalNames() {
337
+ return [...this.declaredSignals()];
338
+ }
339
+ assertDeclared(name) {
340
+ if (this.declaredSignals().has(name)) return;
341
+ const ctor = this.constructor;
342
+ const declared = [...this.declaredSignals()];
343
+ throw new IncantoError("UNKNOWN_SIGNAL", `Unknown signal '${name}' on '${this.getPath()}' (${ctor.typeName}). Declared signals: [${declared.join(", ")}]. Declare it with "static signals = ['${name}']" on the node class or its behavior, or call node.declareSignal('${name}').`, {
344
+ signal: name,
345
+ path: this.getPath(),
346
+ nodeType: ctor.typeName,
347
+ validOptions: declared
348
+ });
349
+ }
350
+ /** Get the named DECLARED signal (creating its Signal object on demand). */
351
+ signal(name) {
352
+ this.assertDeclared(name);
353
+ let sig = this._signals.get(name);
354
+ if (!sig) {
355
+ sig = new Signal();
356
+ this._signals.set(name, sig);
357
+ }
358
+ return sig;
359
+ }
360
+ on(signal, fn, opts) {
361
+ return this.signal(signal).connect(fn, opts);
362
+ }
363
+ off(signal, fn) {
364
+ this._signals.get(signal)?.disconnect(fn);
365
+ }
366
+ emit(signal, ...args) {
367
+ this.assertDeclared(signal);
368
+ this._signals.get(signal)?.emit(...args);
369
+ }
370
+ /**
371
+ * Defer destruction to the end of the current update pass (flushed by the
372
+ * SceneTree). Frees immediately when detached from any tree.
373
+ */
374
+ queueFree() {
375
+ if (this._tree) this._tree._queueFree(this);
376
+ else this.free();
377
+ }
378
+ /** Immediately detach and tear down this node and its children. */
379
+ free() {
380
+ const tree = this._tree;
381
+ for (const child of [...this._children]) child.free();
382
+ this._parent?.removeChild(this);
383
+ if (this._tree) this._propagateExitTree();
384
+ tree?._detachRoot(this);
385
+ for (const sig of this._signals.values()) sig.disconnectAll();
386
+ this._signals.clear();
387
+ }
388
+ /** @internal */
389
+ _propagateEnterTree(tree) {
390
+ this._tree = tree;
391
+ this.onEnterTree();
392
+ this.behavior?.onEnterTree?.();
393
+ for (const c of [...this._children]) if (c._tree !== tree) c._propagateEnterTree(tree);
394
+ }
395
+ /** @internal */
396
+ _propagateReady() {
397
+ for (const c of [...this._children]) if (c._tree) c._propagateReady();
398
+ if (!this._ready) {
399
+ this._ready = true;
400
+ this.onReady();
401
+ this.behavior?.onReady?.();
402
+ }
403
+ }
404
+ /** @internal */
405
+ _propagateExitTree() {
406
+ for (const c of [...this._children]) c._propagateExitTree();
407
+ this.onExitTree();
408
+ this.behavior?.onExitTree?.();
409
+ this._tree = null;
410
+ }
411
+ /** @internal */
412
+ _propagateUpdate(dt) {
413
+ this.update(dt);
414
+ this.behavior?.update?.(dt);
415
+ const kids = this._children;
416
+ for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateUpdate(dt);
417
+ }
418
+ /** @internal */
419
+ _propagateFixedUpdate(dt) {
420
+ this.fixedUpdate(dt);
421
+ this.behavior?.fixedUpdate?.(dt);
422
+ const kids = this._children;
423
+ for (let i = 0, n = kids.length; i < n; i++) kids[i]?._propagateFixedUpdate(dt);
424
+ }
425
+ onEnterTree() {}
426
+ onReady() {}
427
+ onExitTree() {}
428
+ update(_dt) {}
429
+ fixedUpdate(_dt) {}
430
+ };
431
+ function collectByName(node, name, out) {
432
+ if (node.name === name) out.push(node);
433
+ for (const c of node.children) collectByName(c, name, out);
434
+ }
435
+ /**
436
+ * Deterministic sibling auto-rename: keep the requested name when free,
437
+ * otherwise increment its trailing number ('Enemy' → 'Enemy2', 'Enemy2' → 'Enemy3').
438
+ */
439
+ function uniqueSiblingName(requested, siblings, self) {
440
+ const taken = new Set(siblings.filter((s) => s !== self).map((s) => s.name));
441
+ if (!taken.has(requested)) return requested;
442
+ const m = requested.match(/^(.*?)(\d+)$/);
443
+ const stem = m ? m[1] : requested;
444
+ let counter = m ? Number(m[2]) + 1 : 2;
445
+ while (taken.has(`${stem}${counter}`)) counter += 1;
446
+ return `${stem}${counter}`;
447
+ }
448
+ //#endregion
449
+ //#region src/core/scene/constants.ts
450
+ /**
451
+ * Named scene CONSTANTS — a scene-level table of reusable typed values
452
+ * (`scene.constants`) that node props may reference instead of hard-coding a
453
+ * literal. A prop value of `{"@const": "NAME"}` is replaced, AT LOAD, with the
454
+ * constant's literal value before the node is built — so nodes never see a
455
+ * reference, only the resolved value (mirrors how assets stay simple, but
456
+ * resolved eagerly because a prop's value must match its declared type).
457
+ *
458
+ * Authoring tools keep the `{"@const": …}` reference in their own JSON; only the
459
+ * runtime resolves it. Constant values are FINAL literals — a constant must not
460
+ * reference another constant (keep the table flat).
461
+ *
462
+ * Pure + three-free; lives in core so the loader and any tool can share it.
463
+ */
464
+ /** The single reserved key marking a prop value as a constant reference. */
465
+ const CONST_REF_KEY = "@const";
466
+ /** A `{"@const": "NAME"}` reference — exactly that one string key, nothing else. */
467
+ function isConstRef(value) {
468
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
469
+ const keys = Object.keys(value);
470
+ return keys.length === 1 && keys[0] === "@const" && typeof value["@const"] === "string";
471
+ }
472
+ /**
473
+ * Deep-resolve every `{"@const": "NAME"}` reference in `value` to its literal
474
+ * from `constants` (cloned, so the table is never aliased into node state).
475
+ * Unknown names hard-fail — agents self-correct on hard errors.
476
+ */
477
+ function resolveConstants(value, constants) {
478
+ if (value === void 0 || value === null) return value;
479
+ if (isConstRef(value)) {
480
+ const name = value[CONST_REF_KEY];
481
+ if (!constants || !(name in constants)) {
482
+ const declared = constants ? Object.keys(constants) : [];
483
+ throw new IncantoError("UNKNOWN_CONSTANT", `Unknown constant '@const: ${name}'. Declared constants: ${declared.length ? declared.join(", ") : "(none — add a \"constants\" section)"}.`);
484
+ }
485
+ return jsonClone(constants[name]);
486
+ }
487
+ if (Array.isArray(value)) return value.map((item) => resolveConstants(item, constants));
488
+ if (typeof value === "object") {
489
+ const out = {};
490
+ for (const [k, v] of Object.entries(value)) out[k] = resolveConstants(v, constants);
491
+ return out;
492
+ }
493
+ return value;
494
+ }
495
+ //#endregion
496
+ //#region src/core/scene-tree.ts
497
+ /**
498
+ * Owns a node tree and drives its lifecycle:
499
+ *
500
+ * - `setRoot` → onEnterTree parent-first, then onReady children-first (once per instance)
501
+ * - `update`/`fixedUpdate` → parent-first traversal, then flush of queued frees
502
+ * - group queries across attached nodes
503
+ *
504
+ * Headless by design — tests step it manually; the render loop (M2) calls it.
505
+ */
506
+ var SceneTree = class {
507
+ _root = null;
508
+ _engine = null;
509
+ freeQueue = /* @__PURE__ */ new Set();
510
+ _frameId = 0;
511
+ /**
512
+ * Monotonic update-frame counter — bumped once per `update()`. Per-frame
513
+ * caches (e.g. the 3d grass-bender body gather) key off this so they rebuild
514
+ * at most once a frame regardless of node visitation order.
515
+ */
516
+ get frameId() {
517
+ return this._frameId;
518
+ }
519
+ /**
520
+ * @internal Opaque per-frame scratch the 3d layer parks its shared moving-body
521
+ * snapshot on (kept here so it is per-tree-instance, never a module global —
522
+ * core stays three-free, the 3d resolver owns the shape).
523
+ */
524
+ _frameScratch = null;
525
+ get root() {
526
+ return this._root;
527
+ }
528
+ /** The engine driving this tree (set by Engine.setScene), or null. */
529
+ get engine() {
530
+ return this._engine;
531
+ }
532
+ /** @internal */
533
+ _setEngine(engine) {
534
+ this._engine = engine;
535
+ }
536
+ setRoot(node) {
537
+ if (this._root) throw new IncantoError("TREE_VIOLATION", "SceneTree already has a root.");
538
+ if (node.parent) throw new IncantoError("TREE_VIOLATION", `Root must not have a parent ('${node.name}' is a child of '${node.parent.name}').`);
539
+ this._root = node;
540
+ node._propagateEnterTree(this);
541
+ node._propagateReady();
542
+ }
543
+ update(dt) {
544
+ this._frameId++;
545
+ this._root?._propagateUpdate(dt);
546
+ this.flushFreeQueue();
547
+ }
548
+ fixedUpdate(dt) {
549
+ this._root?._propagateFixedUpdate(dt);
550
+ this.flushFreeQueue();
551
+ }
552
+ getNodesInGroup(group) {
553
+ const out = [];
554
+ if (this._root) collectGroup(this._root, group, out);
555
+ return out;
556
+ }
557
+ /** Call `method(...args)` on every group member that implements it. */
558
+ callGroup(group, method, ...args) {
559
+ for (const node of this.getNodesInGroup(group)) {
560
+ const fn = node[method];
561
+ if (typeof fn === "function") fn.apply(node, args);
562
+ }
563
+ }
564
+ /** @internal */
565
+ _queueFree(node) {
566
+ this.freeQueue.add(node);
567
+ }
568
+ /** @internal Called by Node.free(): a freed root must not be re-drivable. */
569
+ _detachRoot(node) {
570
+ if (this._root === node) this._root = null;
571
+ }
572
+ flushFreeQueue() {
573
+ if (this.freeQueue.size === 0) return;
574
+ const items = [...this.freeQueue];
575
+ this.freeQueue.clear();
576
+ for (const node of items) node.free();
577
+ }
578
+ };
579
+ function collectGroup(node, group, out) {
580
+ if (node.isInGroup(group)) out.push(node);
581
+ for (const c of node.children) collectGroup(c, group, out);
582
+ }
583
+ //#endregion
584
+ //#region src/core/viewport.ts
585
+ const FITS = [
586
+ "expand",
587
+ "letterbox",
588
+ "integer"
589
+ ];
590
+ /**
591
+ * Parse + hard-validate a scene's `viewport` header. With a design resolution,
592
+ * scene JSON owns layout again: the game is authored in fixed design pixels
593
+ * and the renderer maps them onto whatever canvas size the page provides.
594
+ */
595
+ function resolveViewport(viewport) {
596
+ if (viewport === void 0) return null;
597
+ const design = viewport.design;
598
+ if (!Array.isArray(design) || design.length !== 2 || typeof design[0] !== "number" || typeof design[1] !== "number" || design[0] <= 0 || design[1] <= 0) throw new IncantoError("BAD_FORMAT", `Scene "viewport.design" must be [width, height] with positive numbers, got ${JSON.stringify(design)}.`);
599
+ const fit = viewport.fit ?? "expand";
600
+ if (typeof fit !== "string" || !FITS.includes(fit)) throw new IncantoError("BAD_FORMAT", `Scene "viewport.fit" must be one of [${FITS.join(", ")}], got ${JSON.stringify(fit)}.`);
601
+ return {
602
+ design: [design[0], design[1]],
603
+ fit
604
+ };
605
+ }
606
+ /**
607
+ * Pure viewport math (headless-testable):
608
+ * - expand: the design rect is always fully visible; extra world shows beyond it
609
+ * - letterbox: EXACTLY the design rect, centered, bars elsewhere
610
+ * - integer: expand with whole-number scaling (pixel art), floored, min 1
611
+ */
612
+ function computeViewport(canvasW, canvasH, viewport) {
613
+ const dw = viewport.design[0];
614
+ const dh = viewport.design[1];
615
+ const raw = Math.min(canvasW / dw, canvasH / dh);
616
+ const scale = viewport.fit === "integer" ? Math.max(1, Math.floor(raw)) : raw;
617
+ if (viewport.fit === "letterbox") {
618
+ const contentW = dw * scale;
619
+ const contentH = dh * scale;
620
+ return {
621
+ scale,
622
+ viewW: dw,
623
+ viewH: dh,
624
+ offsetX: (canvasW - contentW) / 2,
625
+ offsetY: (canvasH - contentH) / 2,
626
+ contentW,
627
+ contentH
628
+ };
629
+ }
630
+ return {
631
+ scale,
632
+ viewW: canvasW / scale,
633
+ viewH: canvasH / scale,
634
+ offsetX: 0,
635
+ offsetY: 0,
636
+ contentW: canvasW,
637
+ contentH: canvasH
638
+ };
639
+ }
640
+ //#endregion
641
+ //#region src/core/scene/schema.ts
642
+ /** Current scene file format version. Defaults are frozen per format version. */
643
+ const SCENE_FORMAT = 1;
644
+ //#endregion
645
+ //#region src/core/scene/serializer.ts
646
+ /**
647
+ * Serialize a node subtree to scene JSON with delta-only props: values equal
648
+ * to the type's schema defaults are omitted (Godot PackedScene behavior —
649
+ * files stay tiny and AI-readable).
650
+ */
651
+ function serializeNode(node) {
652
+ const ctor = node.constructor;
653
+ const json = {
654
+ name: node.name,
655
+ type: ctor.typeName
656
+ };
657
+ if (node.uid !== null) json.uid = node.uid;
658
+ if (node.groups.size > 0) json.groups = [...node.groups];
659
+ if (Object.keys(node.tags).length > 0) json.tags = jsonClone(node.tags);
660
+ const schema = getNodeSchema(ctor.typeName);
661
+ const props = {};
662
+ for (const [key, def] of Object.entries(schema)) {
663
+ const value = node[key];
664
+ if (value === void 0) continue;
665
+ assertJsonSafe(value, `${node.name}.${key}`);
666
+ if (!jsonEquals(value, def.default)) props[key] = jsonClone(value);
667
+ }
668
+ if (Object.keys(props).length > 0) json.props = props;
669
+ if (node.script) json.script = jsonClone(node.script);
670
+ if (node.network) json.network = jsonClone(node.network);
671
+ if (node.children.length > 0) json.children = node.children.map((c) => serializeNode(c));
672
+ return json;
673
+ }
674
+ /**
675
+ * NaN/Infinity silently become `null` in JSON.stringify — corrupting saved
676
+ * scenes. Fail loudly at serialize time instead.
677
+ */
678
+ function assertJsonSafe(value, at) {
679
+ if (typeof value === "number" && !Number.isFinite(value)) throw new IncantoError("PROP_TYPE_MISMATCH", `Prop '${at}' is ${value} — scene props must be finite numbers (NaN/Infinity become null in JSON).`);
680
+ if (Array.isArray(value)) for (let i = 0; i < value.length; i++) assertJsonSafe(value[i], `${at}[${i}]`);
681
+ else if (value !== null && typeof value === "object") for (const [k, v] of Object.entries(value)) assertJsonSafe(v, `${at}.${k}`);
682
+ }
683
+ //#endregion
684
+ //#region src/core/scene/scene.ts
685
+ /**
686
+ * A loaded, live scene: the node tree plus everything needed to round-trip
687
+ * back to scene JSON (declared connections, asset/input/multiplayer blobs).
688
+ *
689
+ * JSON is the source of truth for STRUCTURE; imperative listeners added via
690
+ * `node.on(...)` are code, not data, and intentionally do not serialize.
691
+ */
692
+ var Scene = class {
693
+ name;
694
+ dimension;
695
+ root;
696
+ tree;
697
+ /** Scene-level rendering environment (read by renderer adapters). */
698
+ environment;
699
+ /** Asset declarations (consumed by renderer/asset layers, `$key` references). */
700
+ assets;
701
+ /** Named constant values (`{"@const": "NAME"}` prop references resolve to these). */
702
+ constants;
703
+ /** Input action declarations (loaded into `Engine.input` on setScene). */
704
+ input;
705
+ /** Physics config (gravity etc. — interpreted by the physics modules). */
706
+ physics;
707
+ /** Multiplayer config (room etc. — interpreted by incanto/net). */
708
+ multiplayer;
709
+ /** Design-resolution viewport (consumed by renderers via resolveViewport). */
710
+ viewport;
711
+ connections;
712
+ constructor(source, root, tree) {
713
+ this.name = source.name;
714
+ this.dimension = source.dimension;
715
+ this.root = root;
716
+ this.tree = tree;
717
+ this.assets = source.assets ? jsonClone(source.assets) : void 0;
718
+ this.constants = source.constants ? jsonClone(source.constants) : void 0;
719
+ this.input = source.input ? jsonClone(source.input) : void 0;
720
+ this.physics = source.physics ? jsonClone(source.physics) : void 0;
721
+ this.multiplayer = source.multiplayer ? jsonClone(source.multiplayer) : void 0;
722
+ this.environment = source.environment ? jsonClone(source.environment) : void 0;
723
+ this.viewport = source.viewport ? jsonClone(source.viewport) : void 0;
724
+ this.connections = source.connections ? jsonClone(source.connections) : [];
725
+ }
726
+ /** Lossless export. Note: expanded sub-scene instances serialize as full trees in M1. */
727
+ toJSON() {
728
+ return {
729
+ format: 1,
730
+ type: "scene",
731
+ name: this.name,
732
+ ...this.dimension ? { dimension: this.dimension } : {},
733
+ ...this.assets ? { assets: jsonClone(this.assets) } : {},
734
+ ...this.constants ? { constants: jsonClone(this.constants) } : {},
735
+ ...this.input ? { input: jsonClone(this.input) } : {},
736
+ ...this.physics ? { physics: jsonClone(this.physics) } : {},
737
+ ...this.multiplayer ? { multiplayer: jsonClone(this.multiplayer) } : {},
738
+ ...this.environment ? { environment: jsonClone(this.environment) } : {},
739
+ ...this.viewport ? { viewport: jsonClone(this.viewport) } : {},
740
+ root: serializeNode(this.root),
741
+ ...this.connections.length > 0 ? { connections: jsonClone(this.connections) } : {}
742
+ };
743
+ }
744
+ };
745
+ //#endregion
746
+ //#region src/core/scene/loader.ts
747
+ /**
748
+ * Build a live Scene from scene JSON.
749
+ *
750
+ * Takes `unknown` on purpose: the loader hard-validates everything at runtime,
751
+ * so consumers can pass an imported JSON module without TypeScript casts.
752
+ *
753
+ * Order: validate header → instantiate nodes top-down → attach to a SceneTree
754
+ * (enterTree/ready fire) → wire connections LAST. Every failure is a hard
755
+ * IncantoError naming valid alternatives — agents self-correct on hard errors.
756
+ */
757
+ function loadScene(json, opts) {
758
+ if (typeof json !== "object" || json === null) throw new IncantoError("BAD_FORMAT", `Scene JSON must be an object, got ${json === null ? "null" : typeof json}.`);
759
+ const sceneJson = json;
760
+ validateHeader(sceneJson);
761
+ const root = buildNode(sceneJson.root, opts, [], "", sceneJson.constants);
762
+ validateUniqueUids(root);
763
+ const tree = new SceneTree();
764
+ if (opts?.engine) tree._setEngine(opts.engine);
765
+ tree.setRoot(root);
766
+ wireConnections(root, sceneJson.connections ?? [], opts?.declareConnectionSignals);
767
+ return new Scene(sceneJson, root, tree);
768
+ }
769
+ /** @internal Rebuild a detached node subtree from node JSON (used by duplicateNode).
770
+ * The source comes from a live node's serialized props, which are already resolved
771
+ * literals — no constants table needed. */
772
+ function buildNodeJson(json, opts) {
773
+ return buildNode(json, opts, [], "", void 0);
774
+ }
775
+ function validateHeader(json) {
776
+ if (json.format !== 1) throw new IncantoError("BAD_FORMAT", `Unsupported scene format ${JSON.stringify(json.format)}; this engine reads format 1.`);
777
+ if (json.type !== "scene") throw new IncantoError("BAD_FORMAT", `Expected "type": "scene", got ${JSON.stringify(json.type)}.`);
778
+ if (typeof json.name !== "string" || json.name === "") throw new IncantoError("BAD_FORMAT", "Scene \"name\" must be a non-empty string.");
779
+ if (json.dimension !== void 0 && json.dimension !== "2d" && json.dimension !== "3d") throw new IncantoError("BAD_FORMAT", `Scene "dimension" must be "2d" or "3d", got ${JSON.stringify(json.dimension)}.`);
780
+ if (typeof json.root !== "object" || json.root === null) throw new IncantoError("BAD_FORMAT", "Scene \"root\" must be a node object.");
781
+ resolveViewport(json.viewport);
782
+ }
783
+ /**
784
+ * Re-raise a node-build error with the node's absolute scene path attached —
785
+ * "Unknown prop 'x' on 'Sprite2D'" alone cannot locate the node among forty
786
+ * Sprite2Ds; "(at '/Level/Enemies/Slime3')" can. Inner (deeper) contexts win.
787
+ */
788
+ function withNodeContext(e, path, nj) {
789
+ if (e instanceof IncantoError && !e.details.path) return new IncantoError(e.code, `${e.message} (at '${path}')`, {
790
+ ...e.details,
791
+ path,
792
+ ...typeof nj.uid === "string" && nj.uid !== "" ? { uid: nj.uid } : {},
793
+ ...e.details.nodeType === void 0 && nj.type ? { nodeType: nj.type } : {}
794
+ });
795
+ return e;
796
+ }
797
+ function buildNode(nj, opts, stack, parentPath, constants) {
798
+ const path = `${parentPath}/${nj.name}`;
799
+ try {
800
+ if (nj.instance !== void 0 && nj.type !== void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' declares both "type" and "instance" — they are mutually exclusive.`);
801
+ if (nj.instance !== void 0) return buildInstance(nj, opts, stack, path, constants);
802
+ if (nj.type === void 0) throw new IncantoError("BAD_FORMAT", `Node '${nj.name}' must declare either "type" or "instance".`);
803
+ const node = createNode(nj.type, resolveConstants(nj.props, constants));
804
+ applyCommon(node, nj, opts);
805
+ node.constructor.validateJson?.(node);
806
+ for (const childJson of nj.children ?? []) node.addChild(buildNode(childJson, opts, stack, path, constants));
807
+ return node;
808
+ } catch (e) {
809
+ throw withNodeContext(e, path, nj);
810
+ }
811
+ }
812
+ function buildInstance(nj, opts, stack, path, constants) {
813
+ const instancePath = nj.instance;
814
+ if (!opts?.resolveScene) throw new IncantoError("UNRESOLVED_INSTANCE", `Node '${nj.name}' references instance '${instancePath}' but no resolveScene option was provided.`);
815
+ if (stack.includes(instancePath)) throw new IncantoError("UNRESOLVED_INSTANCE", `Instance cycle detected: ${[...stack, instancePath].join(" → ")}.`);
816
+ const sub = opts.resolveScene(instancePath);
817
+ if (!sub) throw new IncantoError("UNRESOLVED_INSTANCE", `resolveScene returned nothing for instance '${instancePath}' (node '${nj.name}').`);
818
+ validateHeader(sub);
819
+ const overrides = resolveConstants(nj.overrides ?? {}, constants);
820
+ const node = buildNode({
821
+ ...sub.root,
822
+ name: nj.name,
823
+ props: deepMerge(sub.root.props ?? {}, overrides)
824
+ }, opts, [...stack, instancePath], parentOf(path), sub.constants);
825
+ applyCommon(node, {
826
+ ...nj,
827
+ type: void 0,
828
+ props: void 0
829
+ }, opts);
830
+ for (const childJson of nj.children ?? []) node.addChild(buildNode(childJson, opts, stack, path, constants));
831
+ wireConnections(node, sub.connections ?? [], opts?.declareConnectionSignals);
832
+ return node;
833
+ }
834
+ function parentOf(path) {
835
+ const i = path.lastIndexOf("/");
836
+ return i <= 0 ? "" : path.slice(0, i);
837
+ }
838
+ function applyCommon(node, nj, opts) {
839
+ if (typeof nj.uid === "string" && nj.uid !== "") node.uid = nj.uid;
840
+ node.name = nj.name;
841
+ for (const g of nj.groups ?? []) node.addToGroup(g);
842
+ if (nj.tags) node.tags = jsonClone(nj.tags);
843
+ if (nj.script) {
844
+ node.script = jsonClone(nj.script);
845
+ const name = nj.script.name;
846
+ if (typeof name !== "string" || name === "") throw new IncantoError("BAD_FORMAT", `Node '${nj.name}': "script" needs a non-empty "name" (a registered behavior).`);
847
+ const behavior = opts?.stubMissingBehaviors && !hasBehavior(name) ? new class extends StubBehavior {}() : createBehavior(name, nj.script.props);
848
+ behavior.node = node;
849
+ node.behavior = behavior;
850
+ for (const s of mergeStaticSignals(behavior.constructor)) node.declareSignal(s);
851
+ }
852
+ if (nj.network) node.network = jsonClone(nj.network);
853
+ }
854
+ function deepMerge(base, override) {
855
+ const out = { ...base };
856
+ for (const [key, value] of Object.entries(override)) {
857
+ const prev = out[key];
858
+ if (prev !== null && value !== null && typeof prev === "object" && typeof value === "object" && !Array.isArray(prev) && !Array.isArray(value)) out[key] = deepMerge(prev, value);
859
+ else out[key] = value;
860
+ }
861
+ return out;
862
+ }
863
+ function wireConnections(root, connections, declareSignals = false) {
864
+ for (const conn of connections) {
865
+ const fromNode = resolveEndpoint(root, conn.from, conn, "from");
866
+ if (declareSignals || fromNode.behavior instanceof StubBehavior) fromNode.declareSignal(conn.signal);
867
+ const toNode = resolveEndpoint(root, conn.to, conn, "to");
868
+ const nodeFn = toNode[conn.handler];
869
+ const behaviorFn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
870
+ if (typeof nodeFn !== "function" && typeof behaviorFn !== "function" && !(toNode.behavior instanceof StubBehavior)) throw new IncantoError("UNKNOWN_HANDLER", `Connection '${conn.signal}' → '${conn.to}' names handler '${conn.handler}', but '${toNode.name}' has no such method` + (toNode.behavior ? ` (nor does its behavior '${toNode.script?.name}')` : "") + `.`);
871
+ const listener = (...args) => {
872
+ if (conn.filter && !matchesFilter(args[0], conn.filter)) return;
873
+ if (conn.once) fromNode.off(conn.signal, listener);
874
+ const fn = toNode[conn.handler];
875
+ if (typeof fn === "function") {
876
+ fn.apply(toNode, args);
877
+ return;
878
+ }
879
+ const bfn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
880
+ if (typeof bfn === "function") bfn.apply(toNode.behavior, args);
881
+ };
882
+ try {
883
+ fromNode.on(conn.signal, listener);
884
+ } catch (e) {
885
+ if (e instanceof IncantoError && e.code === "UNKNOWN_SIGNAL") throw new IncantoError("UNKNOWN_SIGNAL", `Connection from '${conn.from}': ${e.message}`, e.details);
886
+ throw e;
887
+ }
888
+ }
889
+ }
890
+ function resolveEndpoint(root, path, conn, side) {
891
+ try {
892
+ return root.getNode(path);
893
+ } catch (e) {
894
+ if (e instanceof IncantoError) throw new IncantoError("DANGLING_CONNECTION", `Connection '${conn.signal}' has a dangling '${side}' path '${path}': ${e.message}`);
895
+ throw e;
896
+ }
897
+ }
898
+ function matchesFilter(arg, filter) {
899
+ if (!(arg instanceof Node)) return false;
900
+ if (filter.group && !arg.isInGroup(filter.group)) return false;
901
+ if (filter.tag) {
902
+ for (const [key, value] of Object.entries(filter.tag)) if (JSON.stringify(arg.tags[key]) !== JSON.stringify(value)) return false;
903
+ }
904
+ return true;
905
+ }
906
+ function validateUniqueUids(root) {
907
+ const seen = /* @__PURE__ */ new Map();
908
+ const walk = (node) => {
909
+ if (node.uid !== null) {
910
+ const other = seen.get(node.uid);
911
+ if (other) throw new IncantoError("DUPLICATE_UID", `duplicate uid '${node.uid}' on ${other.getPath()} and ${node.getPath()} — uids must be unique across the scene.`);
912
+ seen.set(node.uid, node);
913
+ }
914
+ for (const child of node.children) walk(child);
915
+ };
916
+ walk(root);
917
+ }
918
+ //#endregion
919
+ export { registerBehavior as _, SCENE_FORMAT as a, SceneTree as c, resolveConstants as d, Node as f, getBehavior as g, clearBehaviors as h, serializeNode as i, CONST_REF_KEY as l, Behavior as m, loadScene as n, computeViewport as o, parseNodePath as p, Scene as r, resolveViewport as s, buildNodeJson as t, isConstRef as u, registeredBehaviors as v, Signal as y };