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,851 @@
1
+ import { f as Node } from "./loader-CGs_G-r0.js";
2
+ import { i as applyParticlePreset, r as PARTICLE_PRESET_NAMES, t as ParticleSim } from "./particle-sim-DYuSUxvK.js";
3
+ import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
+ import { t as Rng } from "./rng-DP-SR7eg.js";
5
+ import { t as registerCoreNodes } from "./register-BuUV1_KB.js";
6
+ import { i as getNodeSchema, l as registerNode } from "./registry-BVJ2HbCn.js";
7
+ import { AdditiveBlending, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NormalBlending, Object3D, PlaneGeometry } from "three";
8
+ //#region src/2d/physics/collider-validate.ts
9
+ /**
10
+ * Hard-validate a 2D collider prop — called at LOAD time (via the loader's
11
+ * static validateJson hook) so bad shapes fail when the scene is authored,
12
+ * not minutes later when physics starts. An EMPTY collider is fine: a body
13
+ * without a collider is a work-in-progress node, not an error.
14
+ */
15
+ function validateCollider2D(collider, nodeName) {
16
+ const shape = collider.shape;
17
+ if (shape === void 0) return;
18
+ if (shape === "rect") {
19
+ const size = collider.size;
20
+ if (!Array.isArray(size) || size.length !== 2 || size.some((v) => typeof v !== "number")) throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': rect needs "size": [width, height].`);
21
+ return;
22
+ }
23
+ if (shape === "circle") {
24
+ if (typeof collider.radius !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': circle needs "radius".`);
25
+ return;
26
+ }
27
+ if (shape === "capsule") {
28
+ if (typeof collider.radius !== "number" || typeof collider.height !== "number") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': capsule needs "radius" and "height".`);
29
+ return;
30
+ }
31
+ throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "shape" must be 'rect', 'circle', or 'capsule', got ${JSON.stringify(shape)}.`);
32
+ }
33
+ //#endregion
34
+ //#region src/2d/nodes/node-2d.ts
35
+ /**
36
+ * Base of every 2D node. Convention (Phaser/Godot prior):
37
+ * **1 unit = 1 px, +y DOWN, (0,0) top-left, positive rotation = clockwise.**
38
+ * Internally mapped onto three's y-up space by negating y and the z-spin.
39
+ */
40
+ var Node2D = class extends Node {
41
+ static typeName = "Node2D";
42
+ static props = {
43
+ position: { default: [0, 0] },
44
+ rotation: { default: 0 },
45
+ scale: { default: [1, 1] },
46
+ renderOrder: { default: 0 },
47
+ visible: { default: true }
48
+ };
49
+ /** Legacy alias: old 2D scenes (and `zIndex`-trained agents) keep loading —
50
+ * `zIndex` resolves to `renderOrder` at load. The schema exposes only
51
+ * `renderOrder` so 2D and 3D share ONE render-order name. */
52
+ static propAliases = { zIndex: "renderOrder" };
53
+ /** Pixels, y-down. */
54
+ position = [0, 0];
55
+ /** Degrees, clockwise. */
56
+ rotation = 0;
57
+ scale = [1, 1];
58
+ /** Draw order among 2D drawables (higher = on top); matches 3D `renderOrder`. */
59
+ renderOrder = 0;
60
+ visible = true;
61
+ _object2D = null;
62
+ /** @internal The backing three object (lazily created). */
63
+ _ensureObject2D() {
64
+ if (!this._object2D) {
65
+ this._object2D = this._createObject2D();
66
+ this._object2D.userData.incantoNode = this;
67
+ }
68
+ return this._object2D;
69
+ }
70
+ /** @internal Override point. */
71
+ _createObject2D() {
72
+ return new Object3D();
73
+ }
74
+ /** @internal Push JSON props onto the backing object. Called every frame. */
75
+ _syncObject2D(_assets) {
76
+ const o = this._ensureObject2D();
77
+ o.position.set(this.position[0] ?? 0, -(this.position[1] ?? 0), 0);
78
+ o.rotation.z = -MathUtils.degToRad(this.rotation);
79
+ o.scale.set(this.scale[0] ?? 1, this.scale[1] ?? 1, 1);
80
+ o.visible = this.visible;
81
+ }
82
+ free() {
83
+ super.free();
84
+ this._object2D?.removeFromParent();
85
+ this._object2D = null;
86
+ }
87
+ };
88
+ //#endregion
89
+ //#region src/2d/nodes/bodies-2d.ts
90
+ /**
91
+ * Shared base for physics-backed 2D nodes. Colliders are NODE PROPS:
92
+ * `{shape:'rect', size:[w,h]}` · `{shape:'circle', radius}` ·
93
+ * `{shape:'capsule', radius, height}` — never child shape nodes.
94
+ */
95
+ var PhysicsBody2D = class extends Node2D {
96
+ static props = { collider: { default: {} } };
97
+ /** The unified collision model: every collider participant can emit these. */
98
+ static signals = ["triggerEnter", "triggerExit"];
99
+ collider = {};
100
+ /** Loader hook: bad collider shapes fail at LOAD, not at physics start. */
101
+ static validateJson(node) {
102
+ validateCollider2D(node.collider, node.name);
103
+ }
104
+ /** @internal Set by Physics2D when the body is created. */
105
+ _physics = null;
106
+ };
107
+ /** Immovable collider (ground, walls, platforms). */
108
+ var StaticBody2D = class extends PhysicsBody2D {
109
+ static typeName = "StaticBody2D";
110
+ };
111
+ /**
112
+ * Sensor volume. Emits `triggerEnter(other)` / `triggerExit(other)` — the
113
+ * unified collision model (solid bodies emit the same signals on contact).
114
+ */
115
+ var Area2D = class extends PhysicsBody2D {
116
+ static typeName = "Area2D";
117
+ };
118
+ /** Dynamic simulated body. */
119
+ var RigidBody2D = class extends PhysicsBody2D {
120
+ static typeName = "RigidBody2D";
121
+ static props = {
122
+ mass: { default: 1 },
123
+ gravityScale: { default: 1 },
124
+ fixedRotation: { default: false },
125
+ friction: { default: .5 },
126
+ restitution: { default: 0 },
127
+ linearVelocity: { default: [0, 0] }
128
+ };
129
+ mass = 1;
130
+ gravityScale = 1;
131
+ fixedRotation = false;
132
+ friction = .5;
133
+ restitution = 0;
134
+ /** px/s, y-down. Read back every step; write to launch. */
135
+ linearVelocity = [0, 0];
136
+ };
137
+ /**
138
+ * Kinematic body on Rapier's character controller. Gravity is NOT applied
139
+ * automatically (Godot semantics) — integrate `velocity` yourself, then call
140
+ * `moveAndSlide()` from `fixedUpdate`.
141
+ */
142
+ var CharacterBody2D = class extends PhysicsBody2D {
143
+ static typeName = "CharacterBody2D";
144
+ static props = {
145
+ velocity: { default: [0, 0] },
146
+ snapToGround: { default: true },
147
+ slopeLimitDeg: { default: 45 }
148
+ };
149
+ /** px/s, y-down (up = -y). */
150
+ velocity = [0, 0];
151
+ /** Keep contact on slopes/steps while not moving upward. */
152
+ snapToGround = true;
153
+ /** Max climbable slope angle. */
154
+ slopeLimitDeg = 45;
155
+ /** @internal Updated by Physics2D.moveAndSlide. */
156
+ _grounded = false;
157
+ moveAndSlide() {
158
+ if (!this._physics) throw new IncantoError("TREE_VIOLATION", this.collider.shape === void 0 ? `CharacterBody2D '${this.name}' has no collider — set a collider prop (physics skips collider-less bodies).` : `CharacterBody2D '${this.name}' has no physics world — await enablePhysics2D(engine) first.`);
159
+ this._physics.moveAndSlide(this);
160
+ }
161
+ isOnFloor() {
162
+ return this._grounded;
163
+ }
164
+ };
165
+ //#endregion
166
+ //#region src/2d/nodes/sprite-2d.ts
167
+ /** Shared unit quad — sprites are sized via the quad's scale, never THREE.Sprite. */
168
+ const UNIT_PLANE$2 = new PlaneGeometry(1, 1);
169
+ /**
170
+ * A textured quad. The drawable is an inner mesh child of the node's backing
171
+ * object, so node children never inherit the texture-size scaling.
172
+ */
173
+ var Sprite2D = class extends Node2D {
174
+ static typeName = "Sprite2D";
175
+ static props = {
176
+ texture: { default: "" },
177
+ anchor: { default: [.5, .5] },
178
+ flipX: { default: false },
179
+ flipY: { default: false },
180
+ tint: { default: "#ffffff" },
181
+ opacity: { default: 1 }
182
+ };
183
+ /** `'$assetKey'` texture reference. Empty = hidden. */
184
+ texture = "";
185
+ /** [0,0] = top-left on the origin … [1,1] = bottom-right on the origin. */
186
+ anchor = [.5, .5];
187
+ flipX = false;
188
+ flipY = false;
189
+ tint = "#ffffff";
190
+ opacity = 1;
191
+ quadMesh = null;
192
+ /** @internal The drawable quad (lazily created, attached under the backing object). */
193
+ _quad() {
194
+ if (!this.quadMesh) {
195
+ this.quadMesh = new Mesh(UNIT_PLANE$2, new MeshBasicMaterial({
196
+ transparent: true,
197
+ depthWrite: false
198
+ }));
199
+ this._ensureObject2D().add(this.quadMesh);
200
+ }
201
+ return this.quadMesh;
202
+ }
203
+ _createObject2D() {
204
+ return super._createObject2D();
205
+ }
206
+ /** Override point: AnimatedSprite2D substitutes its frame window here. */
207
+ resolveTexture(assets) {
208
+ if (!this.texture || !assets) return null;
209
+ const texture = assets.getTexture(this.texture);
210
+ const image = texture.image;
211
+ if (!image?.width || !image.height) return null;
212
+ return {
213
+ texture,
214
+ width: image.width,
215
+ height: image.height
216
+ };
217
+ }
218
+ _syncObject2D(assets) {
219
+ super._syncObject2D(assets);
220
+ const quad = this._quad();
221
+ const mat = quad.material;
222
+ const resolved = this.resolveTexture(assets);
223
+ if (!resolved) {
224
+ quad.visible = false;
225
+ return;
226
+ }
227
+ quad.visible = true;
228
+ if (mat.map !== resolved.texture) {
229
+ mat.map = resolved.texture;
230
+ mat.needsUpdate = true;
231
+ }
232
+ const w = resolved.width;
233
+ const h = resolved.height;
234
+ quad.scale.set(this.flipX ? -w : w, this.flipY ? -h : h, 1);
235
+ const ax = this.anchor[0] ?? .5;
236
+ const ay = this.anchor[1] ?? .5;
237
+ quad.position.set((.5 - ax) * w, (ay - .5) * h, 0);
238
+ quad.renderOrder = this.renderOrder;
239
+ mat.color.set(this.tint);
240
+ mat.opacity = this.opacity;
241
+ }
242
+ };
243
+ //#endregion
244
+ //#region src/2d/nodes/animated-sprite-2d.ts
245
+ /**
246
+ * Spritesheet animation: pure-JSON animation map, frame selection via a UV
247
+ * window on the node's own texture clone. Frames advance in `update(dt)` —
248
+ * deterministic and headless-testable.
249
+ */
250
+ var AnimatedSprite2D = class extends Sprite2D {
251
+ static typeName = "AnimatedSprite2D";
252
+ static signals = ["animationFinished"];
253
+ static props = {
254
+ sheet: { default: "" },
255
+ animations: { default: {} },
256
+ autoplay: { default: "" }
257
+ };
258
+ /** `'$assetKey'` of a spritesheet asset. */
259
+ sheet = "";
260
+ animations = {};
261
+ autoplay = "";
262
+ playing = false;
263
+ /** Absolute frame index within the sheet. */
264
+ currentFrame = 0;
265
+ currentAnimation = "";
266
+ frameList = [];
267
+ frameIndex = 0;
268
+ frameTime = 0;
269
+ ownTexture = null;
270
+ loadedSheetRef = "";
271
+ play(name) {
272
+ const def = this.animations[name];
273
+ if (!def) throw new IncantoError("UNKNOWN_ANIMATION", `No animation '${name}' on '${this.name}'. Available: [${Object.keys(this.animations).join(", ")}].`);
274
+ this.currentAnimation = name;
275
+ this.frameList = resolveFrames(def.frames, name);
276
+ this.frameIndex = 0;
277
+ this.currentFrame = this.frameList[0] ?? 0;
278
+ this.frameTime = 0;
279
+ this.playing = true;
280
+ }
281
+ stop() {
282
+ this.playing = false;
283
+ }
284
+ onReady() {
285
+ if (this.autoplay) this.play(this.autoplay);
286
+ }
287
+ update(dt) {
288
+ if (!this.playing || this.currentAnimation === "") return;
289
+ const def = this.animations[this.currentAnimation];
290
+ if (!def || !Number.isFinite(def.fps) || def.fps <= 0) return;
291
+ const perFrame = 1 / def.fps;
292
+ this.frameTime += dt;
293
+ while (this.frameTime >= perFrame) {
294
+ this.frameTime -= perFrame;
295
+ this.frameIndex += 1;
296
+ if (this.frameIndex >= this.frameList.length) if (def.loop) this.frameIndex = 0;
297
+ else {
298
+ this.frameIndex = this.frameList.length - 1;
299
+ this.playing = false;
300
+ this.emit("animationFinished", this.currentAnimation);
301
+ break;
302
+ }
303
+ }
304
+ this.currentFrame = this.frameList[this.frameIndex] ?? 0;
305
+ }
306
+ resolveTexture(assets) {
307
+ if (!this.sheet || !assets) return null;
308
+ const info = assets.getSheet(this.sheet);
309
+ if (this.loadedSheetRef !== this.sheet) {
310
+ this.ownTexture = info.texture.clone();
311
+ this.ownTexture.needsUpdate = true;
312
+ this.loadedSheetRef = this.sheet;
313
+ }
314
+ const tex = this.ownTexture;
315
+ const image = info.texture.image;
316
+ if (!image?.width || !image.height) return null;
317
+ const cols = Math.max(1, Math.floor(image.width / info.frameWidth));
318
+ const col = this.currentFrame % cols;
319
+ const row = Math.floor(this.currentFrame / cols);
320
+ tex.repeat.set(info.frameWidth / image.width, info.frameHeight / image.height);
321
+ tex.offset.set(col * info.frameWidth / image.width, 1 - (row + 1) * info.frameHeight / image.height);
322
+ return {
323
+ texture: tex,
324
+ width: info.frameWidth,
325
+ height: info.frameHeight
326
+ };
327
+ }
328
+ };
329
+ function resolveFrames(frames, name) {
330
+ if (!Array.isArray(frames) || frames.length === 0) throw new IncantoError("BAD_FORMAT", `Animation '${name}': "frames" must be a non-empty array.`);
331
+ if (frames.length === 2) {
332
+ const [start, end] = frames;
333
+ if (end >= start) {
334
+ const out = [];
335
+ for (let f = start; f <= end; f++) out.push(f);
336
+ return out;
337
+ }
338
+ }
339
+ return [...frames];
340
+ }
341
+ //#endregion
342
+ //#region src/2d/nodes/camera-2d.ts
343
+ /**
344
+ * 2D camera: its `position` is the view CENTER. `follow` tracks a node path
345
+ * with exponential smoothing; `limits [minX,minY,maxX,maxY]` clamp the view
346
+ * rect inside a world region (applied by the renderer via `clampedCenter`).
347
+ */
348
+ var Camera2D = class extends Node2D {
349
+ static typeName = "Camera2D";
350
+ static props = {
351
+ follow: { default: "" },
352
+ smoothing: { default: 0 },
353
+ zoom: { default: 1 },
354
+ limits: { default: [] },
355
+ current: { default: false }
356
+ };
357
+ /** NodePath of a Node2D to track. */
358
+ follow = "";
359
+ /** 0 = snap; 0.85–0.95 = smooth chase (per-frame retention at 60fps). */
360
+ smoothing = 0;
361
+ zoom = 1;
362
+ /** [minX, minY, maxX, maxY] world px; empty = unlimited. */
363
+ limits = [];
364
+ current = false;
365
+ /** `zoom` clamped to a positive floor — 0/negative zoom must never produce NaN views. */
366
+ get effectiveZoom() {
367
+ return Math.max(.01, this.zoom);
368
+ }
369
+ update(dt) {
370
+ if (!this.follow) return;
371
+ const target = this.getNodeOrNull(this.follow);
372
+ if (!(target instanceof Node2D)) return;
373
+ const tx = target.position[0] ?? 0;
374
+ const ty = target.position[1] ?? 0;
375
+ const s = Math.min(Math.max(this.smoothing, 0), .99);
376
+ const f = s <= 0 ? 1 : 1 - s ** (dt * 60);
377
+ const px = this.position[0] ?? 0;
378
+ const py = this.position[1] ?? 0;
379
+ this.position = [px + (tx - px) * f, py + (ty - py) * f];
380
+ }
381
+ /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */
382
+ clampedCenter(vw, vh) {
383
+ let cx = this.position[0] ?? 0;
384
+ let cy = this.position[1] ?? 0;
385
+ if (this.limits.length !== 0 && this.limits.length !== 4) throw new IncantoError("BAD_FORMAT", `Camera2D '${this.name}' limits must be [] or [minX,minY,maxX,maxY], got length ${this.limits.length}.`);
386
+ if (this.limits.length === 4) {
387
+ const [minX, minY, maxX, maxY] = this.limits;
388
+ const halfW = vw / (2 * this.effectiveZoom);
389
+ const halfH = vh / (2 * this.effectiveZoom);
390
+ cx = clampCentered(cx, minX + halfW, maxX - halfW);
391
+ cy = clampCentered(cy, minY + halfH, maxY - halfH);
392
+ }
393
+ return {
394
+ x: cx,
395
+ y: cy
396
+ };
397
+ }
398
+ };
399
+ function clampCentered(v, lo, hi) {
400
+ if (lo > hi) return (lo + hi) / 2;
401
+ return Math.min(hi, Math.max(lo, v));
402
+ }
403
+ //#endregion
404
+ //#region src/2d/nodes/character-controller-2d.ts
405
+ /**
406
+ * Preset controller: drives its PARENT CharacterBody2D from the engine
407
+ * InputMap — JSON-only games get a playable character with zero TypeScript.
408
+ *
409
+ * - `platformer`: x movement + gravity (from the scene `physics.gravity[1]`,
410
+ * default 980) + jump (`jumpHeight` px → impulse √(2·g·h))
411
+ * - `topDown`: full-axis movement, no gravity
412
+ */
413
+ var CharacterController2D = class extends Node {
414
+ static typeName = "CharacterController2D";
415
+ static props = {
416
+ mode: {
417
+ default: "platformer",
418
+ options: ["platformer", "topDown"]
419
+ },
420
+ maxSpeed: { default: 220 },
421
+ jumpHeight: { default: 64 },
422
+ moveAction: { default: "move" },
423
+ jumpAction: { default: "jump" }
424
+ };
425
+ mode = "platformer";
426
+ maxSpeed = 220;
427
+ /** Pixels (platformer mode). */
428
+ jumpHeight = 64;
429
+ moveAction = "move";
430
+ jumpAction = "jump";
431
+ onReady() {
432
+ if (!(this.parent instanceof CharacterBody2D)) throw new IncantoError("TREE_VIOLATION", `CharacterController2D '${this.name}' must be a child of a CharacterBody2D (parent is '${this.parent?.name ?? "none"}').`);
433
+ if (this.mode !== "platformer" && this.mode !== "topDown") throw new IncantoError("PROP_TYPE_MISMATCH", `CharacterController2D.mode must be 'platformer' or 'topDown', got '${this.mode}'.`);
434
+ }
435
+ fixedUpdate(dt) {
436
+ const engine = this.tree?.engine;
437
+ if (!engine?.scene) return;
438
+ const body = this.parent;
439
+ if (!(body instanceof CharacterBody2D)) return;
440
+ const input = engine.input;
441
+ const dir = input.getVector(this.moveAction);
442
+ if (this.mode === "topDown") {
443
+ body.velocity = [dir.x * this.maxSpeed, dir.y * this.maxSpeed];
444
+ body.moveAndSlide();
445
+ return;
446
+ }
447
+ const gravityY = (engine.scene.physics?.gravity)?.[1] ?? 980;
448
+ body.velocity[0] = dir.x * this.maxSpeed;
449
+ if (body.isOnFloor()) {
450
+ const jumped = input.justPressed(this.jumpAction);
451
+ body.velocity[1] = jumped ? -Math.sqrt(2 * gravityY * this.jumpHeight) : 20;
452
+ } else body.velocity[1] = (body.velocity[1] ?? 0) + gravityY * dt;
453
+ body.moveAndSlide();
454
+ }
455
+ };
456
+ //#endregion
457
+ //#region src/2d/nodes/color-rect-2d.ts
458
+ /** Shared unit quad (same discipline as sprites — never THREE.Sprite). */
459
+ const UNIT_PLANE$1 = new PlaneGeometry(1, 1);
460
+ /**
461
+ * A solid-colored rectangle — no texture, no asset file. The prototyping
462
+ * workhorse: paddles, walls, platforms, flashes, fade overlays. Swap in a
463
+ * Sprite2D when art arrives.
464
+ */
465
+ var ColorRect2D = class extends Node2D {
466
+ static typeName = "ColorRect2D";
467
+ static props = {
468
+ size: { default: [100, 100] },
469
+ color: { default: "#ffffff" },
470
+ opacity: { default: 1 },
471
+ anchor: { default: [.5, .5] }
472
+ };
473
+ /** [width, height] in world px. */
474
+ size = [100, 100];
475
+ color = "#ffffff";
476
+ opacity = 1;
477
+ /** [0,0] = top-left on the origin … [1,1] = bottom-right on the origin. */
478
+ anchor = [.5, .5];
479
+ quadMesh = null;
480
+ /** @internal The drawable quad (lazily created under the backing object). */
481
+ _quad() {
482
+ if (!this.quadMesh) {
483
+ this.quadMesh = new Mesh(UNIT_PLANE$1, new MeshBasicMaterial({
484
+ transparent: true,
485
+ depthWrite: false
486
+ }));
487
+ this._ensureObject2D().add(this.quadMesh);
488
+ }
489
+ return this.quadMesh;
490
+ }
491
+ _syncObject2D(assets) {
492
+ super._syncObject2D(assets);
493
+ const quad = this._quad();
494
+ const w = this.size[0] ?? 0;
495
+ const h = this.size[1] ?? 0;
496
+ quad.visible = w > 0 && h > 0 && this.opacity > 0;
497
+ quad.scale.set(w, h, 1);
498
+ const ax = this.anchor[0] ?? .5;
499
+ const ay = this.anchor[1] ?? .5;
500
+ quad.position.set((.5 - ax) * w, (ay - .5) * h, 0);
501
+ quad.renderOrder = this.renderOrder;
502
+ const mat = quad.material;
503
+ mat.color.set(this.color);
504
+ mat.opacity = this.opacity;
505
+ }
506
+ };
507
+ //#endregion
508
+ //#region src/2d/nodes/label.ts
509
+ const LABEL_PLANE = new PlaneGeometry(1, 1);
510
+ /**
511
+ * CanvasTexture-backed text (zero deps, WebGL-safe). Re-rasterizes only when
512
+ * text props change. Headless-safe: without a DOM it simply stays hidden —
513
+ * all prop/serialization logic still works.
514
+ */
515
+ var Label = class extends Node2D {
516
+ static typeName = "Label";
517
+ static props = {
518
+ text: { default: "" },
519
+ fontSize: { default: 16 },
520
+ color: { default: "#ffffff" },
521
+ font: { default: "monospace" },
522
+ align: { default: "left" }
523
+ };
524
+ text = "";
525
+ fontSize = 16;
526
+ color = "#ffffff";
527
+ font = "monospace";
528
+ /** 'left' | 'center' | 'right' — anchor of the text block on the node origin. */
529
+ align = "left";
530
+ quadMesh = null;
531
+ canvas = null;
532
+ lastKey = "";
533
+ /** @internal */
534
+ _quad() {
535
+ if (!this.quadMesh) {
536
+ this.quadMesh = new Mesh(LABEL_PLANE, new MeshBasicMaterial({
537
+ transparent: true,
538
+ depthWrite: false
539
+ }));
540
+ this._ensureObject2D().add(this.quadMesh);
541
+ }
542
+ return this.quadMesh;
543
+ }
544
+ _syncObject2D(assets) {
545
+ super._syncObject2D(assets);
546
+ const quad = this._quad();
547
+ if (typeof document === "undefined" || this.text === "") {
548
+ quad.visible = false;
549
+ return;
550
+ }
551
+ const key = `${this.text}\0${this.fontSize}\0${this.color}\0${this.font}\0${this.align}`;
552
+ if (key !== this.lastKey) {
553
+ this.lastKey = key;
554
+ this.rasterize();
555
+ }
556
+ quad.visible = true;
557
+ quad.renderOrder = this.renderOrder;
558
+ }
559
+ rasterize() {
560
+ const dpr = Math.min(globalThis.devicePixelRatio ?? 1, 2);
561
+ this.canvas ??= document.createElement("canvas");
562
+ const ctx = this.canvas.getContext("2d");
563
+ if (!ctx) return;
564
+ const fontSpec = `${this.fontSize * dpr}px ${this.font}`;
565
+ ctx.font = fontSpec;
566
+ const metrics = ctx.measureText(this.text);
567
+ const w = Math.max(1, Math.ceil(metrics.width));
568
+ const h = Math.ceil(this.fontSize * dpr * 1.4);
569
+ this.canvas.width = w;
570
+ this.canvas.height = h;
571
+ ctx.font = fontSpec;
572
+ ctx.fillStyle = this.color;
573
+ ctx.textBaseline = "middle";
574
+ ctx.fillText(this.text, 0, h / 2);
575
+ const quad = this._quad();
576
+ const mat = quad.material;
577
+ mat.map?.dispose();
578
+ mat.map = new CanvasTexture(this.canvas);
579
+ mat.needsUpdate = true;
580
+ const cssW = w / dpr;
581
+ const cssH = h / dpr;
582
+ quad.scale.set(cssW, cssH, 1);
583
+ const ax = this.align === "center" ? .5 : this.align === "right" ? 1 : 0;
584
+ quad.position.set((.5 - ax) * cssW, 0, 0);
585
+ }
586
+ };
587
+ //#endregion
588
+ //#region src/2d/nodes/particles-2d.ts
589
+ const UNIT_PLANE = new PlaneGeometry(1, 1);
590
+ /**
591
+ * GPU-instanced particle emitter with predefined looks: set `preset` to
592
+ * `'fire' | 'smoke' | 'sparks' | 'fireworks' | 'explosion' | 'flash' |
593
+ * 'lightning' | 'rain' | 'snow' | 'magic'` and every prop snaps to that
594
+ * look's baseline — any prop you write in the scene overrides it. `rate: 0`
595
+ * + `burst` makes a one-shot that emits `finished` (queueFree it there).
596
+ * Simulation is deterministic under the engine seed.
597
+ */
598
+ var Particles2D = class Particles2D extends Node2D {
599
+ static typeName = "Particles2D";
600
+ static signals = ["finished"];
601
+ static props = {
602
+ preset: {
603
+ default: "custom",
604
+ options: ["custom", ...PARTICLE_PRESET_NAMES]
605
+ },
606
+ emitting: { default: true },
607
+ rate: { default: 40 },
608
+ burst: { default: 0 },
609
+ lifetime: { default: [.6, 1.2] },
610
+ speed: { default: [40, 120] },
611
+ directionDeg: { default: -90 },
612
+ spreadDeg: { default: 30 },
613
+ gravity: { default: [0, 0] },
614
+ drag: { default: 0 },
615
+ sizeStart: { default: 8 },
616
+ sizeEnd: { default: 2 },
617
+ colorStart: { default: "#ffffff" },
618
+ colorEnd: { default: "#ffffff" },
619
+ paletteColors: { default: [] },
620
+ alphaStart: { default: 1 },
621
+ alphaEnd: { default: 0 },
622
+ shimmer: { default: 0 },
623
+ blend: {
624
+ default: "add",
625
+ options: ["add", "normal"]
626
+ },
627
+ maxParticles: { default: 256 }
628
+ };
629
+ preset = "custom";
630
+ emitting = true;
631
+ rate = 40;
632
+ burst = 0;
633
+ lifetime = [.6, 1.2];
634
+ speed = [40, 120];
635
+ directionDeg = -90;
636
+ spreadDeg = 30;
637
+ gravity = [0, 0];
638
+ drag = 0;
639
+ sizeStart = 8;
640
+ sizeEnd = 2;
641
+ colorStart = "#ffffff";
642
+ colorEnd = "#ffffff";
643
+ /** Per-particle base colours sampled by the particle's stable seed (multi-
644
+ * colour confetti). Empty → the colorStart→colorEnd ramp is used unchanged. */
645
+ paletteColors = [];
646
+ alphaStart = 1;
647
+ alphaEnd = 0;
648
+ /** Per-particle alpha twinkle frequency in Hz (0 = off) — a seed-phased sine
649
+ * over the particle's age, so each sparkle glitters on its own rhythm. */
650
+ shimmer = 0;
651
+ blend = "add";
652
+ maxParticles = 256;
653
+ /** Loader hook: unknown presets and bad blends fail at LOAD. */
654
+ static validateJson(node) {
655
+ const p = node;
656
+ if (p.preset !== "custom" && !PARTICLE_PRESET_NAMES.includes(p.preset)) throw new IncantoError("BAD_FORMAT", `Particles2D '${node.name}' preset must be 'custom' or one of [${PARTICLE_PRESET_NAMES.join(", ")}], got '${p.preset}'.`, {
657
+ prop: "preset",
658
+ validOptions: PARTICLE_PRESET_NAMES
659
+ });
660
+ if (p.blend !== "add" && p.blend !== "normal") throw new IncantoError("BAD_FORMAT", `Particles2D '${node.name}' blend must be 'add' or 'normal', got '${p.blend}'.`, {
661
+ prop: "blend",
662
+ validOptions: ["add", "normal"]
663
+ });
664
+ }
665
+ sim = null;
666
+ bursted = false;
667
+ finishedEmitted = false;
668
+ /** @internal Lazily applies the preset + builds the deterministic sim. */
669
+ _ensureSim() {
670
+ if (!this.sim) {
671
+ applyParticlePreset(this, this.preset, getNodeSchema(Particles2D.typeName));
672
+ const rng = this.tree?.engine?.rng ?? new Rng(4660);
673
+ this.sim = new ParticleSim({
674
+ rate: this.emitting ? this.rate : 0,
675
+ lifetime: [this.lifetime[0] ?? 1, this.lifetime[1] ?? 1],
676
+ speed: [this.speed[0] ?? 0, this.speed[1] ?? 0],
677
+ directionDeg: this.directionDeg,
678
+ spreadDeg: this.spreadDeg,
679
+ gravity: [
680
+ this.gravity[0] ?? 0,
681
+ this.gravity[1] ?? 0,
682
+ 0
683
+ ],
684
+ drag: this.drag,
685
+ maxParticles: this.maxParticles
686
+ }, rng);
687
+ }
688
+ return this.sim;
689
+ }
690
+ update(dt) {
691
+ const sim = this._ensureSim();
692
+ if (this.burst > 0 && !this.bursted) {
693
+ this.bursted = true;
694
+ sim.burst(this.burst);
695
+ }
696
+ sim.update(dt);
697
+ if ((!this.emitting || this.rate === 0) && this.bursted && sim.done && !this.finishedEmitted) {
698
+ this.finishedEmitted = true;
699
+ this.emit("finished");
700
+ }
701
+ }
702
+ /** Restart a one-shot (re-burst + allow finished to fire again). */
703
+ replay() {
704
+ this.bursted = false;
705
+ this.finishedEmitted = false;
706
+ }
707
+ mesh = null;
708
+ /** Parsed `paletteColors`, reused; rebuilt only when the hex list changes. */
709
+ paletteCache = [];
710
+ paletteCacheKey = "";
711
+ /** Parse `paletteColors` into a reused `Color[]`, rebuilt only on change;
712
+ * null when empty so the caller uses the colorStart→colorEnd ramp. */
713
+ refreshPalette() {
714
+ const list = this.paletteColors;
715
+ if (list.length === 0) return null;
716
+ const key = list.join("|");
717
+ if (key !== this.paletteCacheKey) {
718
+ this.paletteCacheKey = key;
719
+ for (let i = 0; i < list.length; i++) {
720
+ let c = this.paletteCache[i];
721
+ if (!c) {
722
+ c = new Color();
723
+ this.paletteCache[i] = c;
724
+ }
725
+ c.set(list[i]);
726
+ }
727
+ this.paletteCache.length = list.length;
728
+ }
729
+ return this.paletteCache;
730
+ }
731
+ _syncObject2D(assets) {
732
+ super._syncObject2D(assets);
733
+ const sim = this._ensureSim();
734
+ if (!this.mesh || this.mesh.count !== this.maxParticles) {
735
+ if (this.mesh) this.mesh.removeFromParent();
736
+ const material = new MeshBasicMaterial({
737
+ transparent: true,
738
+ depthWrite: false,
739
+ blending: this.blend === "add" ? AdditiveBlending : NormalBlending
740
+ });
741
+ this.mesh = new InstancedMesh(UNIT_PLANE, material, this.maxParticles);
742
+ this.mesh.instanceMatrix.setUsage(DynamicDrawUsage);
743
+ this.mesh.frustumCulled = false;
744
+ this._ensureObject2D().add(this.mesh);
745
+ }
746
+ const mesh = this.mesh;
747
+ const palette = this.refreshPalette();
748
+ const start = palette ? null : colorScratchA.set(this.colorStart);
749
+ const end = colorScratchB.set(this.colorEnd);
750
+ const shimmerHz = this.shimmer;
751
+ let index = 0;
752
+ sim.forEach((p) => {
753
+ const size = this.sizeStart + (this.sizeEnd - this.sizeStart) * p.t;
754
+ matrixScratch.makeScale(size, size, 1);
755
+ matrixScratch.setPosition(p.x, -p.y, 0);
756
+ mesh.setMatrixAt(index, matrixScratch);
757
+ const base = palette ? palette[Math.floor(p.seed * palette.length)] : start;
758
+ let alpha = Math.max(0, this.alphaStart + (this.alphaEnd - this.alphaStart) * p.t);
759
+ if (shimmerHz > 0) alpha *= .5 + .5 * Math.sin(2 * Math.PI * (p.age * shimmerHz + p.seed));
760
+ colorScratchC.copy(base).lerp(end, p.t).multiplyScalar(alpha);
761
+ mesh.setColorAt(index, colorScratchC);
762
+ index += 1;
763
+ });
764
+ mesh.count = index;
765
+ mesh.instanceMatrix.needsUpdate = true;
766
+ if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
767
+ mesh.renderOrder = this.renderOrder;
768
+ }
769
+ };
770
+ const matrixScratch = new Matrix4();
771
+ const colorScratchA = new Color();
772
+ const colorScratchB = new Color();
773
+ const colorScratchC = new Color();
774
+ //#endregion
775
+ //#region src/2d/nodes/ui-layer.ts
776
+ const ANCHORS = [
777
+ "top-left",
778
+ "top",
779
+ "top-right",
780
+ "left",
781
+ "center",
782
+ "right",
783
+ "bottom-left",
784
+ "bottom",
785
+ "bottom-right"
786
+ ];
787
+ /**
788
+ * Screen-space container (Godot CanvasLayer): 2D descendants render in a
789
+ * separate pass that ignores the world camera — HUDs, scores, menus.
790
+ *
791
+ * `anchor` pins the layer's origin to a screen corner/edge/center, so HUD
792
+ * coordinates stay tiny offsets that survive any canvas size: a Label at
793
+ * [-16, 16] under a 'top-right' layer hugs the top-right corner everywhere.
794
+ */
795
+ var UILayer = class extends Node {
796
+ static typeName = "UILayer";
797
+ static props = { anchor: {
798
+ default: "top-left",
799
+ options: ANCHORS
800
+ } };
801
+ anchor = "top-left";
802
+ group = null;
803
+ /** @internal The three group all descendants mount under (lazily created). */
804
+ _ensureGroup() {
805
+ if (!this.group) {
806
+ this.group = new Group();
807
+ this.group.userData.incantoNode = this;
808
+ }
809
+ return this.group;
810
+ }
811
+ /** Loader hook: a misspelled anchor fails at LOAD, listing the valid set. */
812
+ static validateJson(node) {
813
+ const anchor = node.anchor;
814
+ if (!ANCHORS.includes(anchor)) throw new IncantoError("BAD_FORMAT", `UILayer '${node.name}' anchor must be one of [${ANCHORS.join(", ")}], got '${anchor}'.`, {
815
+ prop: "anchor",
816
+ validOptions: ANCHORS
817
+ });
818
+ }
819
+ /** @internal Anchor origin in y-down ui px for a given ui size. */
820
+ _anchorOrigin(width, height) {
821
+ const a = this.anchor;
822
+ return {
823
+ x: a.includes("right") ? width : a === "top" || a === "center" || a === "bottom" ? width / 2 : 0,
824
+ y: a.includes("bottom") ? height : a === "left" || a === "center" || a === "right" ? height / 2 : 0
825
+ };
826
+ }
827
+ };
828
+ //#endregion
829
+ //#region src/2d/register.ts
830
+ /**
831
+ * Register the 2D node taxonomy (and the core nodes). Call once in your game
832
+ * entry before loading a 2D scene. Explicit — never an import side effect.
833
+ */
834
+ function registerNodes2D() {
835
+ registerCoreNodes();
836
+ registerNode(Node2D);
837
+ registerNode(Sprite2D);
838
+ registerNode(ColorRect2D);
839
+ registerNode(Particles2D);
840
+ registerNode(AnimatedSprite2D);
841
+ registerNode(Camera2D);
842
+ registerNode(Label);
843
+ registerNode(UILayer);
844
+ registerNode(StaticBody2D);
845
+ registerNode(RigidBody2D);
846
+ registerNode(Area2D);
847
+ registerNode(CharacterBody2D);
848
+ registerNode(CharacterController2D);
849
+ }
850
+ //#endregion
851
+ export { ColorRect2D as a, AnimatedSprite2D as c, CharacterBody2D as d, PhysicsBody2D as f, validateCollider2D as g, Node2D as h, Label as i, Sprite2D as l, StaticBody2D as m, UILayer as n, CharacterController2D as o, RigidBody2D as p, Particles2D as r, Camera2D as s, registerNodes2D as t, Area2D as u };