incanto 0.69.0 → 0.71.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 (147) 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 +121 -102
  15. package/bin/incanto.mjs +3 -2
  16. package/dist/2d.d.ts +296 -34
  17. package/dist/2d.js +4 -4
  18. package/dist/3d.d.ts +157 -9
  19. package/dist/3d.js +8 -8
  20. package/dist/{agent8-CvsfVskX.js → agent8-Cfmd3ar_.js} +61 -8
  21. package/dist/{audio-player-C0j2k-yB.d.ts → audio-player-L8yccdMP.d.ts} +2 -2
  22. package/dist/{behavior-rm8vVzu_.d.ts → behavior-Dcz0fr1S.d.ts} +755 -46
  23. package/dist/{create-game-CwxEDlCm.js → create-game-CH4kt78v.js} +143 -60
  24. package/dist/{create-game-CvJ1wSRP.js → create-game-LCHBLfA4.js} +98 -48
  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 +1088 -882
  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-99BzP_L-.js} +298 -45
  33. package/dist/{frame-report-Dlq13Gyj.js → frame-report-D-_7YF2G.js} +1 -1
  34. package/dist/{gameplay-D1KixWK8.js → gameplay-CNULJvwh.js} +914 -214
  35. package/dist/gameplay.d.ts +254 -10
  36. package/dist/gameplay.js +2 -2
  37. package/dist/{heightmap-CRK0M4jT.js → heightmap-BYgD5Edk.js} +1 -1
  38. package/dist/index.d.ts +179 -13
  39. package/dist/index.js +10 -12
  40. package/dist/json-CfTjpvW8.js +67 -0
  41. package/dist/{loader-DadV2QCT.d.ts → loader-8-IHvbKD.d.ts} +2 -2
  42. package/dist/net.d.ts +27 -3
  43. package/dist/net.js +2 -2
  44. package/dist/{noise-CGUMx44x.js → noise-D3nPpmFg.js} +1 -1
  45. package/dist/{physics-2d-CF5yyjbj.js → physics-2d-FhrXUc6B.js} +136 -11
  46. package/dist/{physics-3d-DbYaEcZy.js → physics-3d-CFEGxBXZ.js} +104 -23
  47. package/dist/{teardown-CMqFVxO9.js → picking-DVo7fI13.js} +104 -13
  48. package/dist/react.d.ts +2 -2
  49. package/dist/react.js +2 -2
  50. package/dist/{register-DbVArsns.js → register-BYQCBySi.js} +429 -28
  51. package/dist/{register-mmfugpaQ.js → register-DuVdzxbq.js} +594 -831
  52. package/dist/{replay-BEHLDcQV.js → replay-DEvp3kyV.js} +224 -14
  53. package/dist/{replay-BQfkX4x7.d.ts → replay-F7IZHdFR.d.ts} +20 -3
  54. package/dist/{schema-B6ugCV1Q.d.ts → rng-Bb-IutXB.d.ts} +38 -21
  55. package/dist/{rng-DP-SR7eg.js → rng-CDOMybym.js} +22 -0
  56. package/dist/{loader-DZE7B07H.js → save-slots-Bvuh2p_r.js} +4619 -2318
  57. package/dist/{split-screen-Cg76MCV5.d.ts → split-screen-CIYf1zSf.d.ts} +36 -4
  58. package/dist/{split-screen-CauMwu6y.js → split-screen-eULetcg-.js} +159 -26
  59. package/dist/{sprite-animation-CY-mrr1L.js → sprite-animation-CqR2o3SA.js} +39 -8
  60. package/dist/{src-CYEu-_S9.js → src-_jk0qLsS.js} +2 -17
  61. package/dist/test-CmN54kWv.js +4264 -0
  62. package/dist/test.d.ts +639 -30
  63. package/dist/test.js +3 -3
  64. package/dist/touch-BnCyPA0G.js +519 -0
  65. package/dist/vite.d.ts +20 -37
  66. package/dist/vite.js +177 -7
  67. package/dist/{webgl-unavailable-Z5_g8x9H.js → webgl-unavailable-C8aDbGmR.js} +44 -0
  68. package/editor/assets/agent8-yLIEYHbd.js +1 -0
  69. package/editor/assets/{debug-CMULinBW.js → debug-u31w_yhq.js} +2 -2
  70. package/editor/assets/index-DK9xMGpW.js +11046 -0
  71. package/editor/index.html +1 -1
  72. package/package.json +4 -14
  73. package/schemas/scene.schema.json +34 -3
  74. package/skills/incanto-3d-character.md +14 -1
  75. package/skills/incanto-3d-models.md +12 -0
  76. package/skills/incanto-assets.md +9 -1
  77. package/skills/incanto-audio.md +8 -2
  78. package/skills/incanto-behaviors-and-scripts.md +105 -7
  79. package/skills/incanto-building-2d-games.md +97 -10
  80. package/skills/incanto-building-3d-games.md +58 -4
  81. package/skills/incanto-editor.md +46 -7
  82. package/skills/incanto-environment.md +19 -1
  83. package/skills/incanto-game-feel.md +70 -0
  84. package/skills/incanto-gameplay-behaviors.md +172 -20
  85. package/skills/incanto-hud.md +142 -7
  86. package/skills/incanto-localization.md +13 -5
  87. package/skills/incanto-multiplayer.md +83 -3
  88. package/skills/incanto-node-reference.md +239 -59
  89. package/skills/incanto-performance.md +52 -0
  90. package/skills/incanto-physics-and-input.md +123 -24
  91. package/skills/incanto-playtesting.md +112 -2
  92. package/skills/incanto-save-slots.md +217 -7
  93. package/skills/incanto-scene-json-authoring.md +69 -12
  94. package/skills/incanto-verifying-your-game.md +207 -10
  95. package/skills/incanto-web-integration.md +28 -0
  96. package/skills/incanto-your-first-game.md +3 -1
  97. package/templates-app/beacon-isle-3d/generate-world.ts +77 -9
  98. package/templates-app/beacon-isle-3d/package.json +2 -2
  99. package/templates-app/beacon-isle-3d/src/behaviors.ts +22 -0
  100. package/templates-app/beacon-isle-3d/src/game.scene.json +143 -600
  101. package/templates-app/beacon-isle-3d/src/main.ts +11 -1
  102. package/templates-app/beacon-isle-3d/tsconfig.json +1 -1
  103. package/templates-app/beacon-isle-3d/verify.ts +3 -1
  104. package/templates-app/molehill-2d/.incanto/playtest/lost-seed1.json +4277 -0
  105. package/templates-app/molehill-2d/PROJECT/Context.md +58 -0
  106. package/templates-app/molehill-2d/PROJECT/Requirements.md +39 -0
  107. package/templates-app/molehill-2d/PROJECT/Status.md +27 -0
  108. package/templates-app/molehill-2d/PROJECT/Structure.md +48 -0
  109. package/templates-app/molehill-2d/docs/project-2d-rules.md +44 -0
  110. package/templates-app/molehill-2d/index.html +73 -0
  111. package/templates-app/molehill-2d/package.json +23 -0
  112. package/templates-app/molehill-2d/src/behaviors.ts +198 -0
  113. package/templates-app/molehill-2d/src/game.scene.json +1205 -0
  114. package/templates-app/molehill-2d/src/main.ts +41 -0
  115. package/templates-app/molehill-2d/tsconfig.json +13 -0
  116. package/templates-app/molehill-2d/verify.ts +247 -0
  117. package/templates-app/molehill-2d/vite.config.ts +12 -0
  118. package/templates-app/platformer-2d/package.json +2 -2
  119. package/templates-app/platformer-2d/src/game.scene.json +178 -986
  120. package/templates-app/platformer-2d/src/main.ts +11 -1
  121. package/templates-app/platformer-2d/tsconfig.json +1 -1
  122. package/templates-app/star-survivor/package.json +2 -2
  123. package/templates-app/star-survivor/src/game.scene.json +43 -216
  124. package/templates-app/star-survivor/src/main.ts +11 -1
  125. package/templates-app/star-survivor/tsconfig.json +1 -1
  126. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  127. package/templates-app/tps-3d/package.json +2 -2
  128. package/templates-app/tps-3d/src/behaviors.ts +19 -1
  129. package/templates-app/tps-3d/src/game.scene.json +66 -263
  130. package/templates-app/tps-3d/src/main.ts +13 -2
  131. package/templates-app/tps-3d/tsconfig.json +1 -1
  132. package/templates-app/village-quest-3d/.incanto/playtest/swapped-seed1.json +1735 -0
  133. package/templates-app/village-quest-3d/package.json +2 -2
  134. package/templates-app/village-quest-3d/src/behaviors.ts +42 -4
  135. package/templates-app/village-quest-3d/src/grove.scene.json +68 -306
  136. package/templates-app/village-quest-3d/src/main.ts +11 -1
  137. package/templates-app/village-quest-3d/src/village.scene.json +202 -863
  138. package/templates-app/village-quest-3d/tsconfig.json +1 -1
  139. package/templates-app/village-quest-3d/verify.ts +14 -1
  140. package/dist/duplicate-Dj4L-5AA.js +0 -22
  141. package/dist/json-CwwhxQgb.js +0 -36
  142. package/dist/registry-CF70EArN.js +0 -212
  143. package/dist/rolldown-runtime-D7D4PA-g.js +0 -13
  144. package/dist/test-D6ISmuFE.js +0 -2383
  145. package/dist/touch-DEAmqGdf.js +0 -225
  146. package/editor/assets/agent8-BDvaZMQJ.js +0 -1
  147. package/editor/assets/index-CZIRr5EL.js +0 -10958
