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,10 +1,10 @@
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 { H as validateCollider3D, I as Area3D, L as CharacterBody3D, R as PhysicsBody3D, V as Node3D, z as RigidBody3D } from "./gameplay-Dtzd2itW.js";
4
+ import { B as CharacterBody3D, G as validateCollider3D, H as RigidBody3D, V as PhysicsBody3D, W as Node3D, z as Area3D } from "./gameplay-CuqoHHUB.js";
5
5
  import { n as registerDebugSource } from "./debug-draw-BM3DsvtT.js";
6
6
  import { t as withoutRapierInitNoise } from "./quiet-rapier-BAJ4K94N.js";
7
- import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-CNxCuhZF.js";
7
+ import { A as Terrain3D, F as InstancedMesh3D, L as buildMeshGeometry, P as Joint3D } from "./environment-presets-QR7_75KJ.js";
8
8
  import { Euler, Matrix4, Quaternion, Vector3 } from "three";
9
9
  //#region src/3d/physics/collider-lines.ts
10
10
  /**
@@ -318,6 +318,9 @@ var Physics3D = class {
318
318
  rot[0] = eulerScratch.x / DEG;
319
319
  rot[1] = eulerScratch.y / DEG;
320
320
  rot[2] = eulerScratch.z / DEG;
321
+ e.lastRot[0] = rot[0];
322
+ e.lastRot[1] = rot[1];
323
+ e.lastRot[2] = rot[2];
321
324
  }
322
325
  const lv = e.body.linvel();
323
326
  const nv = node.linearVelocity;
@@ -381,6 +384,8 @@ var Physics3D = class {
381
384
  if (node.stickToGround && vy <= 0) this.kcc.enableSnapToGround(.1);
382
385
  else this.kcc.disableSnapToGround();
383
386
  this.kcc.setMaxSlopeClimbAngle(node.slopeLimitDeg * DEG);
387
+ if (node.stepHeight > 0) this.kcc.enableAutostep(node.stepHeight, node.stepHeight / 2, false);
388
+ else this.kcc.disableAutostep();
384
389
  let carryX = 0;
385
390
  let carryY = 0;
386
391
  let carryZ = 0;
@@ -644,6 +649,11 @@ var Physics3D = class {
644
649
  wy,
645
650
  wz
646
651
  ],
652
+ lastRot: [
653
+ node.rotation[0] ?? 0,
654
+ node.rotation[1] ?? 0,
655
+ node.rotation[2] ?? 0
656
+ ],
647
657
  lastV: [
648
658
  0,
649
659
  0,
@@ -679,6 +689,21 @@ var Physics3D = class {
679
689
  e.last[2] = p[2];
680
690
  node._interpPrev = node._interpCurr = null;
681
691
  }
692
+ const rx = node.rotation[0] ?? 0;
693
+ const ry = node.rotation[1] ?? 0;
694
+ const rz = node.rotation[2] ?? 0;
695
+ if (rx !== e.lastRot[0] || ry !== e.lastRot[1] || rz !== e.lastRot[2]) {
696
+ quatScratch.setFromEuler(eulerScratch.set(rx * DEG, ry * DEG, rz * DEG, "XYZ"));
697
+ e.body.setRotation({
698
+ x: quatScratch.x,
699
+ y: quatScratch.y,
700
+ z: quatScratch.z,
701
+ w: quatScratch.w
702
+ }, true);
703
+ e.lastRot[0] = rx;
704
+ e.lastRot[1] = ry;
705
+ e.lastRot[2] = rz;
706
+ }
682
707
  if (node instanceof RigidBody3D) {
683
708
  const v = [
684
709
  node.linearVelocity[0] ?? 0,
@@ -854,15 +879,20 @@ function collectBodies(node, out) {
854
879
  * Convex, always: a hull is a few planes to test against, where a triangle
855
880
  * mesh is a tree of them, and nothing scattered across a map needs concavity.
856
881
  */
