incanto 0.61.0 → 0.63.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 (54) hide show
  1. package/bin/incanto-verify.mjs +134 -55
  2. package/dist/2d.d.ts +42 -0
  3. package/dist/2d.js +3 -3
  4. package/dist/3d.d.ts +22 -0
  5. package/dist/3d.js +5 -5
  6. package/dist/{create-game-DH7JI5xx.js → create-game-lLeITaZ5.js} +8 -8
  7. package/dist/{create-game-IX5lEH0P.js → create-game-viBqXUoZ.js} +6 -6
  8. package/dist/{duplicate-CGqAmK2h.js → duplicate-BOOKmkQ7.js} +1 -1
  9. package/dist/editor.js +39 -18
  10. package/dist/{environment-presets-CNxCuhZF.js → environment-presets-QR7_75KJ.js} +5 -3
  11. package/dist/{gameplay-Dtzd2itW.js → gameplay-CuqoHHUB.js} +153 -16
  12. package/dist/gameplay.js +1 -1
  13. package/dist/index.d.ts +92 -1
  14. package/dist/index.js +6 -6
  15. package/dist/{loader-D7jTvDQv.js → loader-zDynoew_.js} +160 -2
  16. package/dist/net.js +1 -1
  17. package/dist/{physics-2d-CBnor8Zf.js → physics-2d-CllJXlic.js} +11 -3
  18. package/dist/{physics-3d-BTUfUSWO.js → physics-3d-BL_pFJ19.js} +123 -17
  19. package/dist/react.js +1 -1
  20. package/dist/{register-DL3izw8j.js → register-1cKM8DEj.js} +60 -9
  21. package/dist/{register-xuSRyD6b.js → register-Btm7_Emq.js} +158 -2
  22. package/dist/{replay-D7-yle3s.js → replay-Bhdntkvq.js} +15 -2
  23. package/dist/{split-screen-5Ban4q4n.js → split-screen-Dx0LvzqS.js} +2 -2
  24. package/dist/{src-DozXvyZS.js → src-BlV9Sv4m.js} +1 -1
  25. package/dist/{teardown-B6rwJOyS.js → teardown-BwhkcNt8.js} +1 -1
  26. package/dist/{test-Dch_7VQD.js → test-Ct1_zf5S.js} +64 -20
  27. package/dist/test.d.ts +41 -0
  28. package/dist/test.js +2 -2
  29. package/dist/vite.js +2 -2
  30. package/editor/assets/{agent8-CF1JL2tR.js → agent8-CLZXBRoM.js} +1 -1
  31. package/editor/assets/{debug-3QzYhOPA.js → debug-eaSKFAKW.js} +1 -1
  32. package/editor/assets/{index-CAD2c5ug.js → index-D66AuRwG.js} +91 -91
  33. package/editor/index.html +1 -1
  34. package/package.json +1 -1
  35. package/schemas/scene.schema.json +91 -0
  36. package/skills/README.md +4 -0
  37. package/skills/incanto-behaviors-and-scripts.md +37 -0
  38. package/skills/incanto-building-2d-games.md +20 -1
  39. package/skills/incanto-building-3d-games.md +1 -1
  40. package/skills/incanto-game-feel.md +3 -2
  41. package/skills/incanto-gameplay-behaviors.md +45 -6
  42. package/skills/incanto-node-reference.md +12 -0
  43. package/skills/incanto-physics-and-input.md +53 -2
  44. package/skills/incanto-scene-json-authoring.md +15 -1
  45. package/skills/incanto-verifying-your-game.md +31 -1
  46. package/skills/incanto-your-first-game.md +237 -0
  47. package/templates-app/beacon-isle-3d/package.json +1 -1
  48. package/templates-app/platformer-2d/package.json +1 -1
  49. package/templates-app/star-survivor/package.json +1 -1
  50. package/templates-app/star-survivor/src/game.scene.json +4 -2
  51. package/templates-app/tps-3d/package.json +1 -1
  52. package/templates-app/tps-3d/src/game.scene.json +3 -2
  53. package/templates-app/tps-3d/verify.ts +38 -0
  54. package/templates-app/village-quest-3d/package.json +1 -1
@@ -1,7 +1,7 @@
1
- import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-D7jTvDQv.js";
1
+ import { O as Node, P as Behavior, _ as SaveSlots, k as diagnose, n as loadScene, w as effectiveOrder, z as registerBehavior } from "./loader-zDynoew_.js";
2
2
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
3
3
  import { t as jsonClone } from "./json-CwwhxQgb.js";
4
- import { t as duplicateNode } from "./duplicate-CGqAmK2h.js";
4
+ import { t as duplicateNode } from "./duplicate-BOOKmkQ7.js";
5
5
  import { Color, CubeCamera, DepthTexture, DoubleSide, Euler, FloatType, Frustum, HalfFloatType, LinearMipmapLinearFilter, MathUtils, Matrix4, Mesh, MeshDepthMaterial, Object3D, PerspectiveCamera, Plane, PlaneGeometry, Quaternion, ShaderMaterial, Sphere, Vector2, Vector3, Vector4, WebGLCubeRenderTarget, WebGLRenderTarget } from "three";
6
6
  //#region src/3d/frustum.ts
7
7
  const scratchFrustum = new Frustum();
@@ -2582,7 +2582,11 @@ var CharacterBody3D = class extends PhysicsBody3D {
2582
2582
  0
2583
2583
  ] },
2584
2584
  stickToGround: { default: true },
2585
- slopeLimitDeg: { default: 45 }
2585
+ slopeLimitDeg: { default: 45 },
2586
+ stepHeight: {
2587
+ default: .35,
2588
+ range: { min: 0 }
2589
+ }
2586
2590
  };
2587
2591
  /** m/s, y-up (up = +y). */