package/dist/3d.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-C0j2k-yB.js";
2
- import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-rm8vVzu_.js";
3
- import { n as DiagnosticSink, t as EditorSwitchOptions } from "./editor-switch-CnIOiyNJ.js";
4
- import { i as SceneJson$1, s as JsonObject } from "./schema-B6ugCV1Q.js";
1
+ import { c as showBootFailure, i as ParticleSim, o as ParticleView, r as SpatialPose, s as isWebGLAvailable } from "./audio-player-L8yccdMP.js";
2
+ import { D as QualityCaps, P as Scene$1, S as Scheduler, T as RendererStats, b as Engine, d as PropSchema, jt as Node, n as BehaviorCtor, w as GameStats, x as EngineOptions } from "./behavior-Dcz0fr1S.js";
3
+ import { t as DiagnosticSink } from "./diagnostics-Cu85N3tL.js";
4
+ import { a as SceneJson$1, s as JsonObject } from "./rng-Bb-IutXB.js";
5
+ import { t as EditorSwitchOptions } from "./editor-switch-CFU9mCec.js";
5
6
  import { a as GridCell, c as diffText, d as frameText, i as FrameStatsOptions, l as frameSignature, n as FrameSignature, o as SIGNATURE_GRID, r as FrameStats, s as diffSignatures, t as FrameDiff, u as frameStats } from "./frame-report-DNxDAb1w.js";
