incanto 0.69.0 → 0.70.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 (146) hide show
  1. package/bin/_behaviors-loader.mjs +22 -0
  2. package/bin/_read-json.mjs +28 -0
  3. package/bin/incanto-assets.mjs +5 -4
  4. package/bin/incanto-check.mjs +58 -31
  5. package/bin/incanto-editor.mjs +128 -5
  6. package/bin/incanto-env.mjs +3 -2
  7. package/bin/incanto-feel.mjs +24 -13
  8. package/bin/incanto-frame.mjs +8 -1
  9. package/bin/incanto-multiplay.mjs +11 -9
  10. package/bin/incanto-new.mjs +105 -10
  11. package/bin/incanto-play.mjs +158 -16
  12. package/bin/incanto-playtest.mjs +66 -23
  13. package/bin/incanto-skills.mjs +14 -2
  14. package/bin/incanto-verify.mjs +125 -44
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +281 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +7 -7
  20. package/dist/{agent8-CvsfVskX.js → agent8-CmNF01gA.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-DaMxqfNE.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-DZExDn9o.d.ts} +744 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-Bwvh6q8A.js} +142 -59
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-C7ffQWW7.js} +97 -47
  25. package/dist/debug.d.ts +1 -1
  26. package/dist/debug.js +2 -3
  27. package/dist/diagnostics-Cu85N3tL.d.ts +12 -0
  28. package/dist/{editor-switch-CnIOiyNJ.d.ts → editor-switch-CFU9mCec.d.ts} +22 -13
  29. package/dist/editor.js +1065 -863
  30. package/dist/env.d.ts +1 -1
  31. package/dist/env.js +5 -3
  32. package/dist/{environment-presets-BO-7XoFV.js → environment-presets-D6Q5BxeE.js} +298 -45
  33. package/dist/{gameplay-D1KixWK8.js → gameplay-BfHkuzVb.js} +809 -211
  34. package/dist/gameplay.d.ts +187 -7
  35. package/dist/gameplay.js +1 -1
  36. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  37. package/dist/index.d.ts +156 -13
  38. package/dist/index.js +10 -12
  39. package/dist/json-CfTjpvW8.js +67 -0
  40. package/dist/{loader-DadV2QCT.d.ts → loader-Cff09LMm.d.ts} +2 -2
  41. package/dist/net.d.ts +27 -3
  42. package/dist/net.js +2 -2
  43. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  44. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-CE0Qvy3V.js} +136 -11
  45. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CpH-2gn5.js} +104 -23
  46. package/dist/{teardown-CMqFVxO9.js → picking-CQJ_PJKh.js} +103 -12
  47. package/dist/react.d.ts +2 -2
  48. package/dist/react.js +2 -2
  49. package/dist/{register-mmfugpaQ.js → register-6DYnKZcy.js} +546 -817
  50. package/dist/{register-DbVArsns.js → register-Bkk0wSDB.js} +332 -23
  51. package/dist/{replay-BEHLDcQV.js → replay-DjAkAzMq.js} +224 -14
  52. package/dist/{replay-BQfkX4x7.d.ts → replay-Dmw-PKQu.d.ts} +20 -3
  53. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  54. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  55. package/dist/{loader-DZE7B07H.js → save-slots-BXVg148r.js} +4570 -2322
  56. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen--k-XpBjr.d.ts} +36 -4
  57. package/dist/{split-screen-CauMwu6y.js → split-screen-PL78oVXP.js} +159 -26
  58. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  59. package/dist/{src-CYEu-_S9.js → src-Cxfiv1Hg.js} +2 -17
  60. package/dist/test-iHYVUcDK.js +4036 -0
  61. package/dist/test.d.ts +531 -30
  62. package/dist/test.js +3 -3
  63. package/dist/touch-BnCyPA0G.js +519 -0
  64. package/dist/vite.d.ts +20 -37
  65. package/dist/vite.js +176 -6
  66. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  67. package/editor/assets/agent8-D0MS174y.js +1 -0
  68. package/editor/assets/{debug-CMULinBW.js → debug-BnXkKuYu.js} +2 -2
  69. package/editor/assets/index-CIu3uc3l.js +11046 -0
  70. package/editor/index.html +1 -1
  71. package/package.json +4 -14
  72. package/schemas/scene.schema.json +30 -3
  73. package/skills/incanto-3d-character.md +14 -1
  74. package/skills/incanto-3d-models.md +12 -0
  75. package/skills/incanto-assets.md +9 -1
  76. package/skills/incanto-audio.md +8 -2
  77. package/skills/incanto-behaviors-and-scripts.md +83 -6
  78. package/skills/incanto-building-2d-games.md +77 -8
  79. package/skills/incanto-building-3d-games.md +58 -4
  80. package/skills/incanto-editor.md +46 -7
  81. package/skills/incanto-environment.md +19 -1
  82. package/skills/incanto-game-feel.md +70 -0
  83. package/skills/incanto-gameplay-behaviors.md +109 -19
  84. package/skills/incanto-hud.md +128 -7
  85. package/skills/incanto-localization.md +13 -5
  86. package/skills/incanto-multiplayer.md +83 -3
  87. package/skills/incanto-node-reference.md +222 -58
  88. package/skills/incanto-performance.md +52 -0
  89. package/skills/incanto-physics-and-input.md +123 -24
  90. package/skills/incanto-playtesting.md +78 -2
  91. package/skills/incanto-save-slots.md +161 -6
  92. package/skills/incanto-scene-json-authoring.md +69 -12
  93. package/skills/incanto-verifying-your-game.md +196 -10
  94. package/skills/incanto-web-integration.md +28 -0
  95. package/skills/incanto-your-first-game.md +3 -1
  96. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  97. package/templates-app/beacon-isle-3d/package.json +2 -2
  98. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  99. package/templates-app/beacon-isle-3d/src/game.scene.json +103 -378
  100. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  101. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  102. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  103. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  104. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  105. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  106. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  107. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  108. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  109. package/templates-app/molehill-2d/index.html +73 -0
  110. package/templates-app/molehill-2d/package.json +23 -0
  111. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  112. package/templates-app/molehill-2d/src/game.scene.json +1255 -0
  113. package/templates-app/molehill-2d/src/main.ts +41 -0
  114. package/templates-app/molehill-2d/tsconfig.json +13 -0
  115. package/templates-app/molehill-2d/verify.ts +247 -0
  116. package/templates-app/molehill-2d/vite.config.ts +12 -0
  117. package/templates-app/platformer-2d/package.json +2 -2
  118. package/templates-app/platformer-2d/src/game.scene.json +128 -600
  119. package/templates-app/platformer-2d/src/main.ts +11 -1
  120. package/templates-app/platformer-2d/tsconfig.json +1 -1
  121. package/templates-app/star-survivor/package.json +2 -2
  122. package/templates-app/star-survivor/src/game.scene.json +41 -195
  123. package/templates-app/star-survivor/src/main.ts +11 -1
  124. package/templates-app/star-survivor/tsconfig.json +1 -1
  125. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  126. package/templates-app/tps-3d/package.json +2 -2
  127. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  128. package/templates-app/tps-3d/src/game.scene.json +78 -217
  129. package/templates-app/tps-3d/src/main.ts +13 -2
  130. package/templates-app/tps-3d/tsconfig.json +1 -1
  131. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  132. package/templates-app/village-quest-3d/package.json +2 -2
  133. package/templates-app/village-quest-3d/src/behaviors.ts +21 -0
  134. package/templates-app/village-quest-3d/src/grove.scene.json +54 -221
  135. package/templates-app/village-quest-3d/src/main.ts +11 -1
  136. package/templates-app/village-quest-3d/src/village.scene.json +199 -838
  137. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  138. package/templates-app/village-quest-3d/verify.ts +14 -1
  139. package/dist/duplicate-Dj4L-5AA.js +0 -22
  140. package/dist/json-CwwhxQgb.js +0 -36
  141. package/dist/registry-CF70EArN.js +0 -212
  142. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  143. package/dist/test-D6ISmuFE.js +0 -2383
  144. package/dist/touch-DEAmqGdf.js +0 -225
  145. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  146. package/editor/assets/index-CZIRr5EL.js +0 -10958