857
- function meshShapedDesc(R, kind, size, scale) {
858
- const x = (size[0] ?? 1) * scale;
859
- const y = (size[1] ?? 1) * scale;
860
- const z = (size[2] ?? 1) * scale;
882
+ function meshShapedDesc(R, kind, size, scale, who) {
883
+ const sx = scale[0] ?? 1;
884
+ const sy = scale[1] ?? 1;
885
+ const sz = scale[2] ?? 1;
886
+ const x = (size[0] ?? 1) * sx;
887
+ const y = (size[1] ?? 1) * sy;
888
+ const z = (size[2] ?? 1) * sz;
889
+ const radial = Math.max(Math.abs(sx), Math.abs(sz));
890
+ if (who && kind !== "box" && kind !== "plane" && Math.abs(sx - sz) > 1e-6) console.warn(`[incanto] Collider on '${who}': shape 'auto' fits a ${kind} to a mesh scaled [${sx}, ${sy}, ${sz}]. A ${kind} has one radius, so the widest of x/z (${radial}) is used. Use a box, or scale x and z the same.`);
861
891
  switch (kind) {
862
892
  case "box": return R.ColliderDesc.cuboid(x / 2, y / 2, z / 2);
863
- case "sphere": return R.ColliderDesc.ball(x);
864
- case "capsule": return R.ColliderDesc.capsule(y / 2, x);
865
- case "cylinder": return R.ColliderDesc.cylinder(y / 2, x);
893
+ case "sphere": return R.ColliderDesc.ball((size[0] ?? 1) * Math.max(radial, Math.abs(sy)));
894
+ case "capsule": return R.ColliderDesc.capsule(y / 2, (size[0] ?? 1) * radial);
895
+ case "cylinder": return R.ColliderDesc.cylinder(y / 2, (size[0] ?? 1) * radial);
866
896
  case "plane": return R.ColliderDesc.cuboid(x / 2, .01, z / 2);
867
897
  default: return R.ColliderDesc.convexHull(meshPoints(kind, [
868
898
  x,
@@ -937,18 +967,79 @@ const TRIMESH_BUDGET = 2e4;
937
967
  function isStaticShape(node) {
938
968
  return !(node instanceof RigidBody3D || node instanceof CharacterBody3D);
939
969
  }
940
- /** Every mesh a body carries, itself first — `auto` shapes itself on one. */
941
- function meshesUnder(node, out, models) {
970
+ /**
971
+ * Every mesh a body carries, itself first — `auto` shapes itself on one.
972
+ *
973
+ * The transform chain is composed on the way down because `auto` used to drop
974
+ * it: the collector returned `{name, mesh, size}` and the `auto` branch built
975
+ * the shape at the BODY's origin, at the authored size, scale hard-coded to 1.
976
+ * The renderer applies all three every frame (`Node3D._syncObject3D`), so
977
+ * measured on a body whose plank child sits at `[0, 3, 0]`:
978
+ *
979
+ * ```
980
+ * drawn plank top y = 3.2
981
+ * ray straight down from y=10 : HIT Deck — surface at y=0.200
982
+ * ```
983
+ *
984
+ * and on one scaled 4×:
985
+ *
986
+ * ```
987
+ * drawn crate top y = 2.0
988
+ * ray straight down : HIT Crate — surface at y=0.500
989
+ * ray down inside the DRAWN 4 m crate : MISS
990
+ * ```
991
+ *
992
+ * You walk through the deck you can see and land on an invisible slab at ground
993
+ * level — from the shape whose entire promise is that "a blocker can never
994
+ * disagree with what the player sees". The sibling paths in this same file were
995
+ * already right: `syncScatters` honours every scatter row's translation, yaw
996
+ * and scale, and `modelTriangles` bakes a GLB's world matrices. Only the
997
+ * primitive-mesh branch dropped it.
998
+ */
999
+ function meshesUnder(node, out, models, at = [
1000
+ 0,
1001
+ 0,
1002
+ 0
1003
+ ], turn = [
1004
+ 0,
1005
+ 0,
1006
+ 0
1007
+ ], scale = [
1008
+ 1,
1009
+ 1,
1010
+ 1
1011
+ ]) {
942
1012
  if (isMountedModel(node)) models.push(node);
943
1013
  else {
944
1014
  const self = node;
945
1015
  if (typeof self.mesh === "string" && Array.isArray(self.size)) out.push({
946
1016
  name: node.name,
947
1017
  mesh: self.mesh,
948
- size: self.size
1018
+ size: self.size,
1019
+ at: [...at],
1020
+ turn: [...turn],
1021
+ scale: [...scale]
949
1022
  });
950
1023
  }
951
- for (const child of node.children) meshesUnder(child, out, models);
1024
+ for (const child of node.children) {
1025
+ const t = child;
1026
+ const p = Array.isArray(t.position) ? t.position : [];
1027
+ const r = Array.isArray(t.rotation) ? t.rotation : [];
1028
+ const c = Array.isArray(t.scale) ? t.scale : [];
1029
+ meshesUnder(child, out, models, [
1030
+ at[0] + (p[0] ?? 0) * scale[0],
1031
+ at[1] + (p[1] ?? 0) * scale[1],
1032
+ at[2] + (p[2] ?? 0) * scale[2]
1033
+ ], [
1034
+ turn[0] + (r[0] ?? 0),
1035
+ turn[1] + (r[1] ?? 0),
1036
+ turn[2] + (r[2] ?? 0)
1037
+ ], [
1038
+ scale[0] * (c[0] ?? 1),
1039
+ scale[1] * (c[1] ?? 1),
1040
+ scale[2] * (c[2] ?? 1)
1041
+ ]);
1042
+ }
952
1043
  }
953
1044
  /** Every scatter field asking to be solid (see InstancedMesh3D.collider). */
954
1045
  function collectScatters(node, out) {
@@ -1019,7 +1110,10 @@ function parseCollider3D(R, node) {
1019
1110
  const desc = buildColliderDesc3D(R, collider, node.name, node);
1020
1111
  if (!desc) return null;
1021
1112
  const offset = collider.offset;
1022
- if (Array.isArray(offset)) desc.setTranslation(offset[0] ?? 0, offset[1] ?? 0, offset[2] ?? 0);
1113
+ if (Array.isArray(offset)) {
1114
+ const at = desc.translation;
1115
+ desc.setTranslation(at.x + (offset[0] ?? 0), at.y + (offset[1] ?? 0), at.z + (offset[2] ?? 0));
1116
+ }
1023
1117
  return desc;
1024
1118
  }
1025
1119
  /**
@@ -1070,8 +1164,20 @@ function buildColliderDesc3D(R, collider, nodeName, node) {
1070
1164
  const parts = meshes.length + models.length;
1071
1165
  if (parts > 1) console.warn(`[incanto] Collider on '${nodeName}': shape 'auto' fits ONE mesh and took '${(mesh ?? model)?.name}'; ${parts - 1} other mesh(es) here are not solid. Give each part its own body.`);
1072
1166
  if (mesh) {
1073
- const desc = meshShapedDesc(R, mesh.mesh, mesh.size, 1);
1074
- if (desc) return desc;
1167
+ const desc = meshShapedDesc(R, mesh.mesh, mesh.size, mesh.scale, nodeName);
1168
+ if (desc) {
1169
+ desc.setTranslation(mesh.at[0], mesh.at[1], mesh.at[2]);
1170
+ if (mesh.turn[0] !== 0 || mesh.turn[1] !== 0 || mesh.turn[2] !== 0) {
1171
+ quatScratch.setFromEuler(eulerScratch.set(mesh.turn[0] * DEG, mesh.turn[1] * DEG, mesh.turn[2] * DEG, "XYZ"));
1172
+ desc.setRotation({
1173
+ x: quatScratch.x,
1174
+ y: quatScratch.y,
1175
+ z: quatScratch.z,
1176
+ w: quatScratch.w
1177
+ });
1178
+ }
1179
+ return desc;
1180
+ }
1075
1181
  } else if (model && node) {
1076
1182
  const tri = modelTriangles(node, model);
1077
1183
  if (!tri) return null;
package/dist/react.js CHANGED
@@ -156,7 +156,7 @@ function IncantoCanvas(props) {
156
156
  pointer: latest.pointer,
157
157
  ...keyboard !== void 0 ? { keyboard } : {}
158
158
  };
159
- const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-IX5lEH0P.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-DH7JI5xx.js").then((n) => n.n)).createGame2D(o)))(opts);
159
+ const next = await (_gameFactory ?? (mode === "3d" ? async (o) => (await import("./create-game-viBqXUoZ.js").then((n) => n.n)).createGame3D(o) : async (o) => (await import("./create-game-lLeITaZ5.js").then((n) => n.n)).createGame2D(o)))(opts);
160
160
  if (disposed) {
161
161
  next.dispose();
162
162
  return;
@@ -1,9 +1,10 @@
1
- import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-D7jTvDQv.js";
2
- import { I as translateOn, t as registerCoreNodes } from "./register-xuSRyD6b.js";
1
+ import { O as Node, k as diagnose, w as effectiveOrder } from "./loader-zDynoew_.js";
2
+ import { L as translateOn, t as registerCoreNodes } 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 { i as getNodeSchema, l as registerNode } from "./registry-CF70EArN.js";
6
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 { N as localFromWorld, P as worldPosition } from "./gameplay-CuqoHHUB.js";
7
8
  import { t as checkSheetGrid } from "./sheet-grid-BT6N_Bjs.js";
8
9
  import { AdditiveBlending, BufferAttribute, BufferGeometry, CanvasTexture, Color, DynamicDrawUsage, Group, InstancedMesh, MathUtils, Matrix4, Mesh, MeshBasicMaterial, NearestFilter, NormalBlending, Object3D, PlaneGeometry, SRGBColorSpace } from "three";
9
10
  //#region src/2d/physics/collider-validate.ts
@@ -94,6 +95,30 @@ var Node2D = class extends Node {
94
95
  return new Object3D();
95
96
  }
96
97
  /** @internal Push JSON props onto the backing object. Called every frame. */
98
+ /**
99
+ * Has this node produced the thing `static: true` is about to freeze?
100
+ *
101
+ * `static` latches after the first sync, and a textured node's first sync
102
+ * happens BEFORE its texture has decoded — `TextureLoader.load()` returns a
103
+ * `Texture` with `image === undefined` and fills it asynchronously, while
104
+ * `Renderer2D.render()` calls `assets.load()` and `syncTree2D()` in the same
105
+ * frame. So the state frozen was `visible = false`, forever. Measured on two
106
+ * identical sprites differing only in `static`:
107
+ *
108
+ * ```
109
+ * frame 1 Backdrop(static) visible: false Control visible: false
110
+ * 600 frames later Backdrop(static) visible: false width 1
111
+ * Control visible: true width 64
112
+ * ```
113
+ *
114
+ * Nothing throws: the texture loaded fine, so `assetErrors()` is empty,
115
+ * `stats().errors` is 0, and `framing` reads props rather than pixels and
116
+ * still calls the node on-screen. Overridden by the nodes that wait on an
117
+ * image; a node with nothing to wait for is ready by definition.
118
+ */
119
+ _staticReady(_assets) {
120
+ return true;
121
+ }
97
122
  _syncObject2D(_assets) {
98
123
  const o = this._ensureObject2D();
99
124
  o.position.set(this.position[0] ?? 0, -(this.position[1] ?? 0), 0);
@@ -393,6 +418,9 @@ var Sprite2D = class extends Node2D {
393
418
  height: image.height
394
419
  };
395
420
  }
421
+ _staticReady(assets) {
422
+ return this.texture === "" || this.resolveTexture(assets) !== null;
423
+ }
396
424
  _syncObject2D(assets) {
397
425
  super._syncObject2D(assets);
398
426
  const quad = this._quad();
@@ -577,18 +605,36 @@ var Camera2D = class extends Node2D {
577
605
  if (!this.follow) return;
578
606
  const target = this.getNodeOrNull(this.follow);
579
607
  if (!(target instanceof Node2D)) return;
580
- const tx = target.position[0] ?? 0;
581
- const ty = target.position[1] ?? 0;
608
+ const [tx = 0, ty = 0] = worldPosition(target);
609
+ const [px = 0, py = 0] = worldPosition(this);
582
610
  const s = Math.min(Math.max(this.smoothing, 0), .99);
583
611
  const f = s <= 0 ? 1 : 1 - s ** (dt * 60);
584
- const px = this.position[0] ?? 0;
585
- const py = this.position[1] ?? 0;
586
- this.position = [px + (tx - px) * f, py + (ty - py) * f];
612
+ const [lx = 0, ly = 0] = localFromWorld(this, [px + (tx - px) * f, py + (ty - py) * f]);
613
+ this.position = [lx, ly];
587
614
  }
588
615
  /** View center after clamping the (vw×vh)/zoom view rect inside `limits`. */
616
+ /**
617
+ * The view centre the renderer draws around, in WORLD space.
618
+ *
619
+ * It used to read `this.position` raw — the LOCAL prop — while every other
620
+ * 2D node gets its ancestors composed for free by the three scene graph. So a
621
+ * camera parented to the player, the Godot/Phaser idiom the authoring skill
622
+ * explicitly permits, framed the world origin:
623
+ *
624
+ * ```
625
+ * player world position : [1400, 900]
626
+ * renderer view centre : {"x":0,"y":0}
627
+ * framing : camera /Level/Player/Cam centred [1400, 900]
628
+ * 1 in view, 0 outside it
629
+ * ```
630
+ *
631
+ * `framing` composes full world matrices, so the instrument the skills tell
632
+ * you to trust certified a view the renderer never drew.
633
+ */
589
634
  clampedCenter(vw, vh) {
590
- let cx = this.position[0] ?? 0;
591
- let cy = this.position[1] ?? 0;
635
+ const world = worldPosition(this);
636
+ let cx = world[0] ?? 0;
637
+ let cy = world[1] ?? 0;
592
638
  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}.`);
593
639
  if (this.limits.length === 4) {
594
640
  const [minX, minY, maxX, maxY] = this.limits;
@@ -1435,6 +1481,11 @@ var TileMap2D = class extends Node2D {
1435
1481
  }
1436
1482
  return this.tileMesh;
1437
1483
  }
1484
+ _staticReady(assets) {
1485
+ if (this.texture === "" || !assets) return this.texture === "";
1486
+ const image = assets.getTexture(this.texture).image;
1487
+ return Boolean(image?.width);
1488
+ }
1438
1489
  _syncObject2D(assets) {
1439
1490
  super._syncObject2D(assets);
1440
1491
  const mesh = this._mesh();
@@ -1,5 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
- import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-D7jTvDQv.js";
2
+ import { C as ORDER_GROUP_BASE, E as InputMap, O as Node, S as createSaveStore, T as resolveOrderGroups, V as Signal, b as restoreBehaviors, k as diagnose, y as captureBehaviors } from "./loader-zDynoew_.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 { l as registerNode } from "./registry-CF70EArN.js";
@@ -3294,6 +3294,161 @@ var UiDialogue = class extends HudWidgetBase {
3294
3294
  }
3295
3295
  };
3296
3296
  //#endregion
3297
+ //#region src/core/nodes/respawn.ts
3298
+ /** Out of the world, from the spawn: metres in 3D, pixels in 2D. */
3299
+ const AUTO_DROP_3D_M = 50;
3300
+ const AUTO_DROP_2D_PX = 1e3;
3301
+ /**
3302
+ * Catch whatever this node hangs off when it leaves the world, and put it back.
3303
+ *
3304
+ * Nothing in the engine did this. Not a node, not a behavior, not a scene
3305
+ * header — and the shipped examples prove what that costs: with the `plays`
3306
+ * rung finally counting defects, `basic-3d-sideview` fails 8 seeded runs out of
3307
+ * 8, `water-lake-3d` 7, `water-ocean-3d` 5, `water-pool-3d` 4,
3308
+ * `water-river-3d` 1. Every one of them the same way — the player walks off the
3309
+ * edge of the terrain and falls forever, and the only fix available was a
3310
+ * per-frame `y` check hand-written in TypeScript, which is exactly the kind of
3311
+ * thing this engine exists to keep out of TypeScript.
3312
+ *
3313
+ * A CHILD NODE rather than a Behavior, deliberately: a node holds one behavior
3314
+ * and the player's is already spoken for (`PlayerControl`, `Health`, the game's
3315
+ * own script). Children stack, the way `CharacterController3D` already hangs
3316
+ * off its body.
3317
+ *
3318
+ * ```jsonc
3319
+ * { "name": "Player", "type": "RigidBody3D", "props": { … },
3320
+ * "children": [
3321
+ * { "name": "Ctl", "type": "CharacterController3D" },
3322
+ * { "name": "Catch", "type": "Respawn" } // that is the whole thing
3323
+ * ] }
3324
+ * ```
3325
+ *
3326
+ * With no props it guards its parent, uses the same line the playtest's fall
3327
+ * detector uses (50 m under the spawn in 3D, 1000 px in 2D), and puts the node
3328
+ * back where it started. `respawned` fires each time, so a life can be taken
3329
+ * off a `ScoreKeeper` or a `Health` with an ordinary connection — this node
3330
+ * does not decide what falling COSTS, which is a game-design question and not
3331
+ * a physics one.
3332
+ */
3333
+ var Respawn = class extends Node {
3334
+ static typeName = "Respawn";
3335
+ static signals = ["respawned"];
3336
+ static props = {
3337
+ target: {
3338
+ default: "..",
3339
+ nodePath: true
3340
+ },
3341
+ below: { default: null },
3342
+ to: { default: [] },
3343
+ resetVelocity: { default: true }
3344
+ };
3345
+ /**
3346
+ * Who is being caught. Defaults to the parent, which is the whole point of
3347
+ * hanging this off the thing it guards.
3348
+ */
3349
+ target = "..";
3350
+ /**
3351
+ * The line past which the target has left the world, in the scene's own
3352
+ * down: 3D counts down (−y), 2D counts down the screen (+y).
3353
+ *
3354
+ * `null` is AUTO — the spawn position offset by the same distance the
3355
+ * playtest's own fall detector uses, so "the level says it fell" and "the
3356
+ * ladder says it fell" cannot disagree. Author a number when the level has a
3357
+ * real floor to name; leave it alone otherwise.
3358
+ */
3359
+ below = null;
3360
+ /** Where to put it back. Empty = wherever it started. */
3361
+ to = [];
3362
+ /**
3363
+ * Zero the body's velocity on the way back.
3364
+ *
3365
+ * On by default because the alternative is invisible: a body respawned with
3366
+ * 40 m/s of accumulated fall still in it drops straight back through the
3367
+ * line on the next frame, and the level flickers instead of resuming.
3368
+ */
3369
+ resetVelocity = true;
3370
+ /** How many times this has caught the target — for HUDs and tests. */
3371
+ falls = 0;
3372
+ home = [];
3373
+ line = null;
3374
+ /** +1 when "out" means y greater (2D screen-down), −1 when smaller (3D). */
3375
+ down = -1;
3376
+ onReady() {
3377
+ const target = this.guarded();
3378
+ this.home = this.to.length > 0 ? [...this.to] : [...positionOf(target)];
3379
+ this.spawnY = worldY(target);
3380
+ }
3381
+ spawnY = 0;
3382
+ /**
3383
+ * Which way is out, and where the line is.
3384
+ *
3385
+ * Resolved on the first `update` rather than in `onReady`, because
3386
+ * `onReady` runs inside `loadScene` — BEFORE `setScene` attaches the engine —
3387
+ * so the scene's `dimension` is not readable there yet. Reading it there
3388
+ * silently gives every 2D scene the 3D answer, and a line above a falling
3389
+ * player fires on frame one, forever.
3390
+ *
3391
+ * Not called `resolve`: `Node` has a private method by that name and
3392
+ * `getNodeOrNull` calls it, so a subclass method with the same name
3393
+ * overrides it on the prototype — `private` is a TypeScript fiction that
3394
+ * costs nothing at runtime — and every path lookup this node made returned a
3395
+ * number. `node-method-shadowing.test.ts` refuses the whole family now.
3396
+ */
3397
+ fallLine() {
3398
+ if (this.line !== null) return this.line;
3399
+ this.down = this.tree?.engine?.scene?.dimension === "2d" ? 1 : -1;
3400
+ const drop = this.down > 0 ? AUTO_DROP_2D_PX : AUTO_DROP_3D_M;
3401
+ this.line = this.below ?? this.spawnY + this.down * drop;
3402
+ return this.line;
3403
+ }
3404
+ /** The node this guards, or itself if the path no longer resolves. */
3405
+ guarded() {
3406
+ return this.getNodeOrNull(this.target) ?? this;
3407
+ }
3408
+ update(_dt) {
3409
+ const line = this.fallLine();
3410
+ const target = this.guarded();
3411
+ if (target === this) return;
3412
+ const y = worldY(target);
3413
+ if ((y - line) * this.down <= 0) return;
3414
+ this.falls += 1;
3415
+ setPosition(target, this.home);
3416
+ if (this.resetVelocity) zeroVelocity(target);
3417
+ this.emit("respawned", target, y);
3418
+ }
3419
+ };
3420
+ function positionOf(node) {
3421
+ const p = node.position;
3422
+ return Array.isArray(p) ? p : [];
3423
+ }
3424
+ /** Sum of the `position[1]` of the node and every spatial ancestor. */
3425
+ function worldY(node) {
3426
+ let y = 0;
3427
+ let at = node;
3428
+ while (at) {
3429
+ y += positionOf(at)[1] ?? 0;
3430
+ at = at.parent;
3431
+ }
3432
+ return y;
3433
+ }
3434
+ function setPosition(node, to) {
3435
+ const slot = node;
3436
+ if (!Array.isArray(slot.position)) return;
3437
+ slot.position = slot.position.map((v, i) => to[i] ?? v);
3438
+ }
3439
+ /**
3440
+ * Stop whatever the fall built up.
3441
+ *
3442
+ * Both names are real: `RigidBody` calls it `linearVelocity` and
3443
+ * `CharacterBody` calls it `velocity`, and a respawn has to reach whichever
3444
+ * one this body has.
3445
+ */
3446
+ function zeroVelocity(node) {
3447
+ const body = node;
3448
+ if (Array.isArray(body.linearVelocity)) body.linearVelocity = body.linearVelocity.map(() => 0);
3449
+ if (Array.isArray(body.velocity)) body.velocity = body.velocity.map(() => 0);
3450
+ }
3451
+ //#endregion
3297
3452
  //#region src/core/nodes/timer.ts
3298
3453
  /**
3299
3454
  * The canonical serializable game clock — never `setTimeout` in game logic.
@@ -4021,6 +4176,7 @@ var UiMuteToggle = class extends UiToggle {
4021
4176
  function registerCoreNodes() {
4022
4177
  registerNode(Node);
4023
4178
  registerNode(Timer);
4179
+ registerNode(Respawn);
4024
4180
  registerNode(AudioPlayer);
4025
4181
  registerNode(HudLayer);
4026
4182
  registerNode(UiText);
@@ -4041,4 +4197,4 @@ function registerCoreNodes() {
4041
4197
  registerNode(UiMuteToggle);
4042
4198
  }
4043
4199
  //#endregion
4044
- export { UiBar as A, ROLLOFF_MODELS as B, readDeviceHints as C, HudLayer as D, EffectLog as E, suggestLocale as F, sfxDuration as G, spatialPan as H, translateOn as I, WebAudioMusicBackend as J, synthSfx as K, translationKey as L, BASE_LOCALE as M, Localization as N, HudWidgetBase as O, T_PREFIX as P, SfxEngine as R, qualityRendering as S, LogManager as T, SFX_PRESETS as U, spatialGain as V, SFX_PRESET_NAMES as W, fadeGain as X, crossfadeGains as Y, AudioBuses as Z, Engine as _, UiMuteToggle as a, qualityCaps as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, AudioPlayer as g, UiDialogue as h, UiLanguageSelect as i, UiText as j, UiBanner as k, UiSelect as l, UiButton as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, MusicManager as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, engine_exports as v, suggestQuality as w, qualityEnvironment as x, Settings as y, isAudioContextAvailable as z };
4200
+ export { UiBanner as A, isAudioContextAvailable as B, qualityRendering as C, EffectLog as D, LogManager as E, T_PREFIX as F, SFX_PRESET_NAMES as G, spatialGain as H, suggestLocale as I, MusicManager as J, sfxDuration as K, translateOn as L, UiText as M, BASE_LOCALE as N, HudLayer as O, Localization as P, AudioBuses as Q, translationKey as R, qualityEnvironment as S, suggestQuality as T, spatialPan as U, ROLLOFF_MODELS as V, SFX_PRESETS as W, crossfadeGains as X, WebAudioMusicBackend as Y, fadeGain as Z, AudioPlayer as _, UiMuteToggle as a, Settings as b, UiRenderScaleSelect as c, UiToggle as d, UiVolumeSlider as f, UiDialogue as g, UiButton as h, UiLanguageSelect as i, UiBar as j, HudWidgetBase as k, UiSelect as l, Respawn as m, UiFrameCapSelect as n, UiPanel as o, Timer as p, synthSfx as q, UiImage as r, UiQualitySelect as s, registerCoreNodes as t, UiSlider as u, Engine as v, readDeviceHints as w, qualityCaps as x, engine_exports as y, SfxEngine as z };
@@ -1,5 +1,5 @@
1
- import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-D7jTvDQv.js";
2
- import { L as translationKey, O as HudWidgetBase } from "./register-xuSRyD6b.js";
1
+ import { D as handlerShadowWarnings, j as nodeRefWarnings } from "./loader-zDynoew_.js";
2
+ import { R as translationKey, k as HudWidgetBase } from "./register-Btm7_Emq.js";
3
3
  import { o as getNodeType } from "./registry-CF70EArN.js";
4
4
  //#region src/core/audit.ts
5
5
  const ANIMATED_TYPES = new Set([
@@ -54,6 +54,18 @@ const LIGHT_TYPES = new Set([
54
54
  "SpotLight3D"
55
55
  ]);
56
56
  /**
57
+ * Nodes whose gameplay answer comes from a CPU twin built out of `size`.
58
+ *
59
+ * `Terrain3D.heightAt`, the heightfield collider and `Water3D._waterYAt` all
60
+ * read `size` and compose ancestor POSITIONS only, so an ancestor that scales
61
+ * or rotates them moves the picture and not the answer.
62
+ */
63
+ const WORLD_QUERY_TYPES = new Set([
64
+ "Terrain3D",
65
+ "Water3D",
66
+ "River3D"
67
+ ]);
68
+ /**
57
69
  * Built-ins that put something in a save. A save is keyed by node uid, so one
58
70
  * of these on a node without one is state that silently goes nowhere: the save
59
71
  * comes back `{}` and the load reports a clean bill, because "nothing was
@@ -121,6 +133,7 @@ function auditScene(scene, opts = {}) {
121
133
  if (type === "CharacterController3D") cameraDrivers.push(path);
122
134
  if (LIGHT_TYPES.has(type)) lights += 1;
123
135
  if (BODY_TYPES.has(type) && transformed) warnings.push(`${path}: ${type} sits under '${transformed.path}', whose ${transformed.what} physics cannot compose — the collider will not line up with what is drawn. Keep body ancestors untransformed or translation-only, or put the body outside them.`);
136
+ if (WORLD_QUERY_TYPES.has(type) && transformed) warnings.push(`${path}: ${type} sits under '${transformed.path}', whose ${transformed.what} it cannot compose — the ground (or water) you can see would not be the ground you can stand on. Keep its ancestors untransformed or translation-only.`);
124
137
  if (BODY_TYPES.has(type)) {
125
138
  const collider = props.collider;
126
139
  if (!collider || collider.shape === void 0) warnings.push(`${path}: ${type} has no collider — physics will skip it (add "collider": { "shape": ... }).`);
@@ -1,5 +1,5 @@
1
- import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-D7jTvDQv.js";
2
- import { _ as Engine, t as registerCoreNodes } from "./register-xuSRyD6b.js";
1
+ import { O as Node, V as Signal, k as diagnose, n as loadScene, t as buildNodeJson } from "./loader-zDynoew_.js";
2
+ import { t as registerCoreNodes, v as Engine } from "./register-Btm7_Emq.js";
3
3
  import { t as IncantoError } from "./errors-BpWbnbb_.js";
4
4
  import { n as jsonEquals, t as jsonClone } from "./json-CwwhxQgb.js";
5
5
  import { l as registerNode } from "./registry-CF70EArN.js";
@@ -214,6 +214,6 @@ function newUid() {
214
214
  //#endregion
215
215
  //#region src/index.ts
216
216
  /** Engine version. Kept in sync with package.json by the release pipeline. */
217
- const VERSION = "0.61.0";
217
+ const VERSION = "0.63.0";
218
218
  //#endregion
219
219
  export { preloadUrls as a, preloadSceneAssets as i, newUid as n, findPath as o, assetUrls as r, gridFromRows as s, VERSION as t };
@@ -1,4 +1,4 @@
1
- import { g as AudioPlayer } from "./register-xuSRyD6b.js";
1
+ import { _ as AudioPlayer } from "./register-Btm7_Emq.js";
2
2
  import { a as logReport, s as parseDrive } from "./touch-DESwnpOc.js";
3
3
  import { a as frameSignature, i as frameImage, o as frameStats } from "./frame-report-BSMny7oe.js";
4
4
  //#region src/core/audio-errors.ts