6
- import { t as LoadSceneOptions } from "./loader-DadV2QCT.js";
7
+ import { t as LoadSceneOptions } from "./loader-8-IHvbKD.js";
7
8
  import { n as PathGrid } from "./pathfinding-_fGrCFmH.js";
8
9
  import { AnimationClip, AnimationMixer, BufferGeometry, Color, DirectionalLight, Group, InstancedMesh, Mesh, MeshPhysicalMaterial, Object3D, PerspectiveCamera, Scene, ShaderMaterial, Texture, Vector3, WebGLRenderer } from "three";
9
10
  import { VRM } from "@pixiv/three-vrm";
@@ -341,6 +342,20 @@ declare class Node3D extends Node {
341
342
  * error rather than a silent per-node decode; overrides that do not need it
342
343
  * simply keep the one-argument signature. Null in a headless test or a
343
344
  * detached probe, where the node falls back to loading uncached. */
345
+ /**
346
+ * Is there anything left to wait for before this node can be FROZEN?
347
+ *
348
+ * The static latch fired unconditionally, while `Renderer3D.render()` calls
349
+ * `assets.load()` and `syncTree()` in the SAME frame — so a
350
+ * `ModelInstance3D` frozen by its own or an ancestor group's `static` never
351
+ * mounted its GLB at all, permanently and silently: the asset loaded fine, so
352
+ * `assetErrors()` is empty and `stats().errors` is 0, and `framing` reads
353
+ * props rather than pixels and still calls the node on-screen.
354
+ *
355
+ * The 2D adapter already had exactly this gate (`Node2D._staticReady`); this
356
+ * is its twin. A node with nothing to wait for is ready by definition.
357
+ */
358
+ _staticReady(_assets: AssetStore3D | null): boolean;
344
359
  _syncObject3D(alpha?: number, _assets?: AssetStore3D | null): void;
345
360
  override free(): void;
346
361
  }
@@ -409,7 +424,7 @@ declare class Area3D extends PhysicsBody3D {
409
424
  *
410
425
  * Empty in a scene with no physics world.
411
426
  */
412
- overlapping(group?: string): PhysicsBody3D[];
427
+ overlapping(group?: string | undefined): PhysicsBody3D[];
413
428
  }
414
429
  /** Dynamic simulated body. */