2588
2592
  velocity = [
@@ -2604,6 +2608,28 @@ var CharacterBody3D = class extends PhysicsBody3D {
2604
2608
  /** Max climbable slope angle. */
2605
2609
  slopeLimitDeg = 45;
2606
2610
  /**
2611
+ * How high a step this body walks UP without jumping (0 = none).
2612
+ *
2613
+ * Rapier's character controller does not autostep unless asked, and nothing
2614
+ * asked — so every `CharacterBody3D` was stopped dead by any ledge at all.
2615
+ * The player never showed it, because `CharacterController3D` rides a hover
2616
+ * spring rather than the KCC; enemies are the ones that walk.
2617
+ *
2618
+ * Measured on the shipped `tps-3d` template, whose arena has a 0.6 m `Ramp`
2619
+ * between the spawn and the player. Standing still for 45 seconds:
2620
+ *
2621
+ * ```
2622
+ * hits=0 hp=100 closest an enemy ever got = 33.91m
2623
+ * ```
2624
+ *
2625
+ * They chase at the right speed the whole time and pile up against the near
2626
+ * face of a knee-high box, because a chaser walks a straight line and cannot
2627
+ * climb. The template's own playtest had been printing
2628
+ * `danger: the player took damage 0 times in 4 runs — nothing here can hurt
2629
+ * you` all along, and no rung failed on it.
2630
+ */
2631
+ stepHeight = .35;
2632
+ /**
2607
2633
  * `snapToGround` on a character body is ambiguous, so it is refused.
2608
2634
  *
2609
2635
  * A boolean here used to mean the ground-stick, and as placement it means
@@ -2625,6 +2651,51 @@ var CharacterBody3D = class extends PhysicsBody3D {
2625
2651
  }
2626
2652
  };
2627
2653
  //#endregion
2654
+ //#region src/3d/nodes/unscalable.ts
2655
+ /**
2656
+ * Refuse `scale` on a node that keeps a CPU twin of itself.
2657
+ *
2658
+ * `scale` is a plain `Node3D` prop and the 3D skill actively recommends it —
2659
+ * *"resize with `scale`, not `size` — resizing a mesh rebuilds its geometry
2660
+ * every call, scaling is free"*. On a mesh that is true. On `Terrain3D`,
2661
+ * `Water3D` and `River3D` it resizes only the PICTURE: everything the gameplay
2662
+ * layer reads is scale-blind, because it reads `size`.
2663
+ *
2664
+ * - `Terrain3D.heightAt` indexes a heightmap built from `size` and composes
2665
+ * ancestor POSITIONS only
2666
+ * - `buildHeightfieldDesc3D` passes `{x: hm.width, y: 1, z: hm.depth}` with no
2667
+ * scale term
2668
+ * - `Water3D._waterYAt` rejects anything outside `size / 2` of the node
2669
+ *
2670
+ * Measured on `size: [100, 100]` at `scale: [2, 1, 2]`:
2671
+ *
2672
+ * ```
2673
+ * terrain rendered X extent = [-100.0, 100.0] (was [-50, 50])
2674
+ * terrain heightAt(70, 0) = -50.000 (unchanged: the island floor)
2675
+ * water rendered X extent = [-40.0, 40.0]
2676
+ * water _waterYAt(30, 0) = null (unchanged)
2677
+ * ```
2678
+ *
2679
+ * So the player walks off an invisible cliff at ±50 m onto ground that is
2680
+ * visibly there for another 50; drape, spawn placement and terrain-nav all
2681
+ * agree with the collider and disagree with the picture; a scaled water's
2682
+ * underwater fog, splash and `avoidWater` culling stop at half the visible
2683
+ * shoreline. `incanto check` called the scene valid and `auditScene` said
2684
+ * nothing.
2685
+ *
2686
+ * A hard load error rather than a warning, per the engine's own guardrail: the
2687
+ * dial the author wanted exists and is one word away.
2688
+ */
2689
+ function rejectScale(node, reads) {
2690
+ const scale = node.scale;
2691
+ if (!Array.isArray(scale)) return;
2692
+ if (scale.every((v) => Math.abs((v ?? 1) - 1) < 1e-4)) return;
2693
+ const type = node.constructor.typeName ?? "node";
2694
+ const size = node.size;
2695
+ const suggestion = Array.isArray(size) ? `size: [${size.map((v, i) => (v ?? 0) * (scale[i === 1 ? 2 : i] ?? 1)).join(", ")}]` : "size";
2696
+ throw new IncantoError("BAD_FORMAT", `${type} '${node.name}' has scale ${JSON.stringify(scale)}. On this node scale resizes only the MESH — ${reads} read "size" — so the ground you can see would reach past the ground you can stand on. Resize with "${suggestion}" instead.`, { prop: "scale" });
2697
+ }
2698
+ //#endregion
2628
2699
  //#region src/3d/nodes/water-cutout-3d.ts
2629
2700
  /**
2630
2701
  * A box the water does not enter.
@@ -3155,6 +3226,7 @@ var Water3D = class Water3D extends Node3D {
3155
3226
  }
3156
3227
  /** Loader hook: degenerate planes and bad enums fail at LOAD, not at render. */
3157
3228
  static validateJson(node) {
3229
+ rejectScale(node, "the water surface query, underwater fog, splashes and buoyancy all");
3158
3230
  const w = node;
3159
3231
  if (!((w.size[0] ?? 0) > 0) || !((w.size[1] ?? 0) > 0)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' size must be positive [width, depth] meters, got ${JSON.stringify(w.size)}.`, { prop: "size" });
3160
3232
  if (!WATER_QUALITIES.includes(w.quality)) throw new IncantoError("BAD_FORMAT", `Water3D '${node.name}' quality must be one of [${WATER_QUALITIES.join(", ")}], got '${w.quality}'.`, {
@@ -4388,6 +4460,56 @@ function smoothingFactor(smoothing, dt) {
4388
4460
  const s = Math.min(Math.max(smoothing, 0), .99);
4389
4461
  return s <= 0 ? 1 : 1 - s ** (dt * 60);
4390
4462
  }
4463
+ /**
4464
+ * A node's position with every spatial ancestor's added in.
4465
+ *
4466
+ * The tree composes transforms for DRAWING — three does it for free — and every
4467
+ * piece of engine logic that reads `node.position` gets the LOCAL number
4468
+ * instead. Physics already knew (`worldPosition2D`/`worldPosition3D` in the
4469
+ * adapters); the camera did not, and neither did anything that chases a target.
4470
+ *
4471
+ * A camera parented to the player is the Godot and Phaser idiom, and the
4472
+ * authoring skill explicitly permits it — *"EVERY node type can hold children …
4473
+ * any node works as a grouping container"*. Measured on exactly that scene:
4474
+ *
4475
+ * ```
4476
+ * player world position : [1400, 900]
4477
+ * renderer view centre : {"x":0,"y":0}
4478
+ * framing : camera /Level/Player/Cam centred [1400, 900]
4479
+ * 1 in view, 0 outside it
4480
+ * ```
4481
+ *
4482
+ * The renderer drew empty world 1600 px from the player while `framing` — the
4483
+ * instrument the skills tell you to trust before handing a game back —
4484
+ * certified the player on screen.
4485
+ *
4486
+ * Arity comes from the node itself: a 2D node has two components, a 3D node
4487
+ * three, and an ancestor that is neither contributes nothing.
4488
+ */
4489
+ function worldPosition(node) {
4490
+ const own = node.position;
4491
+ if (!Array.isArray(own)) return [];
4492
+ const out = own.slice();
4493
+ for (let at = node.parent; at; at = at.parent) {
4494
+ const p = at.position;
4495
+ if (!Array.isArray(p)) continue;
4496
+ for (let i = 0; i < out.length; i++) out[i] = (out[i] ?? 0) + (p[i] ?? 0);
4497
+ }
4498
+ return out;
4499
+ }
4500
+ /**
4501
+ * Turn a world position into the local one `node.position` has to hold for the
4502
+ * node to END UP there — the inverse of `worldPosition` for its parent chain.
4503
+ */
4504
+ function localFromWorld(node, world) {
4505
+ const out = world.slice();
4506
+ for (let at = node.parent; at; at = at.parent) {
4507
+ const p = at.position;
4508
+ if (!Array.isArray(p)) continue;
4509
+ for (let i = 0; i < out.length; i++) out[i] = (out[i] ?? 0) - (p[i] ?? 0);
4510
+ }
4511
+ return out;
4512
+ }
4391
4513
  //#endregion
4392
4514
  //#region src/gameplay/chase.ts
4393
4515
  /**
@@ -5062,11 +5184,12 @@ var FollowCamera = class extends Behavior {
5062
5184
  if (!hasPosition$1(cam)) return;
5063
5185
  const target = cam.getNodeOrNull(this.target);
5064
5186
  if (!target || !hasPosition$1(target)) return;
5065
- const desired = add(target.position, this.offset);
5066
- if (!(this.deadzone > 0 && distance$1(cam.position, desired) <= this.deadzone)) {
5187
+ const here = worldPosition(cam);
5188
+ const desired = add(worldPosition(target), this.offset);
5189
+ if (!(this.deadzone > 0 && distance$1(here, desired) <= this.deadzone)) {
5067
5190
  const f = smoothingFactor(this.smoothing, dt);
5068
- if (f >= 1) cam.position = [...desired];
5069
- else cam.position = cam.position.map((c, i) => c + ((desired[i] ?? c) - c) * f);
5191
+ if (f >= 1) cam.position = localFromWorld(cam, desired);
5192
+ else cam.position = localFromWorld(cam, here.map((c, i) => c + ((desired[i] ?? c) - c) * f));
5070
5193
  }
5071
5194
  if (this.shakeT > 0) {
5072
5195
  this.shakeT -= dt;
@@ -6069,12 +6192,12 @@ var Projectile = class extends Behavior {
6069
6192
  return [Math.cos(rad), Math.sin(rad)];
6070
6193
  }
6071
6194
  if (Array.isArray(rot)) {
6072
- const yaw = (rot[1] ?? 0) * Math.PI / 180;
6073
- const pitch = (rot[0] ?? 0) * Math.PI / 180;
6195
+ const ry = (rot[1] ?? 0) * Math.PI / 180;
6196
+ const rx = (rot[0] ?? 0) * Math.PI / 180;
6074
6197
  return normalize([
6075
- -Math.sin(yaw) * Math.cos(pitch),
6076
- Math.sin(pitch),
6077
- -Math.cos(yaw) * Math.cos(pitch)
6198
+ Math.sin(ry),
6199
+ -Math.sin(rx) * Math.cos(ry),
6200
+ Math.cos(rx) * Math.cos(ry)
6078
6201
  ]);
6079
6202
  }
6080
6203
  return [1, 0];
@@ -6360,13 +6483,27 @@ var SpawnSource = class {
6360
6483
  * spawner's node), and return the clone.
6361
6484
  *
6362
6485
  * Placement: with no `at`, the clone keeps the template's authored position;
6363
- * with `at`, it spawns at the spawner's position plus `at` (a non-spatial
6364
- * spawner like a plain Node treats `at` as absolute).
6486
+ * with `at`, it ends up at the spawner's position plus `at`.
6487
+ *
6488
+ * `at` is written as the clone's LOCAL position, because the clone is added
6489
+ * as a CHILD of the spawner's node and that parent's transform already
6490
+ * contributes the spawner's own position. This used to add the two, so the
6491
+ * spawner's position was applied twice and the further from the origin you
6492
+ * put the nest, the further away its enemies appeared. Measured on a spawner
6493
+ * at [820, 240] with `"at": [0, -50]`:
6494
+ *
6495
+ * ```
6496
+ * expected world = [820, 190]
6497
+ * clone WORLD position = [1640, 430] error = [820, 240]
6498
+ * ```
6499
+ *
6500
+ * The scene loads, the spawner spawns, the clone is alive and visible — it is
6501
+ * just somewhere else in the level, and nothing has anything to report.
6365
6502
  */
6366
6503
  spawn(template, opts) {
6367
6504
  const clone = duplicateNode(template);
6368
6505
  const at = opts?.at;
6369
- if (at && at.length > 0 && hasPosition$1(clone)) clone.position = add(hasPosition$1(this.behavior.node) ? this.behavior.node.position : [], at);
6506
+ if (at && at.length > 0 && hasPosition$1(clone)) clone.position = add((opts?.parent ?? this.behavior.node) === this.behavior.node || !hasPosition$1(this.behavior.node) ? [] : this.behavior.node.position, at);
6370
6507
  if (typeof clone.visible === "boolean") clone.visible = true;
6371
6508
  (opts?.parent ?? this.behavior.node).addChild(clone);
6372
6509
  return clone;
@@ -6867,4 +7004,4 @@ function registerGameplayBehaviors(opts) {
6867
7004
  for (const [name, ctor] of Object.entries(GAMEPLAY_BEHAVIORS)) registerBehavior(name, ctor, { replace });
6868
7005
  }
6869
7006
  //#endregion
6870
- export { Health as A, StaticBody3D as B, restartScene as C, DayNight as D, FloatAway as E, WaterCutout3D as F, colliderFootDrop as G, validateCollider3D as H, Area3D as I, CharacterBody3D as L, Chase as M, Water3D as N, phaseOf as O, WATER_CUTOUT_MAX as P, PhysicsBody3D as R, goToScene as S, FollowCamera as T, createCausticsQuad as U, Node3D as V, WATER_MAX_RIPPLES as W, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Collector as j, DamageOnContact as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, tolerateUnknownAction as w, GameFlow as x, screenFlash as y, RigidBody3D as z };
7007
+ export { Health as A, CharacterBody3D as B, restartScene as C, DayNight as D, FloatAway as E, Water3D as F, validateCollider3D as G, RigidBody3D as H, WATER_CUTOUT_MAX as I, colliderFootDrop as J, createCausticsQuad as K, WaterCutout3D as L, Chase as M, localFromWorld as N, phaseOf as O, worldPosition as P, rejectScale as R, goToScene as S, FollowCamera as T, StaticBody3D as U, PhysicsBody3D as V, Node3D as W, Cooldown as _, Wander as a, Interactable as b, SavePoint as c, Patrol as d, PathFollow as f, CameraShake as g, Lifetime as h, WaveSpawner as i, Collector as j, DamageOnContact as k, Projectile as l, MoveTo as m, registerGameplayBehaviors as n, Spawner as o, Oscillate as p, WATER_MAX_RIPPLES as q, ZombieAI as r, ScoreKeeper as s, GAMEPLAY_BEHAVIORS as t, Pickup as u, hitStop as v, tolerateUnknownAction as w, GameFlow as x, screenFlash as y, Area3D as z };
package/dist/gameplay.js CHANGED
@@ -1,2 +1,2 @@
1
- import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-Dtzd2itW.js";
1
+ import { A as Health, C as restartScene, D as DayNight, E as FloatAway, M as Chase, O as phaseOf, S as goToScene, T as FollowCamera, _ as Cooldown, a as Wander, b as Interactable, c as SavePoint, d as Patrol, f as PathFollow, g as CameraShake, h as Lifetime, i as WaveSpawner, j as Collector, k as DamageOnContact, l as Projectile, m as MoveTo, n as registerGameplayBehaviors, o as Spawner, p as Oscillate, r as ZombieAI, s as ScoreKeeper, t as GAMEPLAY_BEHAVIORS, u as Pickup, v as hitStop, x as GameFlow, y as screenFlash } from "./gameplay-CuqoHHUB.js";
2
2
  export { CameraShake, Chase, Collector, Cooldown, DamageOnContact, DayNight, FloatAway, FollowCamera, GAMEPLAY_BEHAVIORS, GameFlow, Health, Interactable, Lifetime, MoveTo, Oscillate, PathFollow, Patrol, Pickup, Projectile, SavePoint, ScoreKeeper, Spawner, Wander, WaveSpawner, ZombieAI, goToScene, hitStop, phaseOf, registerGameplayBehaviors, restartScene, screenFlash };
package/dist/index.d.ts CHANGED
@@ -624,6 +624,97 @@ declare class UiDialogue extends HudWidgetBase {
624
624
  override update(dt: number): void;
625
625
  }
626
626
  //#endregion
627
+ //#region src/core/nodes/respawn.d.ts
628
+ /**
629
+ * Catch whatever this node hangs off when it leaves the world, and put it back.
630
+ *
631
+ * Nothing in the engine did this. Not a node, not a behavior, not a scene
632
+ * header — and the shipped examples prove what that costs: with the `plays`
633
+ * rung finally counting defects, `basic-3d-sideview` fails 8 seeded runs out of
634
+ * 8, `water-lake-3d` 7, `water-ocean-3d` 5, `water-pool-3d` 4,
635
+ * `water-river-3d` 1. Every one of them the same way — the player walks off the
636
+ * edge of the terrain and falls forever, and the only fix available was a
637
+ * per-frame `y` check hand-written in TypeScript, which is exactly the kind of
638
+ * thing this engine exists to keep out of TypeScript.
639
+ *
640
+ * A CHILD NODE rather than a Behavior, deliberately: a node holds one behavior
641
+ * and the player's is already spoken for (`PlayerControl`, `Health`, the game's
642
+ * own script). Children stack, the way `CharacterController3D` already hangs
643
+ * off its body.
644
+ *
645
+ * ```jsonc
646
+ * { "name": "Player", "type": "RigidBody3D", "props": { … },
647
+ * "children": [
648
+ * { "name": "Ctl", "type": "CharacterController3D" },
649
+ * { "name": "Catch", "type": "Respawn" } // that is the whole thing
650
+ * ] }
651
+ * ```
652
+ *
653
+ * With no props it guards its parent, uses the same line the playtest's fall
654
+ * detector uses (50 m under the spawn in 3D, 1000 px in 2D), and puts the node
655
+ * back where it started. `respawned` fires each time, so a life can be taken
656
+ * off a `ScoreKeeper` or a `Health` with an ordinary connection — this node
657
+ * does not decide what falling COSTS, which is a game-design question and not
658
+ * a physics one.
659
+ */
660
+ declare class Respawn extends Node {
661
+ static override readonly typeName: string;
662
+ static override readonly signals: readonly string[];
663
+ static readonly props: PropSchema;
664
+ /**
665
+ * Who is being caught. Defaults to the parent, which is the whole point of
666
+ * hanging this off the thing it guards.
667
+ */
668
+ target: string;
669
+ /**
670
+ * The line past which the target has left the world, in the scene's own
671
+ * down: 3D counts down (−y), 2D counts down the screen (+y).
672
+ *
673
+ * `null` is AUTO — the spawn position offset by the same distance the
674
+ * playtest's own fall detector uses, so "the level says it fell" and "the
675
+ * ladder says it fell" cannot disagree. Author a number when the level has a
676
+ * real floor to name; leave it alone otherwise.
677
+ */
678
+ below: number | null;
679
+ /** Where to put it back. Empty = wherever it started. */
680
+ to: number[];
681
+ /**
682
+ * Zero the body's velocity on the way back.
683
+ *
684
+ * On by default because the alternative is invisible: a body respawned with
685
+ * 40 m/s of accumulated fall still in it drops straight back through the
686
+ * line on the next frame, and the level flickers instead of resuming.
687
+ */
688
+ resetVelocity: boolean;
689
+ /** How many times this has caught the target — for HUDs and tests. */
690
+ falls: number;
691
+ private home;
692
+ private line;
693
+ /** +1 when "out" means y greater (2D screen-down), −1 when smaller (3D). */
694
+ private down;
695
+ override onReady(): void;
696
+ private spawnY;
697
+ /**
698
+ * Which way is out, and where the line is.
699
+ *
700
+ * Resolved on the first `update` rather than in `onReady`, because
701
+ * `onReady` runs inside `loadScene` — BEFORE `setScene` attaches the engine —
702
+ * so the scene's `dimension` is not readable there yet. Reading it there
703
+ * silently gives every 2D scene the 3D answer, and a line above a falling
704
+ * player fires on frame one, forever.
705
+ *
706
+ * Not called `resolve`: `Node` has a private method by that name and
707
+ * `getNodeOrNull` calls it, so a subclass method with the same name
708
+ * overrides it on the prototype — `private` is a TypeScript fiction that
709
+ * costs nothing at runtime — and every path lookup this node made returned a
710
+ * number. `node-method-shadowing.test.ts` refuses the whole family now.
711
+ */
712
+ private fallLine;
713
+ /** The node this guards, or itself if the path no longer resolves. */
714
+ private guarded;
715
+ override update(_dt: number): void;
716
+ }
717
+ //#endregion
627
718
  //#region src/core/nodes/timer.d.ts
628
719
  /**
629
720
  * The canonical serializable game clock — never `setTimeout` in game logic.
@@ -1174,4 +1265,4 @@ declare function computeViewport(canvasW: number, canvasH: number, viewport: {
1174
1265
  /** Engine version. Kept in sync with package.json by the release pipeline. */
1175
1266
  declare const VERSION: string;
1176
1267
  //#endregion
1177
- export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, type EffectEvent, type EffectKind, EffectLog, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RefNode, type RefProblem, type RefResult, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
1268
+ export { type AnimationDef, type AnimationEntry, type AssetFailure, AudioBuses, type AudioElementLike, AudioPlayer, BASE_LOCALE, Behavior, type BehaviorCtor, type BehaviorState, type BusName, CONST_REF_KEY, type ComputedViewport, type ConnectionJson, type CrossfadeGains, type DebugLineSource, type DeviceHints, type DriveScript, type DriveStep, type EffectEvent, type EffectKind, EffectLog, Engine, type EngineOptions, type EngineStats, type FindPathOptions, type GameStats, type GroupedLog, type HudAnchor, HudLayer, IncantoError, type IncantoErrorCode, type IncantoErrorDetails, InputMap, type JsonKind, type JsonObject, type JsonValue, type Listener, type LiveInput, type LiveReport, type LoadSceneOptions, type LocaleTables, Localization, type LogEntry, type LogLevel, LogManager, type MusicBackend, MusicManager, type MusicTrack, Node, type NodeCtor, type NodeJson, type NodeLifecycle, ORDER_GROUP_BASE, type OrderGroup, type OrderGroupTable, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, type ParsedNodePath, type ParticlePresetValues, ParticleSim, type ParticleSimConfig, type ParticleView, type PathGrid, type PlayMusicOptions, type PreloadResult, type PropDef, type PropSchema, type QualityTier, ROLLOFF_MODELS, type Recorder, type RefNode, type RefProblem, type RefResult, type RendererStats, type ReplayEvent, type ReplayJson, type ResolvedRendering, type ResolvedViewport, Respawn, type RestoreReport, Rng, type RolloffModel, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, type SaveSlot, SaveSlots, type SaveStore, Scene, type SceneJson, SceneTree, type Scheduler, Settings, type SettingsValues, SfxEngine, type SfxParams, type SfxPlayOptions, type SfxWave, Signal, type SignalListener, type SpatialParams, type SynthOptions, T_PREFIX, Timer, TouchControls, type TouchControlsOptions, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, type Vec3, type ViewportFit, type Voice, type VoicePreset, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
package/dist/index.js CHANGED
@@ -1,13 +1,13 @@
1
- import { A as describeRefProblem, B as registeredBehaviors, C as ORDER_GROUP_BASE, E as InputMap, F as behaviorSchema, I as behaviorSignals, L as clearBehaviors, M as resolveRefInJson, N as parseNodePath, O as Node, P as Behavior, R as getBehavior, S as createSaveStore, T as resolveOrderGroups, V as Signal, _ as SaveSlots, a as serializeNode, b as restoreBehaviors, d as computeViewport, f as resolveViewport, g as resolveConstants, h as isConstRef, i as Scene, j as nodeRefWarnings, m as CONST_REF_KEY, n as loadScene, o as SCENE_FORMAT, p as SceneTree, v as behaviorsWithoutSave, w as effectiveOrder, x as savesWithoutUid, y as captureBehaviors, z as registerBehavior } from "./loader-D7jTvDQv.js";
2
- import { A as UiBar, B as ROLLOFF_MODELS, C as readDeviceHints, D as HudLayer, E as EffectLog, F as suggestLocale, G as sfxDuration, H as spatialPan, J as WebAudioMusicBackend, K as synthSfx, L as translationKey, M as BASE_LOCALE, N as Localization, P as T_PREFIX, R as SfxEngine, T as LogManager, U as SFX_PRESETS, V as spatialGain, W as SFX_PRESET_NAMES, X as fadeGain, Y as crossfadeGains, Z as AudioBuses, _ as Engine, a as UiMuteToggle, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as AudioPlayer, h as UiDialogue, i as UiLanguageSelect, j as UiText, k as UiBanner, l as UiSelect, m as UiButton, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as MusicManager, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, w as suggestQuality, x as qualityEnvironment, y as Settings, z as isAudioContextAvailable } from "./register-xuSRyD6b.js";
1
+ import { A as describeRefProblem, B as registeredBehaviors, C as ORDER_GROUP_BASE, E as InputMap, F as behaviorSchema, I as behaviorSignals, L as clearBehaviors, M as resolveRefInJson, N as parseNodePath, O as Node, P as Behavior, R as getBehavior, S as createSaveStore, T as resolveOrderGroups, V as Signal, _ as SaveSlots, a as serializeNode, b as restoreBehaviors, d as computeViewport, f as resolveViewport, g as resolveConstants, h as isConstRef, i as Scene, j as nodeRefWarnings, m as CONST_REF_KEY, n as loadScene, o as SCENE_FORMAT, p as SceneTree, v as behaviorsWithoutSave, w as effectiveOrder, x as savesWithoutUid, y as captureBehaviors, z as registerBehavior } from "./loader-zDynoew_.js";
2
+ import { A as UiBanner, B as isAudioContextAvailable, D as EffectLog, E as LogManager, F as T_PREFIX, G as SFX_PRESET_NAMES, H as spatialGain, I as suggestLocale, J as MusicManager, K as sfxDuration, M as UiText, N as BASE_LOCALE, O as HudLayer, P as Localization, Q as AudioBuses, R as translationKey, S as qualityEnvironment, T as suggestQuality, U as spatialPan, V as ROLLOFF_MODELS, W as SFX_PRESETS, X as crossfadeGains, Y as WebAudioMusicBackend, Z as fadeGain, _ as AudioPlayer, a as UiMuteToggle, b as Settings, c as UiRenderScaleSelect, d as UiToggle, f as UiVolumeSlider, g as UiDialogue, h as UiButton, i as UiLanguageSelect, j as UiBar, l as UiSelect, m as Respawn, n as UiFrameCapSelect, o as UiPanel, p as Timer, q as synthSfx, r as UiImage, s as UiQualitySelect, t as registerCoreNodes, u as UiSlider, v as Engine, w as readDeviceHints, z as SfxEngine } from "./register-Btm7_Emq.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { t as Rng } from "./rng-DP-SR7eg.js";
5
5
  import { n as jsonEquals, r as jsonKind, t as jsonClone } from "./json-CwwhxQgb.js";
6
6
  import { a as getNodeSignals, c as mergeStaticSignals, i as getNodeSchema, l as registerNode, n as clearRegistry, o as getNodeType, r as createNode, u as registeredTypes } from "./registry-CF70EArN.js";
7
- import { n as startRecording, r as auditScene, t as replay } from "./replay-D7-yle3s.js";
7
+ import { n as startRecording, r as auditScene, t as replay } from "./replay-Bhdntkvq.js";
8
8
  import { a as logReport, i as resolveRendering, n as attachTouchControls, o as logText, r as joystickVector, s as parseDrive, t as TouchControls } from "./touch-DESwnpOc.js";
9
9
  import { t as createNoise2D } from "./noise-CGUMx44x.js";
10
10
  import { a as PARTICLE_PRESETS, i as ParticleSim, n as resolveFrames, o as PARTICLE_PRESET_NAMES, s as applyParticlePreset, t as resolveAnimation } from "./sprite-animation-CY-mrr1L.js";
11
- import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-DozXvyZS.js";
12
- import { t as duplicateNode } from "./duplicate-CGqAmK2h.js";
13
- export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
11
+ import { a as preloadUrls, i as preloadSceneAssets, n as newUid, o as findPath, r as assetUrls, s as gridFromRows, t as VERSION } from "./src-BlV9Sv4m.js";
12
+ import { t as duplicateNode } from "./duplicate-BOOKmkQ7.js";
13
+ export { AudioBuses, AudioPlayer, BASE_LOCALE, Behavior, CONST_REF_KEY, EffectLog, Engine, HudLayer, IncantoError, InputMap, Localization, LogManager, MusicManager, Node, ORDER_GROUP_BASE, PARTICLE_PRESETS, PARTICLE_PRESET_NAMES, ParticleSim, ROLLOFF_MODELS, Respawn, Rng, SCENE_FORMAT, SFX_PRESETS, SFX_PRESET_NAMES, SaveSlots, Scene, SceneTree, Settings, SfxEngine, Signal, T_PREFIX, Timer, TouchControls, UiBanner, UiBar, UiButton, UiDialogue, UiFrameCapSelect, UiImage, UiLanguageSelect, UiMuteToggle, UiPanel, UiQualitySelect, UiRenderScaleSelect, UiSelect, UiSlider, UiText, UiToggle, UiVolumeSlider, VERSION, WebAudioMusicBackend, applyParticlePreset, assetUrls, attachTouchControls, auditScene, behaviorSchema, behaviorSignals, behaviorsWithoutSave, captureBehaviors, clearBehaviors, clearRegistry, computeViewport, createNode, createNoise2D, createSaveStore, crossfadeGains, describeRefProblem, duplicateNode, effectiveOrder, fadeGain, findPath, getBehavior, getNodeSchema, getNodeSignals, getNodeType, gridFromRows, isAudioContextAvailable, isConstRef, joystickVector, jsonClone, jsonEquals, jsonKind, loadScene, logReport, logText, mergeStaticSignals, newUid, nodeRefWarnings, parseDrive, parseNodePath, preloadSceneAssets, preloadUrls, qualityEnvironment, readDeviceHints, registerBehavior, registerCoreNodes, registerNode, registeredBehaviors, registeredTypes, replay, resolveAnimation, resolveConstants, resolveFrames, resolveOrderGroups, resolveRefInJson, resolveRendering, resolveViewport, restoreBehaviors, savesWithoutUid, serializeNode, sfxDuration, spatialGain, spatialPan, startRecording, suggestLocale, suggestQuality, synthSfx, translationKey };
@@ -854,6 +854,92 @@ function nodeTypeMethods(type) {
854
854
  return out;
855
855
  }
856
856
  const methodCache = /* @__PURE__ */ new Map();
857
+ /** Every code the engine can ever receive, keyboard + mouse + gamepad. */
858
+ const INPUT_CODES = new Set([
859
+ ...[..."ABCDEFGHIJKLMNOPQRSTUVWXYZ"].map((c) => `Key${c}`),
860
+ ...Array.from({ length: 10 }, (_, i) => `Digit${i}`),
861
+ ...Array.from({ length: 10 }, (_, i) => `Numpad${i}`),
862
+ "NumpadAdd",
863
+ "NumpadSubtract",
864
+ "NumpadMultiply",
865
+ "NumpadDivide",
866
+ "NumpadDecimal",
867
+ "NumpadEnter",
868
+ ...Array.from({ length: 12 }, (_, i) => `F${i + 1}`),
869
+ "ArrowUp",
870
+ "ArrowDown",
871
+ "ArrowLeft",
872
+ "ArrowRight",
873
+ "Space",
874
+ "Enter",
875
+ "Escape",
876
+ "Tab",
877
+ "Backspace",
878
+ "Delete",
879
+ "Insert",
880
+ "Home",
881
+ "End",
882
+ "PageUp",
883
+ "PageDown",
884
+ "CapsLock",
885
+ "ShiftLeft",
886
+ "ShiftRight",
887
+ "ControlLeft",
888
+ "ControlRight",
889
+ "AltLeft",
890
+ "AltRight",
891
+ "MetaLeft",
892
+ "MetaRight",
893
+ "ContextMenu",
894
+ "Backquote",
895
+ "Minus",
896
+ "Equal",
897
+ "BracketLeft",
898
+ "BracketRight",
899
+ "Backslash",
900
+ "Semicolon",
901
+ "Quote",
902
+ "Comma",
903
+ "Period",
904
+ "Slash",
905
+ "IntlBackslash",
906
+ "IntlRo",
907
+ "IntlYen",
908
+ ...Array.from({ length: 5 }, (_, i) => `Mouse${i}`),
909
+ ...Array.from({ length: 17 }, (_, i) => `Pad${i}`)
910
+ ]);
911
+ /** The one a typo probably meant, or null when nothing is close enough. */
912
+ function nearestCode(written) {
913
+ const lower = written.toLowerCase();
914
+ if (/^[a-z]$/.test(lower)) return `Key${lower.toUpperCase()}`;
915
+ if (/^[0-9]$/.test(written)) return `Digit${written}`;
916
+ if (lower === "shift" || lower === "ctrl" || lower === "control" || lower === "alt") {
917
+ const side = lower === "ctrl" ? "Control" : lower[0]?.toUpperCase() + lower.slice(1);
918
+ return `${side}Left" or "${side}Right`;
919
+ }
920
+ if (lower === "esc") return "Escape";
921
+ if (lower === "return") return "Enter";
922
+ if (lower === "up" || lower === "down" || lower === "left" || lower === "right") return `Arrow${lower[0]?.toUpperCase()}${lower.slice(1)}`;
923
+ let best = null;
924
+ let bestScore = 3;
925
+ for (const code of INPUT_CODES) {
926
+ const d = distance$1(lower, code.toLowerCase());
927
+ if (d < bestScore) {
928
+ bestScore = d;
929
+ best = code;
930
+ }
931
+ }
932
+ return best;
933
+ }
934
+ function distance$1(a, b) {
935
+ const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
936
+ for (let j = 0; j <= b.length; j++) rows[0][j] = j;
937
+ for (let i = 1; i <= a.length; i++) for (let j = 1; j <= b.length; j++) {
938
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
939
+ rows[i][j] = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
940
+ }
941
+ return rows[a.length][b.length];
942
+ }
857
943
  //#endregion
858
944
  //#region src/core/input.ts
859
945
  /** Keys typed into editable elements belong to the page UI, not the game. */
@@ -1322,6 +1408,21 @@ const DIRECTIONS = [
1322
1408
  "left",
1323
1409
  "right"
1324
1410
  ];
1411
+ /**
1412
+ * Refuse a key code the engine can never receive.
1413
+ *
1414
+ * The map is looked up by EXACT string against what the browser hands us, so
1415
+ * `"W"`, `"space"` and `"Shift"` — the three most natural things to write — bind
1416
+ * to nothing at all. They loaded clean, passed `incanto check`, and left the
1417
+ * player without that control, with nothing anywhere to say so.
1418
+ */
1419
+ function checkCodes(action, where, keys) {
1420
+ for (const key of keys) {
1421
+ if (typeof key !== "string" || INPUT_CODES.has(key)) continue;
1422
+ const guess = nearestCode(key);
1423
+ throw new IncantoError("BAD_FORMAT", `Input action '${action}'${where}: '${key}' is not a key code, so nothing can ever press it${guess ? ` — did you mean "${guess}"?` : ""}. Codes are KeyboardEvent.code values (KeyW, Space, ArrowLeft, ShiftLeft), plus Mouse0..4 and Pad0..16.`, { prop: "keys" });
1424
+ }
1425
+ }
1325
1426
  function parseInputDecls(decls) {
1326
1427
  const out = /* @__PURE__ */ new Map();
1327
1428
  for (const [name, raw] of Object.entries(decls)) {
@@ -1330,6 +1431,7 @@ function parseInputDecls(decls) {
1330
1431
  const keys = decl.keys;
1331
1432
  if (!Array.isArray(keys) || keys.some((k) => typeof k !== "string")) throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "keys" must be an array of KeyboardEvent.code strings.`);
1332
1433
  if (decl.touch !== void 0 && decl.touch !== "button") throw new IncantoError("BAD_FORMAT", `Input action '${name}': button "touch" must be 'button', got ${JSON.stringify(decl.touch)}.`);
1434
+ checkCodes(name, "", keys);
1333
1435
  out.set(name, {
1334
1436
  type: "button",
1335
1437
  keys: [...keys],
@@ -1345,6 +1447,7 @@ function parseInputDecls(decls) {
1345
1447
  }
1346
1448
  if (decl.touch !== void 0 && decl.touch !== "joystick") throw new IncantoError("BAD_FORMAT", `Input action '${name}': vector2 "touch" must be 'joystick', got ${JSON.stringify(decl.touch)}.`);
1347
1449
  const d = dirs;
1450
+ for (const dir of DIRECTIONS) checkCodes(name, ` (${dir})`, d[dir]);
1348
1451
  out.set(name, {
1349
1452
  type: "vector2",
1350
1453
  keys: {
@@ -2712,6 +2815,53 @@ function validateStrings(strings) {
2712
2815
  }
2713
2816
  }
2714
2817
  //#endregion
2818
+ //#region src/core/scene/wire-arity.ts
2819
+ /**
2820
+ * A wire that hands a handler fewer arguments than it needs.
2821
+ *
2822
+ * The engine hard-validates node types, prop kinds, every prop enum, node
2823
+ * paths, handler existence and signal declaration — and never that the signal's
2824
+ * ARGUMENTS fit the handler it is wired to. `died` carries none and
2825
+ * `ScoreKeeper.addScore(n)` wants one, so the most natural way to score a kill:
2826
+ *
2827
+ * ```json
2828
+ * { "signal": "died", "from": "Enemy", "to": "Keeper", "handler": "addScore" }
2829
+ * ```
2830
+ *
2831
+ * sets the score to `NaN` on the first kill and the win condition becomes
2832
+ * permanently unreachable. Measured:
2833
+ *
2834
+ * ```
2835
+ * score = NaN | Number.isNaN: true
2836
+ * HUD text = "Kills 0"
2837
+ * won emitted = false
2838
+ * engine errors = 0
2839
+ * incanto-check → 1/1 scene(s) valid
2840
+ * ```
2841
+ *
2842
+ * Two agents building different games from the published package hit it
2843
+ * independently, which is what makes it worth a rule rather than a footnote.
2844
+ *
2845
+ * **Why this is not a load error.** Nothing declares how many arguments a signal
2846
+ * carries — `static signals` is a list of names — so at load the pair cannot be
2847
+ * compared. At the moment the wire FIRES both halves are known exactly, and
2848
+ * that is where this lives.
2849
+ *
2850
+ * **Why `fn.length` is the right measure.** It counts leading parameters
2851
+ * without defaults, so it makes precisely the distinction the engine needs:
2852
+ * `addScore(n)` is 1 and gets reported; `GameFlow.win(text = 'YOU WIN')` is 0
2853
+ * and is silent, because that wire genuinely works — the banner defaults. The
2854
+ * same arg-less signal into a same-arity method is right in one case and wrong
2855
+ * in the other, and only the defaults tell them apart.
2856
+ */
2857
+ function reportShortWire(from, conn, fn, args) {
2858
+ diagnose(from.tree?.engine ?? null, "error", `[incanto] '${conn.signal}' from '${conn.from}' carries ${args.length} argument(s) and '${conn.to}.${conn.handler}' needs ${fn.length}. The missing one(s) arrive as undefined — a number handler gets NaN and never recovers. Give the handler a default, or wire a signal that carries what it needs.`);
2859
+ }
2860
+ /** Does this wire under-feed its handler? */
2861
+ function isShortWire(fn, args) {
2862
+ return args.length < fn.length;
2863
+ }
2864
+ //#endregion
2715
2865
  //#region src/core/scene/loader.ts
2716
2866
  var loader_exports = /* @__PURE__ */ __exportAll({
2717
2867
  buildNodeJson: () => buildNodeJson,
@@ -2920,17 +3070,25 @@ function wireConnections(root, connections, declareSignals = false) {
2920
3070
  const behaviorFn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
2921
3071
  if (typeof nodeFn !== "function" && typeof behaviorFn !== "function" && !(toNode.behavior instanceof StubBehavior)) throw new IncantoError("UNKNOWN_HANDLER", `Connection '${conn.signal}' → '${conn.to}' names handler '${conn.handler}', but '${toNode.name}' has no such method` + (toNode.behavior ? ` (nor does its behavior '${toNode.script?.name}')` : "") + `.`);
2922
3072
  if (ambiguousHandler(toNode, conn.handler)) throw ambiguousHandlerError(toNode, conn.handler, toNode.script?.name, conn.to);
3073
+ let saidShort = false;
3074
+ const feed = (fn, self, args) => {
3075
+ if (!saidShort && isShortWire(fn, args)) {
3076
+ saidShort = true;
3077
+ reportShortWire(fromNode, conn, fn, args);
3078
+ }
3079
+ fn.apply(self, args);
3080
+ };
2923
3081
  const listener = (...args) => {
2924
3082
  if (conn.filter && !matchesFilter(args[0], conn.filter)) return;
2925
3083
  if (conn.once) fromNode.off(conn.signal, listener);
2926
3084
  try {
2927
3085
  const fn = toNode[conn.handler];
2928
3086
  if (typeof fn === "function") {
2929
- fn.apply(toNode, args);
3087
+ feed(fn, toNode, args);
2930
3088
  return;
2931
3089
  }
2932
3090
  const bfn = toNode.behavior ? toNode.behavior[conn.handler] : void 0;
2933
- if (typeof bfn === "function") bfn.apply(toNode.behavior, args);
3091
+ if (typeof bfn === "function") feed(bfn, toNode.behavior, args);
2934
3092
  } catch (error) {
2935
3093
  quarantineWire(fromNode, toNode, conn, listener, error);
2936
3094
  }
package/dist/net.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import { n as createAgent8Server } from "./agent8-CvsfVskX.js";
2
- import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-5Ban4q4n.js";
2
+ import { a as NetworkManager, d as createLocalGameServer, f as LoopbackHub, l as LocalGameServer, n as registerNodesNet, o as applySyncPatch, p as LoopbackTransport, r as NetworkSpawner, t as createSplitScreen, u as LocalGameServerTransport } from "./split-screen-Dx0LvzqS.js";
3
3
  export { LocalGameServer, LocalGameServerTransport, LoopbackHub, LoopbackTransport, NetworkManager, NetworkSpawner, applySyncPatch, createAgent8Server, createLocalGameServer, createSplitScreen, registerNodesNet };
@@ -1,7 +1,7 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { k as diagnose } from "./loader-D7jTvDQv.js";
2
+ import { k as diagnose } from "./loader-zDynoew_.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
- import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-DL3izw8j.js";
4
+ import { _ as RigidBody2D, b as validateCollider2D, g as PhysicsBody2D, h as CharacterBody2D, m as Area2D, p as Joint2D, y as Node2D } from "./register-1cKM8DEj.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
7
  //#region src/2d/physics/physics-2d.ts
@@ -100,7 +100,10 @@ var Physics2D = class {
100
100
  node.position = [t.x * SCALE - ox, t.y * SCALE - oy];
101
101
  }
102
102
  if (node instanceof RigidBody2D) {
103
- if (!node.fixedRotation) node.rotation = e.body.rotation() / DEG;
103
+ if (!node.fixedRotation) {
104
+ node.rotation = e.body.rotation() / DEG;
105
+ e.lastRot = node.rotation;
106
+ }
104
107
  const lv = e.body.linvel();
105
108
  node.linearVelocity = [lv.x * SCALE, lv.y * SCALE];
106
109
  e.lastVX = node.linearVelocity[0] ?? 0;
@@ -428,6 +431,7 @@ var Physics2D = class {
428
431
  collider,
429
432
  lastX: wx,
430
433
  lastY: wy,
434
+ lastRot: node.rotation,
431
435
  lastVX: 0,
432
436
  lastVY: 0
433
437
  };
@@ -456,6 +460,10 @@ var Physics2D = class {
456
460
  e.lastX = px;
457
461
  e.lastY = py;
458
462
  }
463
+ if (node.rotation !== e.lastRot) {
464
+ e.body.setRotation(node.rotation * DEG, true);
465
+ e.lastRot = node.rotation;
466
+ }
459
467
  if (node instanceof RigidBody2D) {
460
468
  const vx = node.linearVelocity[0] ?? 0;
461
469
  const vy = node.linearVelocity[1] ?? 0;