@@ -1,10 +1,9 @@
1
- import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-DZE7B07H.js";
2
- import { L as translateOn, t as registerCoreNodes } from "./register-mmfugpaQ.js";
1
+ import { K as Node, M as effectiveOrder, q as diagnose } from "./save-slots-BXVg148r.js";
2
+ import { M as translateOn, t as registerCoreNodes } from "./register-6DYnKZcy.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { t as Rng } from "./rng-DP-SR7eg.js";
5
- import { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
- import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimationAliases, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
7
- import { F as localFromWorld, I as worldPosition } from "./gameplay-D1KixWK8.js";
4
+ import { c as getNodeSchema, h as didYouMean, p as registerNode } from "./touch-BnCyPA0G.js";
5
+ import { i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, r as validateAnimations, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CqR2o3SA.js";
6
+ import { E as effectRng, X as localFromWorld, Z as worldPosition, w as tolerateUnknownAction } from "./gameplay-BfHkuzVb.js";
8
7
  import { t as checkSheetGrid } from "./sheet-grid-Cea343VO.js";
9
8
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
10
9
  //#region src/2d/physics/collider-validate.ts
@@ -14,8 +13,31 @@ import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color
14
13
  * not minutes later when physics starts. An EMPTY collider is fine: a body
15
14
  * without a collider is a work-in-progress node, not an error.
16
15
  */
16
+ /**
17
+ * Every key a 2D collider may carry.
18
+ *
19
+ * The shape was checked thoroughly and everything else let through, so
20
+ * `{"shape": "circle", "radius": 8, "offest": [0, -24]}` loaded clean with its
21
+ * hitbox at the body's origin — the exact defect `verify:playable` was written
22
+ * for after it found hitboxes 155 px behind their own bodies in a starter.
23
+ */
24
+ const COLLIDER_KEYS_2D = [
25
+ "shape",
26
+ "size",
27
+ "radius",
28
+ "height",
29
+ "offset",
30
+ "oneWay"
31
+ ];
17
32
  function validateCollider2D(collider, nodeName) {
18
33
  const shape = collider.shape;
34
+ for (const key of Object.keys(collider)) {
35
+ if (COLLIDER_KEYS_2D.includes(key)) continue;
36
+ throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}' has an unknown key "${key}".` + didYouMean(key, COLLIDER_KEYS_2D) + ` A collider is [${COLLIDER_KEYS_2D.join(", ")}] — friction, restitution and mass are props of the BODY, not of its collider.`, {
37
+ prop: key,
38
+ validOptions: [...COLLIDER_KEYS_2D]
39
+ });
40
+ }
19
41
  if (shape === void 0) return;