415
430
  declare class RigidBody3D extends PhysicsBody3D {
@@ -430,6 +445,13 @@ declare class RigidBody3D extends PhysicsBody3D {
430
445
  * no counterpart to `linearVelocity`. `fixedRotation` pins it at zero.
431
446
  */
432
447
  angularVelocity: number[];
448
+ /**
449
+ * The other name for `linearVelocity` — see `RigidBody2D.velocity`. On a
450
+ * plain TS class `body.velocity = [...]` succeeds silently and does nothing,
451
+ * and that is the spelling the physics skill taught.
452
+ */
453
+ get velocity(): number[];
454
+ set velocity(v: number[]);
433
455
  /** @internal set by Physics3D */
434
456
  _physics3d: Physics3D | null;
435
457
  /** World-space impulse (kg·m/s) — the floating-capsule controller's verb. */
@@ -491,6 +513,31 @@ declare class CharacterBody3D extends PhysicsBody3D {
491
513
  /** @internal Updated by Physics3D.moveAndSlide. */
492
514
  _grounded: boolean;
493
515
  moveAndSlide(): void;
516
+ /** @internal Set by the physics step from the KCC's resolved collisions. */
517
+ _onWall: [number, number, number] | null;
518
+ /** @internal */
519
+ _onCeiling: boolean;
520
+ /**
521
+ * Touching a wall.
522
+ *
523
+ * `CharacterBody2D` has had this since wall jump and wall slide turned out to
524
+ * be not merely awkward to write in 2D but IMPOSSIBLE — `isOnFloor()` was the
525
+ * only surface a character could report, so even a hand-written behavior had
526
+ * nothing to ask. 3D was left with exactly that problem: a 3D platformer
527
+ * could not wall-jump, and nothing said why.
528
+ */
529
+ isOnWall(): boolean;
530
+ /**
531
+ * The wall's normal, pointing AWAY from it (toward the character) — the
532
+ * vector a wall jump pushes along. `null` when not touching one.
533
+ *
534
+ * 2D answers this as `wallSide()`, a -1/+1 scalar, because a 2D character
535
+ * has only two horizontal directions. In three dimensions the honest answer
536
+ * is the normal itself.
537
+ */
538
+ wallNormal(): [number, number, number] | null;
539
+ /** Head hit something — cut the jump short, the way every platformer does. */
540
+ isOnCeiling(): boolean;
494
541
  isOnFloor(): boolean;
495
542
  }
496
543
  //#endregion
@@ -557,6 +604,14 @@ declare class Physics3D {
557
604
  */
558
605
  private poseFromTree;
559
606
  /** @internal Called by CharacterBody3D.moveAndSlide (during tree fixedUpdate). */
607
+ /**
608
+ * May `candidate` carry `rider`? — see the 2D twin.
609
+ *
610
+ * "Never a CharacterBody3D" is the whole CLASS, and a CharacterBody3D
611
+ * animated by position writes is the documented moving-platform path. What
612
+ * the guard was really for is a carry CYCLE, so that is what it tests.
613
+ */
614
+ private canCarry;
560
615
  moveAndSlide(node: CharacterBody3D): void;
561
616
  /** Rapier debug segments (meters). Null while debugDraw is off. */
562
617
  debugLines(): Float32Array | null;
@@ -582,6 +637,16 @@ declare class Physics3D {
582
637
  * off ONE fixed body — they never move, so there is nothing to step — and the
583
638
  * whole set is rebuilt only when the tree's shape changes.
584
639
  */
640
+ /** Where each live scatter field was when its colliders were built. */
641
+ private readonly scatterState;
642
+ /**
643
+ * Has any scatter field moved, or had its rows replaced, since the build?
644
+ *
645
+ * `transforms` is compared by IDENTITY, which is the operation the skill
646
+ * teaches ("replace the whole array"); the world position catches the field
647
+ * itself or an ancestor being moved.
648
+ */
649
+ private scattersMoved;
585
650
  private syncScatters;
586
651
  private syncBodies;
587
652
  /**
@@ -621,7 +686,8 @@ declare class Physics3D {
621
686
  */
622
687
  overlapping(node: PhysicsBody3D): PhysicsBody3D[];
623
688
  /**
624
- * World-space raycast (meters). `exclude` skips that body's collider
689
+ * World-space raycast (meters). `dir` may be any length (it is normalized),
690
+ * so `distance` and `maxLen` are metres. `exclude` skips that body's collider —
625
691
  * ground probes from inside a capsule need it. `opts.staticOnly` hits ONLY the
626
692
  * fixed/static world (excludes both dynamic AND kinematic bodies): the camera
627
693
  * spring arm uses it so neither a dynamic projectile nor a KINEMATIC enemy
@@ -633,6 +699,7 @@ declare class Physics3D {
633
699
  distance: number;
634
700
  normal: [number, number, number];
635
701
  node: PhysicsBody3D | null;
702
+ point: [number, number, number];
636
703
  } | null;
637
704
  /**
638
705
  * Sweep a sphere of `radius` from `origin` along `dir` — a THICK ray. The
@@ -669,6 +736,12 @@ interface CreateGame3DOptions {
669
736
  /** Keyboard source (default: window when available). `false` disables. */
670
737
  keyboard?: KeyboardTarget | false;
671
738
  /** Pointer-look on the canvas (FPS pattern). Default false. */
739
+ /**
740
+ * Omitted, this is AUTO: the pointer is attached when the scene contains a
741
+ * `Clickable`, the same way `physics: 'auto'` turns Rapier on for a scene
742
+ * with bodies. Pass `true` for mouse-look (it also requests pointer lock on
743
+ * click here), or when a behaviour of your own reads `pointerPosition()`.
744
+ */
672
745
  pointer?: boolean | {
673
746
  lockOnClick?: boolean;
674
747
  };
@@ -1343,16 +1416,37 @@ declare class BoneLookAt3D extends Node3D {
1343
1416
  declare class Camera3D extends Node3D {
1344
1417
  static override readonly typeName: string;
1345
1418
  static override readonly props: PropSchema;
1419
+ /** A frustum whose planes are the wrong way round draws nothing at all. */
1420
+ static validateJson(node: Node): void;
1346
1421
  fov: number;
1347
1422
  near: number;
1348
1423
  far: number;
1349
1424
  current: boolean;
1425
+ lookAt: string;
1426
+ /**
1427
+ * Aim, every frame, whenever `lookAt` names something.
1428
+ *
1429
+ * In `update` and not `_syncObject3D`: the second is the renderer's copy pass
1430
+ * and a static node is pruned from it, which silently freezes anything driven
1431
+ * there. `rotation` is written as an ordinary prop, so the editor, the
1432
+ * serializer and `describeFraming` all see what the camera is actually doing.
1433
+ */
1434
+ /** The `lookAt` path already reported as unlookable — cleared by a new one. */
1435
+ private warnedLookAt;
1436
+ override update(_dt: number): void;
1350
1437
  protected override _createObject3D(): Object3D;
1351
1438
  override _syncObject3D(): void;
1352
1439
  }
1353
1440
  //#endregion
1354
1441
  //#region src/3d/nodes/character-controller-3d.d.ts
1355
1442
  /**
1443
+ * The states this controller enters, and therefore the only keys its
1444
+ * `animations` map may carry.
1445
+ */
1446
+ declare const MOVEMENT_STATES: readonly ["idle", "walk", "run", "fastRun", "airborne"];
1447
+ /** One of {@link MOVEMENT_STATES}. */
1448
+ type MovementState = (typeof MOVEMENT_STATES)[number];
1449
+ /**
1356
1450
  * The floating-capsule character controller behind every agent8 3D template
1357
1451
  * (vibe-starter-3d parity, same numbers): put it UNDER a dynamic RigidBody3D
1358
1452
  * with a capsule collider and it gives you impulse-based movement with
@@ -1449,11 +1543,23 @@ declare class CharacterController3D extends Node3D {
1449
1543
  * "walk": "$walk", "airborne": "$jump" }
1450
1544
  */
1451
1545
  animations: Record<string, string>;
1546
+ /**
1547
+ * Scene-load validation: an animation keyed by a state this controller never
1548
+ * enters is a clip that never plays.
1549
+ *
1550
+ * The five state names live in one place in one skill, and `jumping`,
1551
+ * `sprint`, `walking` and `falling` are what they are called in most other
1552
+ * engines — `jump` is even the ACTION name in every template here. A wrong
1553
+ * key was accepted and never looked at again, so the character walked and
1554
+ * idled correctly and never animated its jump, which reads as broken
1555
+ * animation rather than a wrong key.
1556
+ */
1557
+ static validateJson(node: Node): void;
1452
1558
  /** Camera yaw/pitch in radians (mouse look mutates these). */
1453
1559
  yaw: number;
1454
1560
  pitch: number;
1455
1561
  /** idle | walk | run | fastRun | airborne — drive animations from this. */
1456
- state: "idle" | "walk" | "run" | "fastRun" | "airborne";
1562
+ state: MovementState;
1457
1563
  grounded: boolean;
1458
1564
  /** @internal Feel timers — see the jump block for what each one buys. */
1459
1565
  private coyoteLeft;
@@ -2247,6 +2353,11 @@ declare class ModelInstance3D extends Node3D {
2247
2353
  /** Embedded clip names + the scene's animation `$key`s (editor dropdown). */
2248
2354
  availableAnimations(): string[];
2249
2355
  /** Called by the 3D sync pass with the renderer's asset store. */
2356
+ /**
2357
+ * A model is ready when its GLB is. Freezing before that mounts nothing —
2358
+ * see `Node3D._staticReady`.
2359
+ */
2360
+ override _staticReady(assets: AssetStore3D | null): boolean;
2250
2361
  _syncModel(assets: AssetStore3D): void;
2251
2362
  /**
2252
2363
  * Scale the model so it stands `targetHeight` tall, measured ONCE.
@@ -2957,7 +3068,32 @@ declare class Terrain3D extends Node3D {
2957
3068
  roughness: number;
2958
3069
  /** Detail octave count. */
2959
3070
  detail: number;
2960
- /** Snap-flatten a plateau within ±0.6 m of maxHeight·flatThreshold. */
3071
+ /**
3072
+ * Snap-flatten a plateau within ±0.6 m of maxHeight·flatThreshold.
3073
+ *
3074
+ * A fraction of `maxHeight`, which is a SCALE and not the peak: heights
3075
+ * realize at roughly 3.5–8× it, so the default 0.95 puts the plateau near the
3076
+ * BOTTOM of the range rather than at the summit the name suggests. Measured
3077
+ * on an alpine 200 m terrain, seed 3:
3078
+ *
3079
+ * ```
3080
+ * maxHeight 28 realized 25..150 band at 26.6 = 18% of peak, 28 cells on it
3081
+ * maxHeight 8 realized 8..43 band at 7.6 = 18% of peak, 99 cells on it
3082
+ * ```
3083
+ *
3084
+ * It is doing something — that is a real terrace, and the ±0.6 m boundary
3085
+ * between snapped and unsnapped cells is what `examples/water-ocean-3d` calls
3086
+ * "a sawtooth band across the underwater flat". Read `heightAt` before
3087
+ * choosing a value, the way `maxHeight`'s own note says to.
3088
+ *
3089
+ * 0..1 is the useful range, and anything ABOVE 1 turns the snap off, because
3090
+ * the band then sits over every height the terrain realizes. `examples/water-ocean-3d` uses `20` for exactly that,
3091
+ * with a comment explaining why, which is how a `max: 1` on this prop was
3092
+ * caught the moment it was declared: at `maxHeight 0.1` the ±0.6 m band
3093
+ * swallows the whole field and rips a sawtooth across the underwater flat.
3094
+ * So the bound is `min: 0` and "off" is a documented idiom rather than an
3095
+ * accident.
3096
+ */
2961
3097
  flatThreshold: number;
2962
3098
  /** Wrap edges down into an island rim. null = theme decides (island: on). */
2963
3099
  islandEdge: boolean | null;
@@ -3249,9 +3385,21 @@ declare class VoxelGrid3D extends Node3D {
3249
3385
  static override readonly typeName: string;
3250
3386
  static override readonly signals: readonly string[];
3251
3387
  static override readonly props: PropSchema;
3388
+ /**
3389
+ * Hard-fail an authored grid that cannot all be DRAWN.
3390
+ *
3391
+ * The draw loop stops at `MAX_INSTANCES` and the excess blocks stay in the
3392
+ * map — so `tileAt()` still reports them and a chunk collider builder still
3393
+ * makes them solid. The world past the cap was invisible AND you walked into
3394
+ * it. Every other 3D node whose instance count comes from authored data
3395
+ * hard-fails at load; this one silently truncated.
3396
+ */
3397
+ static validateJson(node: Node): void;
3252
3398
  /** JSON-authored blocks as [x, y, z, tile] tuples (generated terrains bake these). */
3253
3399
  voxels: number[][];
3254
3400
  private appliedVoxels;
3401
+ /** Said once per node: a runtime overflow must not spam a frame loop. */
3402
+ private reportedOverflow;
3255
3403
  private readonly map;
3256
3404
  private mesh;
3257
3405
  private dirty;
package/dist/3d.js CHANGED
@@ -1,12 +1,12 @@
1
- import { c as parseEnvironment3D, l as sunDirectionFromElevationAzimuth, s as horizonColorFromSky, u as sunDirectionFromSky } from "./loader-DZE7B07H.js";
1
+ import { C as sunDirectionFromSky, S as sunDirectionFromElevationAzimuth, b as horizonColorFromSky, x as parseEnvironment3D } from "./save-slots-Bvuh2p_r.js";
2
2
  import { t as showBootFailure } from "./boot-failure-CKYrEcGF.js";
3
- import { t as isWebGLAvailable } from "./webgl-unavailable-Z5_g8x9H.js";
4
- import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-Dlq13Gyj.js";
5
- import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-D1KixWK8.js";
6
- import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-BO-7XoFV.js";
7
- import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-CvJ1wSRP.js";
8
- import { n as splatWeights, t as buildHeightmap } from "./heightmap-CRK0M4jT.js";
9
- import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-DbYaEcZy.js";
3
+ import { t as isWebGLAvailable } from "./webgl-unavailable-C8aDbGmR.js";
4
+ import { a as frameSignature, n as diffSignatures, o as frameStats, r as diffText, s as frameText, t as SIGNATURE_GRID } from "./frame-report-D-_7YF2G.js";
5
+ import { G as StaticBody3D, H as CharacterBody3D, K as Node3D, L as Water3D, R as WATER_CUTOUT_MAX, U as PhysicsBody3D, V as Area3D, W as RigidBody3D, Y as WATER_MAX_RIPPLES, z as WaterCutout3D } from "./gameplay-CNULJvwh.js";
6
+ import { A as Terrain3D, B as keyboardIntensity, C as resolveFlowerDensity, D as BoneLookAt3D, E as Camera3D, F as InstancedMesh3D, G as acquireTexture, H as rigPose, I as MeshInstance3D, M as TERRAIN_THEMES, N as terrainThemeLayers, O as BoneAttachment3D, P as Joint3D, R as QUARTER_PITCH, S as Flowers3D, T as CharacterController3D, U as TextureCache3D, V as movementState, W as acquireOwnTexture, _ as LoftMesh3D, a as Tree3D, b as Foliage3D, c as buildRiverRings, d as riverCarveChannels, f as riverStepFor, g as ModelInstance3D, h as Particles3D, i as VoxelGrid3D, j as DEFAULT_TERRAIN_TEXTURE_BASE, k as Billboard3D, l as findRiverCoverageGaps, m as traceDownhillPath, n as registerNodes3D, o as Trail3D, p as smoothCourse, r as VOXEL_PALETTE, s as River3D, u as projectToRiver, v as DirectionalLight3D, w as FLOWER_VARIETIES, x as DENSITY_PRESETS, y as OmniLight3D, z as cameraRelative } from "./environment-presets-99BzP_L-.js";
7
+ import { a as Environment3D, c as AssetStore3D, i as syncTree, o as applyQualityTier3D, r as Renderer3D, s as setEnvironment3D, t as createGame3D } from "./create-game-LCHBLfA4.js";
8
+ import { n as splatWeights, t as buildHeightmap } from "./heightmap-BYgD5Edk.js";
9
+ import { n as enablePhysics3D, t as Physics3D } from "./physics-3d-CFEGxBXZ.js";
10
10
  //#region src/3d/model-verdict.ts
11
11
  /** Mixamo exports every bone as `mixamorigX`; the retargeter binds by that name. */
12
12
  const MIXAMO = /^mixamorig[:_]?/i;
@@ -1,4 +1,5 @@
1
- import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
1
+ import { o as __exportAll } from "./json-CfTjpvW8.js";
2
+ import { Y as diagnose } from "./save-slots-Bvuh2p_r.js";
2
3
  //#region src/net/agent8.ts
3
4
  var agent8_exports = /* @__PURE__ */ __exportAll({
4
5
  Agent8TransportForTest: () => Agent8Transport,
@@ -20,9 +21,19 @@ var agent8_exports = /* @__PURE__ */ __exportAll({
20
21
  * 3. Reconnect: the SDK clears pending subscriptions on disconnect and never
21
22
  * restores them — we re-issue every active subscription after reconnect.
22
23
  */
23
- async function createAgent8Server(config) {
24
- return new Agent8Transport((await import("@agent8/gameserver/dist/src/server/GameServer.js")).GameServer, config);
24
+ async function createAgent8Server(config, diagnostics) {
25
+ return new Agent8Transport((await import("@agent8/gameserver/dist/src/server/GameServer.js")).GameServer, config, { diagnostics });
25
26
  }
27
+ /** Backoff for a dropped socket: 0.5s, 1, 2, 4, 8, 8, … — see `reconnect`. */
28
+ const RECONNECT_DELAYS_MS = [
29
+ 500,
30
+ 1e3,
31
+ 2e3,
32
+ 4e3,
33
+ 8e3
34
+ ];
35
+ /** ~40 seconds of trying before it says it has given up. */
36
+ const RECONNECT_ATTEMPTS = 8;
26
37
  var Agent8Transport = class {
27
38
  raw;
28
39
  active = /* @__PURE__ */ new Map();
@@ -31,7 +42,14 @@ var Agent8Transport = class {
31
42
  /** Set by disconnect(): the closing socket may still fire onDisconnect — that
32
43
  * must not resurrect the connection after an intentional teardown. */
33
44
  disposed = false;
34
- constructor(GameServer, config) {
45
+ diagnostics;
46
+ /** Injectable so a test does not really wait 40 seconds. */
47
+ wait;
48
+ constructor(GameServer, config, opts) {
49
+ this.diagnostics = opts?.diagnostics ?? null;
50
+ this.wait = opts?.wait ?? ((ms) => new Promise((resolve) => {
51
+ setTimeout(resolve, ms);
52
+ }));
35
53
  this.raw = new GameServer({ ...config });
36
54
  }
37
55
  get account() {
@@ -70,16 +88,51 @@ var Agent8Transport = class {
70
88
  this.active.delete(key);
71
89
  };
72
90
  }
91
+ /**
92
+ * Get back on the wire after the socket dropped, and keep trying.
93
+ *
94
+ * This used to be one `await this.rawConnect()` with no catch. A server that
95
+ * is still restarting REJECTS that connect, and since the caller is
96
+ * `void this.reconnect()` the rejection escaped as an unhandled promise, the
97
+ * re-issue loop below it never ran, and nothing scheduled a second attempt:
98
+ * every subscription the SDK dropped stayed dropped and there was no path
99
+ * back for the session. A resolved `false` was worse still — it counted as
100
+ * success and re-issued every subscription onto a socket that was not
101
+ * connected.
102
+ *
103
+ * A dropped connection is the one multiplayer failure that is nobody's fault
104
+ * and fixes itself, so the answer is to wait and try again — and to SAY so,
105
+ * because a game that has gone quiet looks identical to one where nothing is
106
+ * happening.
107
+ */
73
108
  async reconnect() {
74
109
  if (this.disposed) return;
75
110
  if (this.reconnecting) return;
76
111
  this.reconnecting = true;
77
112
  try {
78
- await this.rawConnect();
79
- for (const entry of this.active.values()) {
80
- entry.off();
81
- entry.off = entry.make();
113
+ for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
114
+ let connected = false;
115
+ let why = null;
116
+ try {
117
+ connected = await this.rawConnect() !== false;
118
+ } catch (error) {
119
+ why = error;
120
+ }
121
+ if (this.disposed) return;
122
+ if (connected) {
123
+ for (const entry of this.active.values()) {
124
+ entry.off();
125
+ entry.off = entry.make();
126
+ }
127
+ if (attempt > 0) diagnose(this.diagnostics, "info", `[incanto] multiplayer reconnected after ${attempt + 1} attempts.`);
128
+ return;
129
+ }
130
+ if (attempt === 0) diagnose(this.diagnostics, "warn", `[incanto] the multiplayer connection dropped and the server would not take it back${why ? ` (${why instanceof Error ? why.message : String(why)})` : ""}. Retrying — other players cannot see this one until it comes back.`);
131
+ const delay = RECONNECT_DELAYS_MS[Math.min(attempt, RECONNECT_DELAYS_MS.length - 1)] ?? 8e3;
132
+ await this.wait(delay);
133
+ if (this.disposed) return;
82
134
  }
135
+ diagnose(this.diagnostics, "error", `[incanto] multiplayer gave up after ${RECONNECT_ATTEMPTS} reconnect attempts — this client is offline and the others cannot see it. Call connect() again to retry.`);
83
136
  } finally {
84
137
  this.reconnecting = false;
85
138
  }
@@ -1,5 +1,5 @@
1
- import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-rm8vVzu_.js";
2
- import { a as Rng } from "./schema-B6ugCV1Q.js";
1
+ import { d as PropSchema, dt as Listener, jt as Node } from "./behavior-Dcz0fr1S.js";
2
+ import { t as Rng } from "./rng-Bb-IutXB.js";
3
3
 
4
4
  //#region src/core/boot-failure.d.ts
5
5
  /**