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,527 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { _ as registerBehavior, n as loadScene, o as computeViewport, s as resolveViewport } from "./loader-CGs_G-r0.js";
3
+ import { a as Engine } from "./particle-sim-DYuSUxvK.js";
4
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
5
+ import { r as AudioPlayer } from "./register-BuUV1_KB.js";
6
+ import { i as resolveRendering, n as attachTouchControls } from "./touch-031PxtCR.js";
7
+ import { n as registerGameplayBehaviors } from "./gameplay-Ccruc3Wd.js";
8
+ import { f as PhysicsBody2D, h as Node2D, n as UILayer, s as Camera2D, t as registerNodes2D } from "./register-DPEV9_9t.js";
9
+ import { t as debugSources } from "./debug-draw-CZmOYjL2.js";
10
+ import { n as enablePhysics2D } from "./physics-2d-KuMWPTf6.js";
11
+ import { Box3, BufferAttribute, BufferGeometry, Color, LineBasicMaterial, LineSegments, LinearFilter, NearestFilter, OrthographicCamera, Raycaster, SRGBColorSpace, Scene, TextureLoader, Vector2, Vector3, WebGLRenderer } from "three";
12
+ //#region src/2d/assets.ts
13
+ /**
14
+ * String-keyed asset registry for the 2D layer (Phaser's loader model).
15
+ *
16
+ * Scene JSON declares assets under stable keys; node props reference them as
17
+ * `"$key"`. The texture loader is injectable so tests run headless.
18
+ */
19
+ var AssetStore2D = class {
20
+ entries = /* @__PURE__ */ new Map();
21
+ loader;
22
+ constructor(loader) {
23
+ this.loader = loader ?? ((url, callbacks) => new TextureLoader().load(url, () => callbacks?.onLoad?.(), void 0, (err) => callbacks?.onError?.(err)));
24
+ }
25
+ /** Load (or extend with) a scene's `assets` declarations. Hard-validates each entry. */
26
+ load(assets) {
27
+ for (const [key, decl] of Object.entries(assets)) {
28
+ const type = decl.type;
29
+ if (type !== "texture" && type !== "spritesheet") throw new IncantoError("BAD_FORMAT", `Asset '${key}': "type" must be 'texture' or 'spritesheet', got ${JSON.stringify(type)}.`);
30
+ if (typeof decl.url !== "string" || decl.url === "") throw new IncantoError("BAD_FORMAT", `Asset '${key}': "url" must be a non-empty string.`);
31
+ let frameWidth;
32
+ let frameHeight;
33
+ if (type === "spritesheet") {
34
+ if (typeof decl.frameWidth !== "number" || typeof decl.frameHeight !== "number") throw new IncantoError("BAD_FORMAT", `Asset '${key}': spritesheets need numeric "frameWidth" and "frameHeight".`);
35
+ frameWidth = decl.frameWidth;
36
+ frameHeight = decl.frameHeight;
37
+ }
38
+ const previous = this.entries.get(key);
39
+ const state = { status: "loading" };
40
+ const texture = this.loader(decl.url, {
41
+ onLoad: () => {
42
+ state.status = "ready";
43
+ },
44
+ onError: (err) => {
45
+ state.status = "error";
46
+ console.error(`[incanto] failed to load texture '${key}' (${decl.url}):`, err);
47
+ }
48
+ });
49
+ if (previous && previous.texture !== texture) previous.texture.dispose();
50
+ texture.colorSpace = SRGBColorSpace;
51
+ if (decl.filter === "nearest") {
52
+ texture.magFilter = NearestFilter;
53
+ texture.minFilter = NearestFilter;
54
+ texture.generateMipmaps = false;
55
+ } else texture.magFilter = LinearFilter;
56
+ texture.needsUpdate = true;
57
+ this.entries.set(key, {
58
+ texture,
59
+ type,
60
+ frameWidth,
61
+ frameHeight,
62
+ state
63
+ });
64
+ }
65
+ }
66
+ /** Load status of a declared asset KEY (no '$'), or undefined if unknown. */
67
+ status(key) {
68
+ return this.entries.get(key)?.state.status;
69
+ }
70
+ getTexture(ref) {
71
+ return this.resolve(ref).texture;
72
+ }
73
+ getSheet(ref) {
74
+ const entry = this.resolve(ref);
75
+ if (entry.type !== "spritesheet") throw new IncantoError("BAD_FORMAT", `Asset '${ref}' is a plain texture; a spritesheet (with frameWidth/frameHeight) is required here.`);
76
+ return {
77
+ texture: entry.texture,
78
+ frameWidth: entry.frameWidth,
79
+ frameHeight: entry.frameHeight
80
+ };
81
+ }
82
+ resolve(ref) {
83
+ if (!ref.startsWith("$")) throw new IncantoError("UNKNOWN_ASSET", `Asset reference '${ref}' must use the '$key' form (e.g. '$${ref}').`);
84
+ const entry = this.entries.get(ref.slice(1));
85
+ if (!entry) throw new IncantoError("UNKNOWN_ASSET", `Unknown asset '${ref}'. Declared assets: [${[...this.entries.keys()].join(", ")}].`);
86
+ return entry;
87
+ }
88
+ };
89
+ //#endregion
90
+ //#region src/2d/picking.ts
91
+ function screenFromWorld(view, wx, wy) {
92
+ return {
93
+ x: (wx - view.cx) * view.zoom + view.w / 2,
94
+ y: (wy - view.cy) * view.zoom + view.h / 2
95
+ };
96
+ }
97
+ function worldFromScreen(view, sx, sy) {
98
+ return {
99
+ x: (sx - view.w / 2) / view.zoom + view.cx,
100
+ y: (sy - view.h / 2) / view.zoom + view.cy
101
+ };
102
+ }
103
+ //#endregion
104
+ //#region src/2d/sync.ts
105
+ /**
106
+ * Mirror an Incanto node tree onto two three scenes:
107
+ * - `world`: camera-space drawables
108
+ * - `ui`: everything under a `UILayer` (screen space, ignores the camera);
109
+ * each layer mounts through its own group, positioned by its `anchor`
110
+ * against `uiSize` (origin when no size is known — headless).
111
+ *
112
+ * Same dirty-push contract as the 3D sync; pure scene-graph math, headless-testable.
113
+ */
114
+ function syncTree2D(root, world, ui, assets, uiSize) {
115
+ const visited = /* @__PURE__ */ new Set();
116
+ const cameras = [];
117
+ walk(root, world, ui, false, visited, cameras, assets, uiSize);
118
+ prune(world, visited);
119
+ prune(ui, visited);
120
+ return { activeCamera: cameras.find((c) => c.current) ?? cameras[0] ?? null };
121
+ }
122
+ function walk(node, parentObj, ui, inUi, visited, cameras, assets, uiSize) {
123
+ let nextParent = parentObj;
124
+ let nextInUi = inUi;
125
+ if (node instanceof UILayer) {
126
+ const group = node._ensureGroup();
127
+ if (group.parent !== ui) ui.add(group);
128
+ const origin = uiSize ? node._anchorOrigin(uiSize.width, uiSize.height) : {
129
+ x: 0,
130
+ y: 0
131
+ };
132
+ group.position.set(origin.x, -origin.y, 0);
133
+ visited.add(group);
134
+ nextInUi = true;
135
+ nextParent = group;
136
+ } else if (node instanceof Node2D) {
137
+ const obj = node._ensureObject2D();
138
+ if (obj.parent !== parentObj) parentObj.add(obj);
139
+ node._syncObject2D(assets);
140
+ visited.add(obj);
141
+ if (node instanceof Camera2D && !inUi) cameras.push(node);
142
+ nextParent = obj;
143
+ }
144
+ for (const child of node.children) walk(child, nextParent, ui, nextInUi, visited, cameras, assets, uiSize);
145
+ }
146
+ function prune(obj, visited) {
147
+ for (const child of [...obj.children]) if (child.userData.incantoNode && !visited.has(child)) obj.remove(child);
148
+ else prune(child, visited);
149
+ }
150
+ //#endregion
151
+ //#region src/2d/renderer.ts
152
+ /**
153
+ * 2D presentation layer: world pass through the active Camera2D
154
+ * (center/zoom/limits), then a screen-space UI pass for UILayer subtrees.
155
+ * 1 unit = 1 px, (0,0) top-left, +y down.
156
+ */
157
+ var Renderer2D = class {
158
+ /**
159
+ * When set, the world pass frames THIS view instead of the scene's active
160
+ * Camera2D — tooling (the scene editor) pans/zooms freely without touching
161
+ * scene data. `null` restores normal camera behavior.
162
+ */
163
+ viewOverride = null;
164
+ webgl;
165
+ worldScene = new Scene();
166
+ uiScene = new Scene();
167
+ worldCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
168
+ uiCam = new OrthographicCamera(-1, 1, 1, -1, -1e3, 1e3);
169
+ engine;
170
+ assets;
171
+ loadedAssetScenes = /* @__PURE__ */ new WeakSet();
172
+ disconnect;
173
+ canvas;
174
+ constructor(opts) {
175
+ this.canvas = opts.canvas;
176
+ this.engine = opts.engine;
177
+ this.assets = opts.assets ?? new AssetStore2D();
178
+ const rendering = resolveRendering(opts.engine.scene?.environment, {
179
+ antialias: true,
180
+ pixelRatio: 1
181
+ }, globalThis.devicePixelRatio ?? 1, opts);
182
+ this.webgl = new WebGLRenderer({
183
+ canvas: opts.canvas,
184
+ antialias: rendering.antialias
185
+ });
186
+ this.webgl.setPixelRatio(rendering.pixelRatio);
187
+ this.webgl.info.autoReset = false;
188
+ this.debugLines = new LineSegments(new BufferGeometry(), new LineBasicMaterial({ color: "#00ff6e" }));
189
+ this.debugLines.frustumCulled = false;
190
+ this.debugLines.renderOrder = 9999;
191
+ this.debugLines.visible = false;
192
+ this.worldScene.add(this.debugLines);
193
+ this.disconnect = this.engine.updated.connect(() => this.render());
194
+ }
195
+ debugLines;
196
+ syncDebugLines() {
197
+ let vertices = null;
198
+ for (const source of debugSources("2d")) {
199
+ vertices = source.debugLines();
200
+ if (vertices) break;
201
+ }
202
+ this.debugLines.visible = vertices !== null;
203
+ if (!vertices) return;
204
+ const xyz = new Float32Array(vertices.length / 2 * 3);
205
+ for (let i = 0, j = 0; i < vertices.length; i += 2, j += 3) {
206
+ xyz[j] = vertices[i];
207
+ xyz[j + 1] = vertices[i + 1];
208
+ xyz[j + 2] = 0;
209
+ }
210
+ this.debugLines.geometry.setAttribute("position", new BufferAttribute(xyz, 3));
211
+ }
212
+ render() {
213
+ const scene = this.engine.scene;
214
+ if (!scene) return;
215
+ this.webgl.info.reset();
216
+ this.syncDebugLines();
217
+ if (scene.assets && !this.loadedAssetScenes.has(scene)) {
218
+ this.assets.load(scene.assets);
219
+ this.loadedAssetScenes.add(scene);
220
+ }
221
+ const background = scene.environment?.background;
222
+ this.worldScene.background = typeof background === "string" ? new Color(background) : null;
223
+ const w = this.canvas.clientWidth || this.canvas.width;
224
+ const h = this.canvas.clientHeight || this.canvas.height;
225
+ const size = this.webgl.getSize(sizeScratch);
226
+ if (size.x !== w || size.y !== h) this.webgl.setSize(w, h, false);
227
+ const vp = this.viewOverride ? null : resolveViewport(scene.viewport);
228
+ const view = vp ? computeViewport(w, h, vp) : null;
229
+ const uiW = view ? view.viewW : w;
230
+ const uiH = view ? view.viewH : h;
231
+ const { activeCamera } = syncTree2D(scene.root, this.worldScene, this.uiScene, this.assets, {
232
+ width: uiW,
233
+ height: uiH
234
+ });
235
+ let center = vp ? {
236
+ x: vp.design[0] / 2,
237
+ y: vp.design[1] / 2
238
+ } : {
239
+ x: w / 2,
240
+ y: h / 2
241
+ };
242
+ let zoom = 1;
243
+ if (this.viewOverride) {
244
+ center = {
245
+ x: this.viewOverride.cx,
246
+ y: this.viewOverride.cy
247
+ };
248
+ zoom = this.viewOverride.zoom;
249
+ } else if (activeCamera) {
250
+ center = activeCamera.clampedCenter(uiW, uiH);
251
+ zoom = activeCamera.effectiveZoom;
252
+ }
253
+ const scale = view?.scale ?? 1;
254
+ this.lastView = {
255
+ cx: center.x,
256
+ cy: center.y,
257
+ zoom: zoom * scale,
258
+ w,
259
+ h
260
+ };
261
+ this.lastViewport = view;
262
+ if (view && (view.offsetX > .5 || view.offsetY > .5)) {
263
+ this.webgl.setScissorTest(false);
264
+ this.webgl.setViewport(0, 0, w, h);
265
+ this.webgl.clear();
266
+ this.webgl.setViewport(view.offsetX, view.offsetY, view.contentW, view.contentH);
267
+ this.webgl.setScissor(view.offsetX, view.offsetY, view.contentW, view.contentH);
268
+ this.webgl.setScissorTest(true);
269
+ } else {
270
+ this.webgl.setScissorTest(false);
271
+ this.webgl.setViewport(0, 0, w, h);
272
+ }
273
+ frame(this.worldCam, center.x, center.y, uiW / zoom, uiH / zoom);
274
+ frame(this.uiCam, uiW / 2, uiH / 2, uiW, uiH);
275
+ this.webgl.autoClear = true;
276
+ this.webgl.render(this.worldScene, this.worldCam);
277
+ this.webgl.autoClear = false;
278
+ this.webgl.render(this.uiScene, this.uiCam);
279
+ this.webgl.autoClear = true;
280
+ }
281
+ lastViewport = null;
282
+ lastView = {
283
+ cx: 0,
284
+ cy: 0,
285
+ zoom: 1,
286
+ w: 1,
287
+ h: 1
288
+ };
289
+ /** The world view used by the LAST render (center/zoom/canvas size). */
290
+ view() {
291
+ return { ...this.lastView };
292
+ }
293
+ /**
294
+ * GPU counters for the LAST rendered frame (world + UI passes combined),
295
+ * straight from three's per-frame-maintained `renderer.info`.
296
+ */
297
+ stats() {
298
+ const info = this.webgl.info;
299
+ return {
300
+ triangles: info.render.triangles,
301
+ drawCalls: info.render.calls,
302
+ geometries: info.memory.geometries,
303
+ textures: info.memory.textures
304
+ };
305
+ }
306
+ worldFromScreen(sx, sy) {
307
+ return worldFromScreen(this.lastView, sx, sy);
308
+ }
309
+ screenFromWorld(wx, wy) {
310
+ return screenFromWorld(this.lastView, wx, wy);
311
+ }
312
+ /**
313
+ * The topmost world-pass node under a canvas pixel (UI pass excluded).
314
+ * Hits resolve through `userData.incantoNode` and prefer higher renderOrder.
315
+ */
316
+ pick(sx, sy) {
317
+ const { w, h } = this.lastView;
318
+ const ox = this.lastViewport?.offsetX ?? 0;
319
+ const oy = this.lastViewport?.offsetY ?? 0;
320
+ const cw = this.lastViewport?.contentW ?? w;
321
+ const ch = this.lastViewport?.contentH ?? h;
322
+ pickNdc.set((sx - ox) / cw * 2 - 1, -((sy - oy) / ch * 2 - 1));
323
+ raycaster.setFromCamera(pickNdc, this.worldCam);
324
+ const hits = raycaster.intersectObjects(this.worldScene.children, true);
325
+ let best = null;
326
+ for (const hit of hits) {
327
+ let obj = hit.object;
328
+ while (obj && !obj.userData.incantoNode) obj = obj.parent;
329
+ const node = obj?.userData.incantoNode;
330
+ if (!node) continue;
331
+ const order = hit.object.renderOrder ?? 0;
332
+ if (!best || order >= best.order) best = {
333
+ node,
334
+ order
335
+ };
336
+ }
337
+ return best?.node ?? null;
338
+ }
339
+ /**
340
+ * Canvas-pixel AABB of a node's rendered world-pass objects, or null when it
341
+ * has no renderable extent (plain Nodes, empty containers).
342
+ */
343
+ boundsOf(node) {
344
+ const obj = node._object2D;
345
+ if (!obj) return null;
346
+ boundsScratch.makeEmpty();
347
+ boundsScratch.expandByObject(obj);
348
+ if (boundsScratch.isEmpty()) return null;
349
+ boundsScratch.getSize(sizeScratch3);
350
+ if (sizeScratch3.x === 0 && sizeScratch3.y === 0) return null;
351
+ const a = this.screenFromWorld(boundsScratch.min.x, -boundsScratch.max.y);
352
+ const b = this.screenFromWorld(boundsScratch.max.x, -boundsScratch.min.y);
353
+ return {
354
+ x: a.x,
355
+ y: a.y,
356
+ w: b.x - a.x,
357
+ h: b.y - a.y
358
+ };
359
+ }
360
+ dispose() {
361
+ this.disconnect();
362
+ for (const scene of [this.worldScene, this.uiScene]) scene.traverse((obj) => {
363
+ const mesh = obj;
364
+ if (!mesh.isMesh) return;
365
+ const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
366
+ for (const mat of mats) {
367
+ mat?.map?.dispose();
368
+ mat?.dispose();
369
+ }
370
+ });
371
+ this.webgl.dispose();
372
+ }
373
+ };
374
+ /** Aim an ortho camera at a y-down world center with a view of vw×vh world px. */
375
+ function frame(cam, cx, cy, vw, vh) {
376
+ cam.position.set(cx, -cy, 10);
377
+ cam.left = -vw / 2;
378
+ cam.right = vw / 2;
379
+ cam.top = vh / 2;
380
+ cam.bottom = -vh / 2;
381
+ cam.updateProjectionMatrix();
382
+ }
383
+ const sizeScratch = new Vector2();
384
+ const sizeScratch3 = new Vector3();
385
+ const pickNdc = new Vector2();
386
+ const raycaster = new Raycaster();
387
+ const boundsScratch = new Box3();
388
+ //#endregion
389
+ //#region src/2d/create-game.ts
390
+ var create_game_exports = /* @__PURE__ */ __exportAll({ createGame2D: () => createGame2D });
391
+ /**
392
+ * The one-call boot every game was hand-rolling: register → load (engine
393
+ * attached, so onReady can use this.rng/this.log) → physics (auto-detected) →
394
+ * input → renderer → start. The returned `dispose()` is the one-call
395
+ * teardown for SPA unmounts. Audio unlocks on the first user gesture.
396
+ */
397
+ async function createGame2D(opts) {
398
+ registerNodes2D();
399
+ if (opts.gameplay ?? true) registerGameplayBehaviors();
400
+ for (const [name, ctor] of Object.entries(opts.behaviors ?? {})) registerBehavior(name, ctor, { replace: true });
401
+ const engine = new Engine({
402
+ seed: opts.seed,
403
+ fixedHz: opts.fixedHz,
404
+ ...opts._scheduler ? { scheduler: opts._scheduler } : {}
405
+ });
406
+ const scene = loadScene(cloneSceneJson(opts.scene), {
407
+ resolveScene: opts.resolveScene,
408
+ engine
409
+ });
410
+ engine.setScene(scene);
411
+ let physics = null;
412
+ if (opts.physics === true || (opts.physics ?? "auto") === "auto" && hasBody(scene.root)) physics = await enablePhysics2D(engine);
413
+ const cleanups = [];
414
+ const keyboard = opts.keyboard ?? (typeof window !== "undefined" ? window : false);
415
+ if (keyboard) engine.input.attachKeyboard(keyboard);
416
+ cleanups.push(wireAudioUnlock(engine, scene.root, opts.canvas));
417
+ const touchMode = opts.touch ?? "auto";
418
+ if (touchMode !== false) {
419
+ const host = opts.touchContainer ?? opts.canvas.parentElement ?? null;
420
+ if (host) {
421
+ const touchOpts = {
422
+ force: touchMode === true,
423
+ ...opts._touchDoc ? { doc: opts._touchDoc } : {}
424
+ };
425
+ let touchControls = attachTouchControls(engine, host, touchOpts);
426
+ const offSceneChanged = engine.sceneChanged.connect(() => {
427
+ touchControls?.dispose();
428
+ touchControls = attachTouchControls(engine, host, touchOpts);
429
+ });
430
+ cleanups.push(() => {
431
+ offSceneChanged();
432
+ touchControls?.dispose();
433
+ });
434
+ }
435
+ }
436
+ const renderer = opts._rendererFactory ? opts._rendererFactory(engine, opts.canvas) : new Renderer2D({
437
+ canvas: opts.canvas,
438
+ engine,
439
+ ...opts.pixelRatio !== void 0 ? { pixelRatio: opts.pixelRatio } : {},
440
+ ...opts.antialias !== void 0 ? { antialias: opts.antialias } : {}
441
+ });
442
+ if (opts.debug ?? false) {
443
+ const host = opts.touchContainer ?? opts.canvas.parentElement ?? (typeof document !== "undefined" ? document.body : null);
444
+ if (host) {
445
+ const { attachDebugOverlay } = await import("./debug.js");
446
+ const overlay = attachDebugOverlay(engine, {
447
+ container: host,
448
+ statsSource: () => renderer.stats?.() ?? {},
449
+ ...opts._debugDoc ? { doc: opts._debugDoc } : {}
450
+ });
451
+ if (overlay) cleanups.push(() => overlay.dispose());
452
+ }
453
+ }
454
+ engine.start();
455
+ return {
456
+ engine,
457
+ scene,
458
+ renderer,
459
+ physics,
460
+ stats() {
461
+ return {
462
+ triangles: 0,
463
+ drawCalls: 0,
464
+ geometries: 0,
465
+ textures: 0,
466
+ ...engine.stats(),
467
+ ...renderer.stats?.()
468
+ };
469
+ },
470
+ dispose() {
471
+ renderer.dispose();
472
+ physics?.dispose();
473
+ for (const cleanup of cleanups) cleanup();
474
+ engine.dispose();
475
+ }
476
+ };
477
+ }
478
+ function cloneSceneJson(json) {
479
+ try {
480
+ return structuredClone(json);
481
+ } catch {
482
+ throw new IncantoError("BAD_FORMAT", "createGame \"scene\" must be plain scene JSON (the imported *.scene.json object) — got something unclonable (a loaded Scene, class instances, or functions).");
483
+ }
484
+ }
485
+ function hasBody(root) {
486
+ if (root instanceof PhysicsBody2D) return true;
487
+ for (const child of root.children) if (hasBody(child)) return true;
488
+ return false;
489
+ }
490
+ /**
491
+ * Browsers gate audio behind a user gesture — retry pending players on the
492
+ * first pointer or key gesture, then stop listening once nothing is pending.
493
+ */
494
+ function wireAudioUnlock(engine, root, canvas) {
495
+ const sources = [];
496
+ const canvasTarget = canvas;
497
+ if (canvasTarget.addEventListener) sources.push({
498
+ target: canvasTarget,
499
+ type: "pointerdown"
500
+ });
501
+ if (typeof window !== "undefined") sources.push({
502
+ target: window,
503
+ type: "keydown"
504
+ });
505
+ if (sources.length === 0) return () => {};
506
+ function detachAll() {
507
+ for (const s of sources) s.target.removeEventListener?.(s.type, unlock);
508
+ }
509
+ function unlock() {
510
+ engine.sfx.unlock();
511
+ engine.music.unlock();
512
+ let pending = false;
513
+ const walk = (node) => {
514
+ if (node instanceof AudioPlayer) {
515
+ node.retryPending();
516
+ if (node.pendingGesture) pending = true;
517
+ }
518
+ for (const child of node.children) walk(child);
519
+ };
520
+ walk(root);
521
+ if (!pending) detachAll();
522
+ }
523
+ for (const s of sources) s.target.addEventListener?.(s.type, unlock, { passive: true });
524
+ return detachAll;
525
+ }
526
+ //#endregion
527
+ export { AssetStore2D as a, syncTree2D as i, create_game_exports as n, Renderer2D as r, createGame2D as t };
@@ -0,0 +1,13 @@
1
+ //#region src/core/debug-draw.ts
2
+ const sources = /* @__PURE__ */ new Set();
3
+ /** @internal physics runtimes self-register on enable. */
4
+ function registerDebugSource(source) {
5
+ sources.add(source);
6
+ return () => sources.delete(source);
7
+ }
8
+ /** @internal renderers pull their dimension's sources. */
9
+ function debugSources(dimension) {
10
+ return [...sources].filter((s) => s.dimension === dimension);
11
+ }
12
+ //#endregion
13
+ export { registerDebugSource as n, debugSources as t };
@@ -0,0 +1,66 @@
1
+ import { C as RendererStats, k as LogLevel, v as Engine } from "./behavior-BAQq7HGM.js";
2
+
3
+ //#region src/debug/panel.d.ts
4
+ /** Minimal document surface the overlay needs (injectable for tests). */
5
+ interface DocumentLike {
6
+ createElement(tag: string): HTMLElement;
7
+ }
8
+ //#endregion
9
+ //#region src/debug/index.d.ts
10
+ type DebugPanelId = "explorer" | "inspector" | "logs" | "stats";
11
+ interface DebugOverlayOptions {
12
+ /** Overlay host (default: document.body). Give it position: relative. */
13
+ container?: HTMLElement;
14
+ /**
15
+ * Renderer counters for the Stats chip — the overlay only holds the engine,
16
+ * so createGame wires `() => renderer.stats()` here. Omitted (headless/stub
17
+ * renderers): the chip shows engine stats only.
18
+ */
19
+ statsSource?: () => Partial<RendererStats>;
20
+ /** @internal Test seam — replaces `document`. */
21
+ doc?: DocumentLike;
22
+ }
23
+ /** Attach the overlay; null when no DOM is available (headless). */
24
+ declare function attachDebugOverlay(engine: Engine, opts?: DebugOverlayOptions): DebugOverlay | null;
25
+ declare class DebugOverlay {
26
+ private readonly engine;
27
+ private readonly container;
28
+ private readonly doc;
29
+ private readonly statsSource?;
30
+ private readonly panels;
31
+ private readonly cleanups;
32
+ private readonly menuButton;
33
+ private dropdown;
34
+ private selected;
35
+ private statsChip;
36
+ private readonly logRows;
37
+ private readonly levelEnabled;
38
+ private consoleCapture;
39
+ private consolePatched;
40
+ private frame;
41
+ private editing;
42
+ constructor(engine: Engine, container: HTMLElement, doc: DocumentLike, statsSource?: (() => Partial<RendererStats>) | undefined);
43
+ isOpen(id: DebugPanelId): boolean;
44
+ open(id: DebugPanelId): void;
45
+ close(id: DebugPanelId): void;
46
+ toggle(id: DebugPanelId): void;
47
+ setLevelEnabled(level: LogLevel, on: boolean): void;
48
+ /**
49
+ * Patch console.* into the Logs panel (restored on disable/dispose).
50
+ * Standard monkey-patch hazard: a tool that patches console AFTER capture
51
+ * enables will be clobbered by our restore — enable capture last.
52
+ */
53
+ setConsoleCapture(on: boolean): void;
54
+ dispose(): void;
55
+ private toggleDropdown;
56
+ private openStatsChip;
57
+ private renderStats;
58
+ private renderExplorer;
59
+ private renderInspector;
60
+ private renderPropRow;
61
+ private trackEditing;
62
+ private pushLog;
63
+ private renderLogs;
64
+ }
65
+ //#endregion
66
+ export { DebugOverlay, DebugOverlayOptions, DebugPanelId, attachDebugOverlay };