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,434 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
+ import { n as registerDebugSource } from "./debug-draw-CZmOYjL2.js";
4
+ import { C as Area3D, E as RigidBody3D, O as Node3D, T as PhysicsBody3D, k as validateCollider3D, w as CharacterBody3D, y as Terrain3D } from "./register-CNlYAS1_.js";
5
+ import { Euler, Quaternion } from "three";
6
+ //#region src/3d/physics/physics-3d.ts
7
+ var physics_3d_exports = /* @__PURE__ */ __exportAll({
8
+ Physics3D: () => Physics3D,
9
+ enablePhysics3D: () => enablePhysics3D
10
+ });
11
+ const DEG = Math.PI / 180;
12
+ /**
13
+ * Enable 3D physics for an engine. Dynamically imports Rapier (compat build)
14
+ * so games without physics never pay its bundle cost. 1 unit = 1 meter, y-up —
15
+ * three's native space, no conversion.
16
+ */
17
+ async function enablePhysics3D(engine, opts) {
18
+ const R = await import("@dimforge/rapier3d-compat");
19
+ await R.init();
20
+ return new Physics3D(R, engine, opts);
21
+ }
22
+ /** Per-engine 3D physics world. Same contract as Physics2D. */
23
+ var Physics3D = class {
24
+ R;
25
+ engine;
26
+ /** Render collider outlines in the GAME view (renderers pick this up). */
27
+ debugDraw = false;
28
+ dimension = "3d";
29
+ unregisterDebug;
30
+ warnedNoCollider = /* @__PURE__ */ new WeakSet();
31
+ entries = /* @__PURE__ */ new Map();
32
+ byColliderHandle = /* @__PURE__ */ new Map();
33
+ world;
34
+ events;
35
+ kcc;
36
+ disconnect;
37
+ lastDt = 1 / 60;
38
+ optsGravity;
39
+ lastScene = null;
40
+ constructor(R, engine, opts) {
41
+ this.R = R;
42
+ this.engine = engine;
43
+ this.optsGravity = opts?.gravity;
44
+ this.lastScene = engine.scene;
45
+ const declared = engine.scene?.physics?.gravity;
46
+ const g = opts?.gravity ?? declared ?? [
47
+ 0,
48
+ -9.81,
49
+ 0
50
+ ];
51
+ this.world = new R.World({
52
+ x: g[0] ?? 0,
53
+ y: g[1] ?? 0,
54
+ z: g[2] ?? 0
55
+ });
56
+ this.kcc = this.world.createCharacterController(.01);
57
+ this.events = new R.EventQueue(true);
58
+ this.unregisterDebug = registerDebugSource(this);
59
+ this.disconnect = engine.fixedUpdated.connect((dt) => this.step(dt));
60
+ this.syncBodies();
61
+ }
62
+ /** @internal Driven by engine.fixedUpdated. */
63
+ step(dt) {
64
+ this.lastDt = dt;
65
+ if (this.engine.scene !== this.lastScene) {
66
+ this.lastScene = this.engine.scene;
67
+ if (!this.optsGravity) {
68
+ const g = this.engine.scene?.physics?.gravity ?? [
69
+ 0,
70
+ -9.81,
71
+ 0
72
+ ];
73
+ this.world.gravity = {
74
+ x: g[0] ?? 0,
75
+ y: g[1] ?? 0,
76
+ z: g[2] ?? 0
77
+ };
78
+ }
79
+ }
80
+ this.syncBodies();
81
+ this.world.timestep = dt;
82
+ this.world.step(this.events);
83
+ for (const [node, e] of this.entries) {
84
+ const t = e.body.translation();
85
+ if (!e.body.isFixed()) {
86
+ const [ox, oy, oz] = parentWorldOffset3D(node);
87
+ const next = [
88
+ t.x - ox,
89
+ t.y - oy,
90
+ t.z - oz
91
+ ];
92
+ node._interpPrev = node._interpCurr ?? next;
93
+ node._interpCurr = next;
94
+ node.position = [
95
+ next[0],
96
+ next[1],
97
+ next[2]
98
+ ];
99
+ }
100
+ if (node instanceof RigidBody3D) {
101
+ if (!node.fixedRotation) {
102
+ const q = e.body.rotation();
103
+ eulerScratch.setFromQuaternion(quatScratch.set(q.x, q.y, q.z, q.w), "XYZ");
104
+ node.rotation = [
105
+ eulerScratch.x / DEG,
106
+ eulerScratch.y / DEG,
107
+ eulerScratch.z / DEG
108
+ ];
109
+ }
110
+ const lv = e.body.linvel();
111
+ node.linearVelocity = [
112
+ lv.x,
113
+ lv.y,
114
+ lv.z
115
+ ];
116
+ e.lastV = [
117
+ lv.x,
118
+ lv.y,
119
+ lv.z
120
+ ];
121
+ }
122
+ e.last = worldPosition3D(node);
123
+ }
124
+ this.events.drainCollisionEvents((h1, h2, started) => {
125
+ const a = this.byColliderHandle.get(h1);
126
+ const b = this.byColliderHandle.get(h2);
127
+ if (!a || !b) return;
128
+ const sig = started ? "triggerEnter" : "triggerExit";
129
+ a.emit(sig, b);
130
+ b.emit(sig, a);
131
+ });
132
+ }
133
+ /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
134
+ moveAndSlide(node) {
135
+ const e = this.ensureEntry(node);
136
+ const vy = node.velocity[1] ?? 0;
137
+ if (node.snapToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
138
+ else this.kcc.disableSnapToGround();
139
+ this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
140
+ const desired = {
141
+ x: (node.velocity[0] ?? 0) * this.lastDt,
142
+ y: vy * this.lastDt,
143
+ z: (node.velocity[2] ?? 0) * this.lastDt
144
+ };
145
+ this.kcc.computeColliderMovement(e.collider, desired, this.R.QueryFilterFlags.EXCLUDE_SENSORS);
146
+ const c = this.kcc.computedMovement();
147
+ const t = e.body.translation();
148
+ e.body.setNextKinematicTranslation({
149
+ x: t.x + c.x,
150
+ y: t.y + c.y,
151
+ z: t.z + c.z
152
+ });
153
+ node._grounded = this.kcc.computedGrounded();
154
+ }
155
+ /** Rapier debug segments (meters). Null while debugDraw is off. */
156
+ debugLines() {
157
+ if (!this.debugDraw) return null;
158
+ return this.world.debugRender().vertices;
159
+ }
160
+ dispose() {
161
+ this.disconnect();
162
+ this.unregisterDebug();
163
+ this.world.free();
164
+ }
165
+ syncBodies() {
166
+ const scene = this.engine.scene;
167
+ if (!scene) return;
168
+ const seen = /* @__PURE__ */ new Set();
169
+ collectBodies(scene.root, seen);
170
+ for (const node of seen) {
171
+ if (node.collider.shape === void 0) {
172
+ const stale = this.entries.get(node);
173
+ if (stale) {
174
+ this.byColliderHandle.delete(stale.collider.handle);
175
+ this.world.removeRigidBody(stale.body);
176
+ this.entries.delete(node);
177
+ node._physics = null;
178
+ node._interpPrev = node._interpCurr = null;
179
+ }
180
+ if (!this.warnedNoCollider.has(node)) {
181
+ this.warnedNoCollider.add(node);
182
+ console.warn(`[incanto] '${node.name}' has no collider — physics skips it.`);
183
+ }
184
+ continue;
185
+ }
186
+ const existing = this.entries.get(node);
187
+ if (existing && existing.colliderKey !== JSON.stringify(node.collider)) {
188
+ this.byColliderHandle.delete(existing.collider.handle);
189
+ this.world.removeRigidBody(existing.body);
190
+ this.entries.delete(node);
191
+ node._physics = null;
192
+ node._interpPrev = node._interpCurr = null;
193
+ }
194
+ this.ensureEntry(node);
195
+ }
196
+ for (const [node, e] of this.entries) if (!seen.has(node)) {
197
+ this.byColliderHandle.delete(e.collider.handle);
198
+ this.world.removeRigidBody(e.body);
199
+ this.entries.delete(node);
200
+ node._physics = null;
201
+ node._interpPrev = node._interpCurr = null;
202
+ }
203
+ }
204
+ ensureEntry(node) {
205
+ let e = this.entries.get(node);
206
+ const R = this.R;
207
+ if (!e) {
208
+ let desc;
209
+ if (node instanceof RigidBody3D) desc = R.RigidBodyDesc.dynamic();
210
+ else if (node instanceof CharacterBody3D || node instanceof Area3D) desc = R.RigidBodyDesc.kinematicPositionBased();
211
+ else desc = R.RigidBodyDesc.fixed();
212
+ const [wx, wy, wz] = worldPosition3D(node);
213
+ desc.setTranslation(wx, wy, wz);
214
+ quatScratch.setFromEuler(eulerScratch.set((node.rotation[0] ?? 0) * DEG, (node.rotation[1] ?? 0) * DEG, (node.rotation[2] ?? 0) * DEG, "XYZ"));
215
+ desc.setRotation({
216
+ x: quatScratch.x,
217
+ y: quatScratch.y,
218
+ z: quatScratch.z,
219
+ w: quatScratch.w
220
+ });
221
+ if (node instanceof RigidBody3D) {
222
+ desc.setGravityScale(node.gravityScale);
223
+ desc.setLinvel(node.linearVelocity[0] ?? 0, node.linearVelocity[1] ?? 0, node.linearVelocity[2] ?? 0);
224
+ if (node.fixedRotation) desc.lockRotations();
225
+ }
226
+ const body = this.world.createRigidBody(desc);
227
+ const colDesc = parseCollider3D(R, node);
228
+ colDesc.setActiveEvents(R.ActiveEvents.COLLISION_EVENTS);
229
+ colDesc.setActiveCollisionTypes(R.ActiveCollisionTypes.ALL);
230
+ if (node instanceof Area3D) colDesc.setSensor(true);
231
+ if (node instanceof RigidBody3D) colDesc.setMass(node.mass).setFriction(node.friction).setRestitution(node.restitution);
232
+ const collider = this.world.createCollider(colDesc, body);
233
+ e = {
234
+ colliderKey: JSON.stringify(node.collider),
235
+ body,
236
+ collider,
237
+ last: [
238
+ wx,
239
+ wy,
240
+ wz
241
+ ],
242
+ lastV: [
243
+ 0,
244
+ 0,
245
+ 0
246
+ ]
247
+ };
248
+ this.entries.set(node, e);
249
+ this.byColliderHandle.set(collider.handle, node);
250
+ node._physics = this;
251
+ if (node instanceof RigidBody3D) node._physics3d = this;
252
+ return e;
253
+ }
254
+ const p = worldPosition3D(node);
255
+ if (p[0] !== e.last[0] || p[1] !== e.last[1] || p[2] !== e.last[2]) {
256
+ e.body.setTranslation({
257
+ x: p[0],
258
+ y: p[1],
259
+ z: p[2]
260
+ }, true);
261
+ e.last = p;
262
+ node._interpPrev = node._interpCurr = null;
263
+ }
264
+ if (node instanceof RigidBody3D) {
265
+ const v = [
266
+ node.linearVelocity[0] ?? 0,
267
+ node.linearVelocity[1] ?? 0,
268
+ node.linearVelocity[2] ?? 0
269
+ ];
270
+ if (v[0] !== e.lastV[0] || v[1] !== e.lastV[1] || v[2] !== e.lastV[2]) {
271
+ e.body.setLinvel({
272
+ x: v[0],
273
+ y: v[1],
274
+ z: v[2]
275
+ }, true);
276
+ e.lastV = v;
277
+ }
278
+ if (node.gravityScale !== e.body.gravityScale()) e.body.setGravityScale(node.gravityScale, true);
279
+ }
280
+ return e;
281
+ }
282
+ /** Apply a world-space impulse to a dynamic body (character controllers). */
283
+ applyImpulse(node, impulse) {
284
+ this.entries.get(node)?.body.applyImpulse({
285
+ x: impulse[0],
286
+ y: impulse[1],
287
+ z: impulse[2]
288
+ }, true);
289
+ }
290
+ /** Mass the solver actually uses (collider-derived unless overridden). */
291
+ massOf(node) {
292
+ return this.entries.get(node)?.body.mass() ?? 1;
293
+ }
294
+ /** Current solver velocity (fresher than the node prop mid-step). */
295
+ velocityOf(node) {
296
+ const v = this.entries.get(node)?.body.linvel();
297
+ return v ? [
298
+ v.x,
299
+ v.y,
300
+ v.z
301
+ ] : [
302
+ 0,
303
+ 0,
304
+ 0
305
+ ];
306
+ }
307
+ /**
308
+ * World-space raycast (meters). `exclude` skips that body's collider —
309
+ * ground probes from inside a capsule need it. `opts.staticOnly` hits ONLY the
310
+ * fixed/static world (excludes both dynamic AND kinematic bodies): the camera
311
+ * spring arm uses it so neither a dynamic projectile nor a KINEMATIC enemy
312
+ * (CharacterBody3D) passing behind the player yanks the camera in.
313
+ */
314
+ castRay(origin, dir, maxLen, exclude, opts) {
315
+ const ray = new this.R.Ray({
316
+ x: origin[0],
317
+ y: origin[1],
318
+ z: origin[2]
319
+ }, {
320
+ x: dir[0],
321
+ y: dir[1],
322
+ z: dir[2]
323
+ });
324
+ const excludeCollider = exclude ? this.entries.get(exclude)?.collider : void 0;
325
+ const flags = opts?.staticOnly ? this.R.QueryFilterFlags.EXCLUDE_DYNAMIC | this.R.QueryFilterFlags.EXCLUDE_KINEMATIC : void 0;
326
+ const hit = this.world.castRayAndGetNormal(ray, maxLen, true, flags, void 0, excludeCollider, void 0, (collider) => !collider.isSensor());
327
+ if (!hit) return null;
328
+ return {
329
+ distance: hit.timeOfImpact,
330
+ normal: [
331
+ hit.normal.x,
332
+ hit.normal.y,
333
+ hit.normal.z
334
+ ],
335
+ node: this.byColliderHandle.get(hit.collider.handle) ?? null
336
+ };
337
+ }
338
+ };
339
+ const quatScratch = new Quaternion();
340
+ const eulerScratch = new Euler();
341
+ function collectBodies(node, out) {
342
+ if (node instanceof PhysicsBody3D) out.add(node);
343
+ for (const c of node.children) collectBodies(c, out);
344
+ }
345
+ /**
346
+ * Composed world position (translation only — ancestor rotation/scale are NOT
347
+ * supported for physics bodies; keep body ancestors untransformed or offset-only).
348
+ */
349
+ function worldPosition3D(node) {
350
+ let x = 0;
351
+ let y = 0;
352
+ let z = 0;
353
+ for (let n = node; n; n = n.parent) if (n instanceof Node3D) {
354
+ x += n.position[0] ?? 0;
355
+ y += n.position[1] ?? 0;
356
+ z += n.position[2] ?? 0;
357
+ }
358
+ return [
359
+ x,
360
+ y,
361
+ z
362
+ ];
363
+ }
364
+ function parentWorldOffset3D(node) {
365
+ let x = 0;
366
+ let y = 0;
367
+ let z = 0;
368
+ for (let n = node.parent; n; n = n.parent) if (n instanceof Node3D) {
369
+ x += n.position[0] ?? 0;
370
+ y += n.position[1] ?? 0;
371
+ z += n.position[2] ?? 0;
372
+ }
373
+ return [
374
+ x,
375
+ y,
376
+ z
377
+ ];
378
+ }
379
+ function parseCollider3D(R, node) {
380
+ const collider = node.collider;
381
+ validateCollider3D(collider, node.name);
382
+ if (collider.shape === "heightfield") return buildHeightfieldDesc3D(R, node);
383
+ const desc = buildColliderDesc3D(R, collider, node.name);
384
+ const offset = collider.offset;
385
+ if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
386
+ return desc;
387
+ }
388
+ /**
389
+ * `{shape:'heightfield'}` pulls its height grid from a Terrain3D CHILD of the
390
+ * body — no duplicated data in the collider prop, terrain and collider can
391
+ * never drift apart.
392
+ */
393
+ function buildHeightfieldDesc3D(R, node) {
394
+ const terrain = node.children.find((c) => c instanceof Terrain3D);
395
+ if (!terrain) throw new IncantoError("BAD_FORMAT", `Collider on '${node.name}': heightfield needs a Terrain3D CHILD of this body — add { "type": "Terrain3D" } under the StaticBody3D; the collider reads its height grid.`, { prop: "collider" });
396
+ const hm = terrain._heightmap();
397
+ const nrows = hm.segsZ;
398
+ const ncols = hm.segsX;
399
+ const heights = new Float32Array((nrows + 1) * (ncols + 1));
400
+ for (let ix = 0; ix <= ncols; ix++) for (let iz = 0; iz <= nrows; iz++) heights[ix * (nrows + 1) + iz] = hm.heights[iz * (ncols + 1) + ix];
401
+ const desc = R.ColliderDesc.heightfield(nrows, ncols, heights, {
402
+ x: hm.width,
403
+ y: 1,
404
+ z: hm.depth
405
+ });
406
+ const offset = Array.isArray(node.collider.offset) ? node.collider.offset : [];
407
+ desc.setTranslation((terrain.position[0] ?? 0) + (offset[0] ?? 0), (terrain.position[1] ?? 0) + (offset[1] ?? 0), (terrain.position[2] ?? 0) + (offset[2] ?? 0));
408
+ return desc;
409
+ }
410
+ function buildColliderDesc3D(R, collider, nodeName) {
411
+ const shape = collider.shape;
412
+ if (shape === "box") {
413
+ const size = collider.size;
414
+ if (!Array.isArray(size) || size.length !== 3 || size.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': box needs "size": [x, y, z].`);
415
+ return R.ColliderDesc.cuboid(size[0] / 2, size[1] / 2, size[2] / 2);
416
+ }
417
+ if (shape === "sphere") {
418
+ if (typeof collider.radius !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': sphere needs "radius".`);
419
+ return R.ColliderDesc.ball(collider.radius);
420
+ }
421
+ if (shape === "capsule") {
422
+ if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
423
+ return R.ColliderDesc.capsule(collider.height / 2, collider.radius);
424
+ }
425
+ if (shape === "trimesh") {
426
+ const vertices = collider.vertices;
427
+ const indices = collider.indices;
428
+ if (!Array.isArray(vertices) || !Array.isArray(indices)) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': trimesh needs "vertices" (flat xyz) and "indices" (triangles).`);
429
+ return R.ColliderDesc.trimesh(new Float32Array(vertices), new Uint32Array(indices));
430
+ }
431
+ throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'box', 'sphere', 'capsule', 'trimesh', or 'heightfield', got ${JSON.stringify(shape)}.`);
432
+ }
433
+ //#endregion
434
+ export { enablePhysics3D as n, physics_3d_exports as r, Physics3D as t };
@@ -0,0 +1,65 @@
1
+ import { c as JsonValue } from "./schema-CcoWb32N.js";
2
+ import { n as BehaviorCtor, v as Engine, w as Scene } from "./behavior-BAQq7HGM.js";
3
+ import { CSSProperties, ReactNode } from "react";
4
+
5
+ //#region src/react/index.d.ts
6
+ /** The common surface of Game2D/Game3D the React layer relies on. */
7
+ interface GameHandle {
8
+ engine: Engine;
9
+ scene: Scene;
10
+ renderer: {
11
+ dispose(): void; /** Both renderers provide it (3D takes a z too) — world → canvas px. */
12
+ screenFromWorld?(wx: number, wy: number, wz?: number): {
13
+ x: number;
14
+ y: number;
15
+ };
16
+ };
17
+ dispose(): void;
18
+ }
19
+ /** The running game (null while booting). Must be used under <IncantoCanvas>. */
20
+ declare function useGame(): GameHandle | null;
21
+ /** The running engine (null while booting). */
22
+ declare function useEngine(): Engine | null;
23
+ /**
24
+ * Subscribe to a node prop by path — re-renders ONLY when the value changes
25
+ * (compared per frame via engine.updated). The React-HUD bread and butter:
26
+ * `const text = useNodeProp<string>('UI/Score', 'text')`.
27
+ */
28
+ declare function useNodeProp<T extends JsonValue>(path: string, prop: string): T | undefined;
29
+ /**
30
+ * Subscribe to a node signal — auto-disconnects on unmount and re-wires on
31
+ * scene swaps. The handler may change between renders without re-wiring.
32
+ */
33
+ declare function useSignal(path: string, signal: string, handler: (...args: unknown[]) => void): void;
34
+ interface IncantoCanvasProps {
35
+ /** Scene JSON (cloned internally). Pass a stable reference — a new object re-boots. */
36
+ scene: unknown;
37
+ /** '2d' | '3d' (default: the scene's dimension, falling back to '2d'). */
38
+ render?: "2d" | "3d";
39
+ behaviors?: Record<string, BehaviorCtor>;
40
+ physics?: "auto" | boolean;
41
+ touch?: "auto" | boolean;
42
+ debug?: boolean;
43
+ seed?: number;
44
+ fixedHz?: number;
45
+ pixelRatio?: number;
46
+ antialias?: boolean;
47
+ /** 3D: pointer-look on the canvas. */
48
+ pointer?: boolean | {
49
+ lockOnClick?: boolean;
50
+ };
51
+ /** 'window' (default) | 'canvas' (tabIndex + focus scoped) | false. */
52
+ keyboard?: "window" | "canvas" | false;
53
+ className?: string;
54
+ style?: CSSProperties;
55
+ /** Shown in the overlay until the game is ready. */
56
+ fallback?: ReactNode;
57
+ onReady?: (game: GameHandle) => void;
58
+ /** DOM HUD overlay — absolutely positioned over the canvas, game context inside. */
59
+ children?: ReactNode;
60
+ /** @internal Test seam — replaces the createGame call. */
61
+ _gameFactory?: (opts: Record<string, unknown>) => Promise<GameHandle>;
62
+ }
63
+ declare function IncantoCanvas(props: IncantoCanvasProps): ReactNode;
64
+ //#endregion
65
+ export { GameHandle, IncantoCanvas, IncantoCanvasProps, useEngine, useGame, useNodeProp, useSignal };
package/dist/react.js ADDED
@@ -0,0 +1,209 @@
1
+ import { n as jsonEquals, t as jsonClone } from "./json-BLk7H2Qa.js";
2
+ import { createContext, useContext, useEffect, useRef, useState } from "react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ //#region src/react/index.tsx
5
+ /**
6
+ * incanto/react — embed an Incanto game in a React app like any component.
7
+ *
8
+ * ```tsx
9
+ * import { IncantoCanvas, useNodeProp } from 'incanto/react';
10
+ * import sceneJson from './game.scene.json';
11
+ * import { PlayerControl } from './behaviors';
12
+ *
13
+ * <IncantoCanvas scene={sceneJson} behaviors={{ PlayerControl }}>
14
+ * <Hud /> // children = DOM HUD overlay, with useGame()/useNodeProp() access
15
+ * </IncantoCanvas>
16
+ * ```
17
+ *
18
+ * StrictMode-safe (every booted game is disposed), SSR-safe (boot happens in
19
+ * an effect), and bundle-lean (the 2D or 3D stack lazy-loads by scene
20
+ * dimension — a 2D game never pulls the 3D renderer). React is an optional
21
+ * peer dependency: nothing here loads unless you import 'incanto/react'.
22
+ */
23
+ const GameContext = createContext(null);
24
+ /** The running game (null while booting). Must be used under <IncantoCanvas>. */
25
+ function useGame() {
26
+ return useContext(GameContext);
27
+ }
28
+ /** The running engine (null while booting). */
29
+ function useEngine() {
30
+ return useGame()?.engine ?? null;
31
+ }
32
+ /**
33
+ * Subscribe to a node prop by path — re-renders ONLY when the value changes
34
+ * (compared per frame via engine.updated). The React-HUD bread and butter:
35
+ * `const text = useNodeProp<string>('UI/Score', 'text')`.
36
+ */
37
+ function useNodeProp(path, prop) {
38
+ const game = useGame();
39
+ const [value, setValue] = useState(void 0);
40
+ useEffect(() => {
41
+ if (!game) return;
42
+ const read = () => {
43
+ let next;
44
+ try {
45
+ const node = game.engine.scene?.root.getNodeOrNull(path);
46
+ next = node ? node[prop] : void 0;
47
+ } catch {
48
+ next = void 0;
49
+ }
50
+ setValue((prev) => {
51
+ if (jsonEquals(prev ?? null, next ?? null)) return prev;
52
+ return next === void 0 ? void 0 : jsonClone(next);
53
+ });
54
+ };
55
+ read();
56
+ const offUpdated = game.engine.updated.connect(read);
57
+ const offScene = game.engine.sceneChanged.connect(read);
58
+ return () => {
59
+ offUpdated();
60
+ offScene();
61
+ };
62
+ }, [
63
+ game,
64
+ path,
65
+ prop
66
+ ]);
67
+ return value;
68
+ }
69
+ /**
70
+ * Subscribe to a node signal — auto-disconnects on unmount and re-wires on
71
+ * scene swaps. The handler may change between renders without re-wiring.
72
+ */
73
+ function useSignal(path, signal, handler) {
74
+ const game = useGame();
75
+ const handlerRef = useRef(handler);
76
+ handlerRef.current = handler;
77
+ useEffect(() => {
78
+ if (!game) return;
79
+ const wire = () => {
80
+ try {
81
+ const node = game.engine.scene?.root.getNodeOrNull(path);
82
+ return node ? node.on(signal, (...args) => handlerRef.current(...args)) : null;
83
+ } catch {
84
+ return null;
85
+ }
86
+ };
87
+ let off = wire();
88
+ const offScene = game.engine.sceneChanged.connect(() => {
89
+ off?.();
90
+ off = wire();
91
+ });
92
+ return () => {
93
+ off?.();
94
+ offScene();
95
+ };
96
+ }, [
97
+ game,
98
+ path,
99
+ signal
100
+ ]);
101
+ }
102
+ function shallowEqual(a, b) {
103
+ if (a === b) return true;
104
+ if (!a || !b) return false;
105
+ const keysA = Object.keys(a);
106
+ return keysA.length === Object.keys(b).length && keysA.every((k) => a[k] === b[k]);
107
+ }
108
+ /**
109
+ * Inline `behaviors={{ PlayerControl }}` creates a new object every render —
110
+ * keep the previous reference while the contents are shallow-equal so the
111
+ * boot effect never sees a phantom change (no re-boot loops).
112
+ */
113
+ function useStableShallow(value) {
114
+ const ref = useRef(value);
115
+ if (!shallowEqual(value, ref.current)) ref.current = value;
116
+ return ref.current;
117
+ }
118
+ function resolveKeyboard(keyboard, canvas) {
119
+ if (keyboard === "canvas") {
120
+ canvas.tabIndex = 0;
121
+ canvas.focus?.();
122
+ return canvas;
123
+ }
124
+ if (keyboard === false) return false;
125
+ }
126
+ function IncantoCanvas(props) {
127
+ const canvasRef = useRef(null);
128
+ const containerRef = useRef(null);
129
+ const [game, setGame] = useState(null);
130
+ const propsRef = useRef(props);
131
+ propsRef.current = props;
132
+ const mode = props.render ?? props.scene.dimension ?? "2d";
133
+ const { scene, _gameFactory } = props;
134
+ const behaviors = useStableShallow(props.behaviors);
135
+ useEffect(() => {
136
+ const canvas = canvasRef.current;
137
+ const container = containerRef.current;
138
+ if (!canvas || !container) return;
139
+ let disposed = false;
140
+ let booted = null;
141
+ (async () => {
142
+ const latest = propsRef.current;
143
+ const keyboard = resolveKeyboard(latest.keyboard, canvas);
144
+ const opts = {
145
+ canvas,
146
+ scene,
147
+ behaviors,
148
+ physics: latest.physics,
149
+ touch: latest.touch,
150
+ touchContainer: container,
151
+ debug: latest.debug,
152
+ seed: latest.seed,
153
+ fixedHz: latest.fixedHz,
154
+ pixelRatio: latest.pixelRatio,
155
+ antialias: latest.antialias,
156
+ pointer: latest.pointer,
157
+ ...keyboard !== void 0 ? { keyboard } : {}
158
+ };
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-BdjpTHrW.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-CZHROKcT.js").then((n) => n.n)).createGame2D(o)))(opts);
160
+ if (disposed) {
161
+ next.dispose();
162
+ return;
163
+ }
164
+ booted = next;
165
+ setGame(next);
166
+ propsRef.current.onReady?.(next);
167
+ })();
168
+ return () => {
169
+ disposed = true;
170
+ booted?.dispose();
171
+ setGame(null);
172
+ };
173
+ }, [
174
+ scene,
175
+ mode,
176
+ behaviors,
177
+ _gameFactory
178
+ ]);
179
+ return /* @__PURE__ */ jsxs("div", {
180
+ ref: containerRef,
181
+ className: props.className,
182
+ style: {
183
+ position: "relative",
184
+ width: "100%",
185
+ height: "100%",
186
+ ...props.style
187
+ },
188
+ children: [/* @__PURE__ */ jsx("canvas", {
189
+ ref: canvasRef,
190
+ style: {
191
+ width: "100%",
192
+ height: "100%",
193
+ display: "block"
194
+ }
195
+ }), /* @__PURE__ */ jsx("div", {
196
+ style: {
197
+ position: "absolute",
198
+ inset: 0,
199
+ pointerEvents: "none"
200
+ },
201
+ children: /* @__PURE__ */ jsx(GameContext.Provider, {
202
+ value: game,
203
+ children: game ? props.children : props.fallback ?? null
204
+ })
205
+ })]
206
+ });
207
+ }
208
+ //#endregion
209
+ export { IncantoCanvas, useEngine, useGame, useNodeProp, useSignal };