20
42
  if (collider.oneWay !== void 0 && typeof collider.oneWay !== "boolean") throw new IncantoError("BAD_FORMAT", `Collider on '${nodeName}': "oneWay" must be a boolean (platforms you jump up through), got ${JSON.stringify(collider.oneWay)}.`);
21
43
  if (shape === "rect") {
@@ -227,7 +249,7 @@ var Area2D = class extends PhysicsBody2D {
227
249
  * Who is inside RIGHT NOW — the standing answer behind the enter/exit pair.
228
250
  * See `Area3D.overlapping`. Empty in a scene with no physics world.
229
251
  */
230
- overlapping(group) {
252
+ overlapping(group = void 0) {
231
253
  const all = this._physics?.overlapping(this) ?? [];
232
254
  return group === void 0 ? all : all.filter((n) => n.isInGroup(group));
233
255
  }
@@ -241,7 +263,8 @@ var RigidBody2D = class extends PhysicsBody2D {
241
263
  fixedRotation: { default: false },
242
264
  friction: { default: .5 },
243
265
  restitution: { default: 0 },
244
- linearVelocity: { default: [0, 0] }
266
+ linearVelocity: { default: [0, 0] },
267
+ angularVelocity: { default: 0 }
245
268
  };
246
269
  mass = 1;
247
270
  gravityScale = 1;
@@ -250,6 +273,44 @@ var RigidBody2D = class extends PhysicsBody2D {
250
273
  restitution = 0;
251
274
  /** px/s, y-down. Read back every step; write to launch. */
252
275
  linearVelocity = [0, 0];
276
+ /**
277
+ * rad/s about the z axis — the same sign as `rotation`, which is CLOCKWISE
278
+ * in this y-down space. Read back every step; write to spin.
279
+ *
280
+ * `RigidBody3D` got its `angularVelocity` because "a puzzle builder could
281
+ * neither launch a spinning body nor read how fast a lever was swinging —
282
+ * only its angle, differenced by hand". Same sentence, same defect, other
283
+ * adapter: 2D had `linearVelocity` and no counterpart. `fixedRotation` pins
284
+ * it at zero.
285
+ */
286
+ angularVelocity = 0;
287
+ /**
288
+ * The other name for `linearVelocity`, because it is the name every skill,
289
+ * every sibling node and every other engine uses.
290
+ *
291
+ * `body.velocity = [0, -400]` is what `incanto-physics-and-input.md` taught
292
+ * for years. On a plain TS class that assignment SUCCEEDS — it just writes a
293
+ * stray field nothing reads — so a jump that never happened produced no
294
+ * error, no warning and nothing to search for. `CharacterBody2D.velocity`
295
+ * next to `RigidBody2D.linearVelocity` is the kind of near-miss a builder
296
+ * cannot be expected to notice.
297
+ *
298
+ * Scene JSON still names the prop `linearVelocity` and rejects `velocity`
299
+ * loudly, with the valid keys listed — a hard error corrects itself.
300
+ */
301
+ get velocity() {
302
+ return this.linearVelocity;
303
+ }
304
+ set velocity(v) {
305
+ this.linearVelocity = v;
306
+ }
307
+ /**
308
+ * Impulse in px·kg/s (y-down) — the 3D twin has had this since it shipped,
309
+ * and calling it here was a TypeError.
310
+ */
311
+ applyImpulse(impulse) {
312
+ this._physics?.applyImpulse(this, impulse);
313
+ }
253
314
  };
254
315
  /**
255
316
  * Kinematic body on Rapier's character controller. Gravity is NOT applied
@@ -261,7 +322,11 @@ var CharacterBody2D = class extends PhysicsBody2D {
261
322
  static props = {
262
323
  velocity: { default: [0, 0] },
263
324
  stickToGround: { default: true },
264
- slopeLimitDeg: { default: 45 }
325
+ slopeLimitDeg: { default: 45 },
326
+ stepHeight: {
327
+ default: 35,
328
+ range: { min: 0 }
329
+ }
265
330
  };
266
331
  /**
267
332
  * Renamed to match the 3D body, where `snapToGround` had to give the name
@@ -275,6 +340,17 @@ var CharacterBody2D = class extends PhysicsBody2D {
275
340
  stickToGround = true;
276
341
  /** Max climbable slope angle. */
277
342
  slopeLimitDeg = 45;
343
+ /**
344
+ * How high a step this body walks up without jumping, in PIXELS.
345
+ *
346
+ * Rapier's character controller does not autostep unless it is asked, and in
347
+ * 2D nothing asked: a ledge of ANY height — measured, ONE pixel — stopped a
348
+ * walking body dead, while the mirrored `CharacterBody3D` has cleared 0.35 m
349
+ * by default since 0.63. The default here is that same 0.35 m at the 2D
350
+ * world's own scale (100 px = 1 m, which is why gravity defaults to 980).
351
+ * `0` restores the old behaviour.
352
+ */
353
+ stepHeight = 35;
278
354
  /** @internal Updated by Physics2D.moveAndSlide. */
279
355
  _grounded = false;
280
356
  moveAndSlide() {
@@ -387,6 +463,10 @@ var Joint2D = class extends Node2D {
387
463
  const a = this.parent;
388
464
  const b = this.getNodeOrNull(this.target);
389
465
  if (!(b instanceof PhysicsBody2D)) throw new IncantoError("NODE_NOT_FOUND", `Joint2D '${this.name}': target '${this.target}' is not a physics body.`);
466
+ if (a === b) throw new IncantoError("BAD_FORMAT", `Joint2D '${this.name}': target '${this.target}' is the SAME body this joint hangs off, so it constrains nothing. A joint is a child of ONE body pointing at the OTHER — name the other body (e.g. "%Crate").`, {
467
+ prop: "target",
468
+ path: this.getPath()
469
+ });
390
470
  return {
391
471
  a,
392
472
  b
@@ -415,7 +495,13 @@ var Sprite2D = class extends Node2D {
415
495
  flipX: { default: false },
416
496
  flipY: { default: false },
417
497
  tint: { default: "#ffffff" },
418
- opacity: { default: 1 }
498
+ opacity: {
499
+ default: 1,
500
+ range: {
501
+ min: 0,
502
+ max: 1
503
+ }
504
+ }
419
505
  };
420
506
  /** `'$assetKey'` texture reference. Empty = hidden. */
421
507
  texture = "";
@@ -479,6 +565,22 @@ var Sprite2D = class extends Node2D {
479
565
  mat.color.set(this.tint);
480
566
  mat.opacity = this.opacity;
481
567
  }
568
+ /**
569
+ * The material is PER NODE and nothing freed it.
570
+ *
571
+ * The geometry is shared for the process (`UNIT_PLANE`, on purpose), but
572
+ * every sprite makes its own `MeshBasicMaterial` — and a 2D game that spawns
573
+ * and frees sprites, which is every 2D game, leaked one per node forever.
574
+ * The TEXTURE is not touched: it belongs to the asset store and is shared by
575
+ * every node that names the same key.
576
+ *
577
+ * `AnimatedSprite2D` extends this, so it is covered by the same override.
578
+ */
579
+ free() {
580
+ (this.quadMesh?.material)?.dispose();
581
+ this.quadMesh = null;
582
+ super.free();
583
+ }
482
584
  };
483
585
  //#endregion
484
586
  //#region src/2d/nodes/animated-sprite-2d.ts
@@ -534,7 +636,7 @@ var AnimatedSprite2D = class extends Sprite2D {
534
636
  this.playing = false;
535
637
  }
536
638
  onReady() {
537
- validateAnimationAliases(this.animations, this.name);
639
+ validateAnimations(this.animations, this.name);
538
640
  if (this.autoplay) this.play(this.autoplay);
539
641
  }
540
642
  update(dt) {
@@ -635,10 +737,19 @@ var Camera2D = class extends Node2D {
635
737
  get effectiveZoom() {
636
738
  return Math.max(.01, this.zoom);
637
739
  }
740
+ /** The `follow` path already reported as untrackable — cleared by a new one. */
741
+ warnedFollow = "";
638
742
  update(dt) {
639
743
  if (!this.follow) return;
640
744
  const target = this.getNodeOrNull(this.follow);
641
- if (!(target instanceof Node2D)) return;
745
+ if (!(target instanceof Node2D)) {
746
+ if (this.warnedFollow !== this.follow) {
747
+ this.warnedFollow = this.follow;
748
+ const what = target ? `a ${target.constructor.typeName ?? "Node"}` : "nothing";
749
+ diagnose(this.tree?.engine ?? null, "warn", `[incanto] ${this.getPath()}: follow '${this.follow}' is ${what}, which has no position for the camera to track — the view will not move. Point it at a Node2D.`);
750
+ }
751
+ return;
752
+ }
642
753
  const [tx = 0, ty = 0] = worldPosition(target);
643
754
  const [px = 0, py = 0] = worldPosition(this);
644
755
  const s = Math.min(Math.max(this.smoothing, 0), .99);
@@ -709,6 +820,12 @@ function clampCentered(v, lo, hi) {
709
820
  * Every one defaults to OFF (0 / 1 / false), so an existing scene behaves
710
821
  * exactly as before until it asks for something.
711
822
  */
823
+ /** What each action prop is called when nobody names it. */
824
+ const DEFAULT_ACTIONS = {
825
+ moveAction: "move",
826
+ jumpAction: "jump",
827
+ dashAction: "dash"
828
+ };
712
829
  var CharacterController2D = class extends Node {
713
830
  static typeName = "CharacterController2D";
714
831
  static props = {
@@ -771,7 +888,10 @@ var CharacterController2D = class extends Node {
771
888
  const body = this.parent;
772
889
  if (!(body instanceof CharacterBody2D)) return;
773
890
  const input = engine.input;
774
- const dir = input.getVector(this.moveAction);
891
+ const dir = this.readAction("moveAction", () => input.getVector(this.moveAction), {
892
+ x: 0,
893
+ y: 0
894
+ }, "the character will not move");
775
895
  if (this.mode === "topDown") {
776
896
  body.velocity = [dir.x * this.maxSpeed, dir.y * this.maxSpeed];
777
897
  body.moveAndSlide();
@@ -788,7 +908,7 @@ var CharacterController2D = class extends Node {
788
908
  this.setState("dash");
789
909
  return;
790
910
  }
791
- if (this.dashSpeed > 0 && input.justPressed(this.dashAction)) {
911
+ if (this.dashSpeed > 0 && this.readAction("dashAction", () => input.justPressed(this.dashAction), false, "the character will never dash")) {
792
912
  this.dashLeft = this.dashSeconds;
793
913
  this.dashDir = dir.x !== 0 ? Math.sign(dir.x) : this.dashDir;
794
914
  body.velocity[0] = this.dashDir * this.dashSpeed;
@@ -805,9 +925,10 @@ var CharacterController2D = class extends Node {
805
925
  this.coyoteLeft = Math.max(0, this.coyoteLeft - dt);
806
926
  if (this.coyoteLeft <= 0 && this.jumpsUsed === 0) this.jumpsUsed = 1;
807
927
  }
808
- if (input.justPressed(this.jumpAction)) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
928
+ const jumpEdge = this.readAction("jumpAction", () => input.justPressed(this.jumpAction), false, "the character will never jump");
929
+ if (jumpEdge) this.bufferLeft = Math.max(this.jumpBufferSeconds, 0);
809
930
  else this.bufferLeft = Math.max(0, this.bufferLeft - dt);
810
- const wantsJump = this.bufferLeft > 0 || input.justPressed(this.jumpAction);
931
+ const wantsJump = this.bufferLeft > 0 || jumpEdge;
811
932
  const onWall = this.wallSlideSpeed > 0 || this.wallJumpImpulse.some((v) => v !== 0) ? body.isOnWall() : false;
812
933
  if (wantsJump && onWall && !grounded && this.wallJumpImpulse.some((v) => v !== 0)) {
813
934
  const away = -body.wallSide();
@@ -827,7 +948,8 @@ var CharacterController2D = class extends Node {
827
948
  this.rising = false;
828
949
  } else {
829
950
  body.velocity[1] = (body.velocity[1] ?? 0) + gravityY * dt;
830
- if (this.rising && this.jumpCutMultiplier < 1 && !input.isPressed(this.jumpAction)) {
951
+ const jumpHeld = this.readAction("jumpAction", () => input.isPressed(this.jumpAction), false, "the character will never jump");
952
+ if (this.rising && this.jumpCutMultiplier < 1 && !jumpHeld) {
831
953
  body.velocity[1] = (body.velocity[1] ?? 0) * this.jumpCutMultiplier;
832
954
  this.rising = false;
833
955
  }
@@ -854,6 +976,38 @@ var CharacterController2D = class extends Node {
854
976
  this.state = next;
855
977
  this.emit("movementStateChanged", next);
856
978
  }
979
+ /** Actions already reported missing, so a per-frame read says it once. */
980
+ reportedActions = /* @__PURE__ */ new Set();
981
+ /**
982
+ * Read an input action this controller is allowed to be missing.
983
+ *
984
+ * The 3D controller has done this since `optional-action.ts` was written —
985
+ * whose own docstring says "`CharacterController3D` and `GameFlow` wrap every
986
+ * input read" — and the 2D one did not. So a top-down 2D game, which is the
987
+ * first thing anyone builds with this node and has no jump key, declared
988
+ * `move` and got:
989
+ *
990
+ * ```
991
+ * [incanto] /Game/Player/Ctl (CharacterController2D) threw in fixedUpdate —
992
+ * this node is now SKIPPED. IncantoError: Unknown input action 'jump'.
993
+ * Declared actions: [move].
994
+ * ```
995
+ *
996
+ * SKIPPED: the character could not move either, and every tool reported the
997
+ * game as `✗ error in 1/1` with the reason only in the log. Tolerated because
998
+ * a game with no jump key is the ordinary case; reported because otherwise a
999
+ * TYPO in `"jumpAction"` looks exactly like a deliberate omission.
1000
+ */
1001
+ readAction(prop, read, fallback, consequence) {
1002
+ const action = this[prop];
1003
+ return tolerateUnknownAction({
1004
+ engine: this.tree?.engine,
1005
+ who: this.getPath(),
1006
+ reported: this.reportedActions,
1007
+ consequence,
1008
+ authored: action !== DEFAULT_ACTIONS[prop]
1009
+ }, action, read, fallback);
1010
+ }
857
1011
  };
858
1012
  //#endregion
859
1013
  //#region src/2d/nodes/color-rect-2d.ts
@@ -869,7 +1023,13 @@ var ColorRect2D = class extends Node2D {
869
1023
  static props = {
870
1024
  size: { default: [100, 100] },
871
1025
  color: { default: "#ffffff" },
872
- opacity: { default: 1 },
1026
+ opacity: {
1027
+ default: 1,
1028
+ range: {
1029
+ min: 0,
1030
+ max: 1
1031
+ }
1032
+ },
873
1033
  anchor: { default: [.5, .5] }
874
1034
  };
875
1035
  /** [width, height] in world px. */
@@ -905,6 +1065,24 @@ var ColorRect2D = class extends Node2D {
905
1065
  mat.color.set(this.color);
906
1066
  mat.opacity = this.opacity;
907
1067
  }
1068
+ /**
1069
+ * The material is PER NODE and nothing freed it.
1070
+ *
1071
+ * The geometry is shared for the process (`UNIT_PLANE`, on purpose — a
1072
+ * `THREE.Sprite` cannot be instanced and this is the discipline that replaces
1073
+ * it), but every rect makes its own `MeshBasicMaterial`, and a 2D game that
1074
+ * spawns and frees — bullets, coins, damage flashes, a level swap — leaks one
1075
+ * GPU program's worth of state per node, forever.
1076
+ *
1077
+ * Four 3D nodes had exactly this and were fixed. These two 2D siblings make a
1078
+ * material on the same line of the same kind of `_quad()` helper, and were
1079
+ * not on that list.
1080
+ */
1081
+ free() {
1082
+ (this.quadMesh?.material)?.dispose();
1083
+ this.quadMesh = null;
1084
+ super.free();
1085
+ }
908
1086
  };
909
1087
  //#endregion
910
1088
  //#region src/2d/nodes/label.ts
@@ -1074,8 +1252,20 @@ var Particles2D = class Particles2D extends Node2D {
1074
1252
  colorStart: { default: "#ffffff" },
1075
1253
  colorEnd: { default: "#ffffff" },
1076
1254
  paletteColors: { default: [] },
1077
- alphaStart: { default: 1 },
1078
- alphaEnd: { default: 0 },
1255
+ alphaStart: {
1256
+ default: 1,
1257
+ range: {
1258
+ min: 0,
1259
+ max: 1
1260
+ }
1261
+ },
1262
+ alphaEnd: {
1263
+ default: 0,
1264
+ range: {
1265
+ min: 0,
1266
+ max: 1
1267
+ }
1268
+ },
1079
1269
  shimmer: { default: 0 },
1080
1270
  blend: {
1081
1271
  default: "add",
@@ -1138,7 +1328,7 @@ var Particles2D = class Particles2D extends Node2D {
1138
1328
  _ensureSim() {
1139
1329
  if (!this.sim) {
1140
1330
  applyParticlePreset(this, this.preset, getNodeSchema(Particles2D.typeName));
1141
- const rng = this.tree?.engine?.rng ?? new Rng(4660);
1331
+ const rng = effectRng(this);
1142
1332
  this.sim = new ParticleSim({
1143
1333
  rate: this.emitting ? this.rate : 0,
1144
1334
  lifetime: [this.lifetime[0] ?? 1, this.lifetime[1] ?? 1],
@@ -1244,6 +1434,7 @@ var Particles2D = class Particles2D extends Node2D {
1244
1434
  kind,
1245
1435
  name: this.preset,
1246
1436
  from: this.getPath(),
1437
+ wiredAs: this.wiringPath(),
1247
1438
  amount,
1248
1439
  at: engine.time
1249
1440
  });
@@ -1343,6 +1534,37 @@ const colorScratchC = new Color();
1343
1534
  * - number rows: passed through (`-1` = empty).
1344
1535
  * Ragged rows are right-padded with empty.
1345
1536
  */
1537
+ /**
1538
+ * A legend entry is a tile INDEX, and anything else is a level with no floor.
1539
+ *
1540
+ * `legend` maps a char to a number; solidity is the SEPARATE `solid` prop, a
1541
+ * list of indices. Every other tilemap format in the world puts them together,
1542
+ * so the natural guess is
1543
+ *
1544
+ * ```jsonc
1545
+ * "legend": { "#": { "solid": true, "color": "#4a5568" } } // WRONG, and silent
1546
+ * ```
1547
+ *
1548
+ * and it loaded clean: `legend[ch]` was not `undefined`, so the object went
1549
+ * straight into a `number[]`. `solid.includes(thatObject)` is false, so the
1550
+ * ground had no colliders — measured on a scene authored exactly that way, two
1551
+ * crates fell from y=120 to y=1859 through a map that `incanto-check` had just
1552
+ * called valid.
1553
+ *
1554
+ * An open dictionary beside a closed one: the loader validates the PROP names
1555
+ * hard and never looked inside this one.
1556
+ */
1557
+ function tileIndex(ch, value) {
1558
+ if (typeof value === "number" && Number.isInteger(value) && value >= -1) return value;
1559
+ throw new IncantoError("PROP_TYPE_MISMATCH", `TileMap2D: legend entry '${ch}' must be a tile INDEX (a whole number, -1 for empty), got ${describeLegendValue(value)}. Solidity is the separate "solid" prop — a list of tile indices: "legend": { "${ch}": 0 }, "solid": [0].`, { prop: "legend" });
1560
+ }
1561
+ function describeLegendValue(value) {
1562
+ if (value === null) return "null";
1563
+ if (Array.isArray(value)) return `an array of ${value.length}`;
1564
+ if (typeof value === "object") return `an object (keys: ${Object.keys(value).join(", ")})`;
1565
+ if (typeof value === "number") return `${value}`;
1566
+ return `a ${typeof value}`;
1567
+ }
1346
1568
  function parseCells(cells, legend) {
1347
1569
  const rows = [];
1348
1570
  let width = 0;
@@ -1350,7 +1572,7 @@ function parseCells(cells, legend) {
1350
1572
  const out = [];
1351
1573
  if (typeof row === "string") for (const ch of row) {
1352
1574
  const mapped = legend[ch];
1353
- if (mapped !== void 0) out.push(mapped);
1575
+ if (mapped !== void 0) out.push(tileIndex(ch, mapped));
1354
1576
  else if (ch === "." || ch === " ") out.push(-1);
1355
1577
  else if (ch >= "0" && ch <= "9") out.push(ch.charCodeAt(0) - 48);
1356
1578
  else throw new IncantoError("BAD_FORMAT", `TileMap2D: unknown tile char '${ch}' — add it to "legend" (char → tile index), or use '.'/' ' for empty and digits 0-9.`);
@@ -1433,7 +1655,13 @@ var TileMap2D = class extends Node2D {
1433
1655
  legend: { default: {} },
1434
1656
  /** Tile indices that get merged static colliders. */
1435
1657
  solid: { default: [] },
1436
- opacity: { default: 1 }
1658
+ opacity: {
1659
+ default: 1,
1660
+ range: {
1661
+ min: 0,
1662
+ max: 1
1663
+ }
1664
+ }
1437
1665
  };
1438
1666
  texture = "";
1439
1667
  tileSize = 32;
@@ -1485,6 +1713,87 @@ var TileMap2D = class extends Node2D {
1485
1713
  grid() {
1486
1714
  return parseCells(this._cells, this._legend);
1487
1715
  }
1716
+ /**
1717
+ * Which CELL is under this world point — `null` outside the grid.
1718
+ *
1719
+ * The one question a tile game asks constantly and this node could not
1720
+ * answer. `VoxelGrid3D`, the 3D grid, has had `tileAt` since it was written;
1721
+ * the 2D one shipped `grid()` and nothing else, so every mouse-driven tile
1722
+ * game hand-rolled the same arithmetic with the same two traps in it: cell
1723
+ * (0,0) hangs its TOP-LEFT on the origin (so a cell's CENTRE is half a tile
1724
+ * further on), and the origin is the node's WORLD position, not its `position`
1725
+ * prop.
1726
+ *
1727
+ * Composes ancestor POSITIONS, like every other world query in the engine
1728
+ * (`Terrain3D.heightAt`, `Water3D`); `auditScene` reports an ancestor that
1729
+ * scales or rotates one of these, because that moves the picture and not the
1730
+ * answer.
1731
+ */
1732
+ cellAt(worldX, worldY) {
1733
+ const at = worldPosition(this);
1734
+ const [tw, th] = this.scaledTile();
1735
+ if (!(tw > 0) || !(th > 0)) return null;
1736
+ const cx = Math.floor((worldX - (at[0] ?? 0)) / tw);
1737
+ const cy = Math.floor((worldY - (at[1] ?? 0)) / th);
1738
+ const row = this.grid()[cy];
1739
+ if (!row || cx < 0 || cx >= row.length) return null;
1740
+ return [cx, cy];
1741
+ }
1742
+ /**
1743
+ * One cell's size in WORLD pixels — `tileSize` times this node's own scale.
1744
+ *
1745
+ * `scale` is an ordinary `Node2D` prop, the renderer scales the mesh by it,
1746
+ * and `rebuildColliders` has always rebuilt when it changes — so at
1747
+ * `scale: [2, 2]` a tile is 64 px wide on screen AND in physics. These
1748
+ * queries divided by 32 and answered the wrong cell, in silence, which is the
1749
+ * picture and the collider agreeing and the decider disagreeing with both.
1750
+ *
1751
+ * Ancestor scale is not composed here, for the same reason `worldPosition`
1752
+ * does not: every world query in the engine reads its own props and composes
1753
+ * ancestor POSITIONS, and `auditScene` reports an ancestor that scales one.
1754
+ */
1755
+ scaledTile() {
1756
+ const [sx = 1, sy = 1] = this.scale;
1757
+ return [this.tileSize * Math.abs(sx), this.tileSize * Math.abs(sy)];
1758
+ }
1759
+ /** The atlas index at a cell — `-1` for an empty cell or one off the grid. */
1760
+ tileAt(cx, cy) {
1761
+ return this.grid()[cy]?.[cx] ?? -1;
1762
+ }
1763
+ /**
1764
+ * The CENTRE of a cell, in world pixels — for putting a highlight, a marker
1765
+ * or a spawned enemy on a tile.
1766
+ *
1767
+ * The centre rather than the corner, because that is where a thing goes and
1768
+ * because the corner is the half-tile every hand-rolled version forgets.
1769
+ */
1770
+ worldAt(cx, cy) {
1771
+ const at = worldPosition(this);
1772
+ const [tw, th] = this.scaledTile();
1773
+ return [(at[0] ?? 0) + cx * tw + tw / 2, (at[1] ?? 0) + cy * th + th / 2];
1774
+ }
1775
+ /**
1776
+ * Write ONE cell, by the character the row holds — dig a tile out, drop a
1777
+ * wall in, flip a switch.
1778
+ *
1779
+ * `cells` are strings and a mutation is not watched, so the documented way to
1780
+ * change a map was to rebuild the whole row by hand
1781
+ * (`row.slice(0, x) + c + row.slice(x + 1)`) and reassign the array. This is
1782
+ * that, once, correctly, and it rebuilds the geometry and the colliders.
1783
+ *
1784
+ * A CHARACTER, not an index: `legend` may map several characters to one tile,
1785
+ * so an index cannot say which of them you meant. `'.'` empties the cell.
1786
+ * Returns false for a cell off the grid, or a numeric row (those hold indices
1787
+ * — assign the row).
1788
+ */
1789
+ setTile(cx, cy, char) {
1790
+ const row = this._cells[cy];
1791
+ if (typeof row !== "string" || cx < 0 || cx >= row.length || char.length !== 1) return false;
1792
+ const next = this._cells.slice();
1793
+ next[cy] = `${row.slice(0, cx)}${char}${row.slice(cx + 1)}`;
1794
+ this.cells = next;
1795
+ return true;
1796
+ }
1488
1797
  onReady() {
1489
1798
  this.rebuildColliders();
1490
1799
  }