incanto 0.72.0 → 0.74.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 (84) hide show
  1. package/bin/incanto-check.mjs +114 -42
  2. package/bin/incanto-verify.mjs +9 -2
  3. package/dist/2d.d.ts +29 -4
  4. package/dist/2d.js +3 -3
  5. package/dist/3d.d.ts +137 -15
  6. package/dist/3d.js +6 -6
  7. package/dist/{agent8-BqZ0_O1u.js → agent8-CHTT4unP.js} +1 -1
  8. package/dist/{audio-player-ioZ7VKC6.d.ts → audio-player-BOrk4eQx.d.ts} +1 -1
  9. package/dist/{behavior-Bf9P9oB2.d.ts → behavior-J1cmVah0.d.ts} +8 -0
  10. package/dist/{create-game-CmFAtLZp.js → create-game-BAiA-FjP.js} +7 -7
  11. package/dist/{create-game-C9rWqqAC.js → create-game-ssvGvP7W.js} +7 -7
  12. package/dist/debug.d.ts +1 -1
  13. package/dist/debug.js +1 -1
  14. package/dist/editor.js +965 -893
  15. package/dist/{environment-presets-BeHoaGhy.js → environment-presets-BkqlWewf.js} +277 -60
  16. package/dist/{gameplay-BlQe-M07.js → gameplay-Bxe1sMVT.js} +611 -39
  17. package/dist/gameplay.d.ts +141 -7
  18. package/dist/gameplay.js +2 -2
  19. package/dist/index.d.ts +6 -4
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-CBfQzB6S.d.ts → loader-BAeWMYfF.d.ts} +1 -1
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-DrIgTlLk.js → physics-2d-C-SfvjzH.js} +94 -11
  25. package/dist/{physics-3d-BzOCw1VS.js → physics-3d-rOzeg890.js} +202 -15
  26. package/dist/{picking-C9McVJrO.js → picking-BMV34Pjl.js} +2 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/{register-5IYfooAN.js → register-CjociOtt.js} +19 -4
  30. package/dist/{register-C8HuRkOf.js → register-DRQyZKGU.js} +45 -9
  31. package/dist/{replay-datiAE-b.js → replay-B6BbgGM_.js} +42 -6
  32. package/dist/{replay-CBEChqXq.d.ts → replay-wZK5Ok5c.d.ts} +1 -1
  33. package/dist/{save-slots-CEuJPUle.js → save-slots-CEUGeLuB.js} +11 -2
  34. package/dist/{split-screen-Bm5DTElG.js → split-screen-BxZ5rBe7.js} +4 -4
  35. package/dist/{split-screen-DkqLng6q.d.ts → split-screen-D6sx8z3Z.d.ts} +2 -2
  36. package/dist/{sprite-animation-CqR2o3SA.js → sprite-animation-Bz3DHD_4.js} +1 -1
  37. package/dist/{src-DRSsRleD.js → src-CqtvYtSN.js} +1 -1
  38. package/dist/{test-Djuq3Vhq.js → test-CEl0y2Sw.js} +162 -30
  39. package/dist/test.d.ts +56 -5
  40. package/dist/test.js +2 -2
  41. package/dist/{touch-BnCyPA0G.js → touch-CioTZB-y.js} +1 -1
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-CMKz2cax.js → agent8-BlzturGi.js} +1 -1
  44. package/editor/assets/{debug-DLrQm-az.js → debug-BMUi8usj.js} +1 -1
  45. package/editor/assets/{index-BEKuxYcE.js → index-CrUCQoaB.js} +53 -53
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +634 -10
  49. package/skills/incanto-3d-character.md +113 -5
  50. package/skills/incanto-assets.md +9 -3
  51. package/skills/incanto-behaviors-and-scripts.md +26 -2
  52. package/skills/incanto-building-2d-games.md +9 -0
  53. package/skills/incanto-building-3d-games.md +45 -0
  54. package/skills/incanto-editor.md +6 -1
  55. package/skills/incanto-environment.md +7 -2
  56. package/skills/incanto-game-feel.md +16 -11
  57. package/skills/incanto-gameplay-behaviors.md +179 -16
  58. package/skills/incanto-node-reference.md +124 -45
  59. package/skills/incanto-physics-and-input.md +67 -5
  60. package/skills/incanto-scene-json-authoring.md +9 -3
  61. package/skills/incanto-verifying-your-game.md +19 -2
  62. package/templates-app/beacon-isle-3d/PROJECT/Status.md +8 -0
  63. package/templates-app/beacon-isle-3d/PROJECT/Structure.md +3 -0
  64. package/templates-app/beacon-isle-3d/package.json +1 -1
  65. package/templates-app/beacon-isle-3d/src/game.scene.json +136 -0
  66. package/templates-app/beacon-isle-3d/verify.ts +144 -0
  67. package/templates-app/molehill-2d/package.json +1 -1
  68. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  69. package/templates-app/platformer-2d/PROJECT/Structure.md +3 -0
  70. package/templates-app/platformer-2d/package.json +1 -1
  71. package/templates-app/platformer-2d/src/game.scene.json +126 -0
  72. package/templates-app/platformer-2d/verify.ts +54 -0
  73. package/templates-app/star-survivor/package.json +1 -1
  74. package/templates-app/tps-3d/PROJECT/Context.md +1 -1
  75. package/templates-app/tps-3d/PROJECT/Status.md +40 -1
  76. package/templates-app/tps-3d/PROJECT/Structure.md +19 -0
  77. package/templates-app/tps-3d/package.json +1 -1
  78. package/templates-app/tps-3d/src/behaviors.ts +9 -0
  79. package/templates-app/tps-3d/src/cover.scene.json +26 -0
  80. package/templates-app/tps-3d/src/game.scene.json +331 -75
  81. package/templates-app/tps-3d/src/main.ts +14 -0
  82. package/templates-app/tps-3d/src/pillar.scene.json +25 -0
  83. package/templates-app/tps-3d/verify.ts +582 -3
  84. package/templates-app/village-quest-3d/package.json +1 -1
@@ -27,17 +27,27 @@ character rides the hover spring, not floor contact, so friction would just brak
27
27
  movement. Declare `move` (vector2) / `jump` /
28
28
  `sprint` actions in the scene input map, and call
29
29
  `engine.input.attachPointer(canvas, { lockOnClick: true })` for mouse look.
30
+ The move vector is SCREEN space, y-down: `up` is `-y`, so in a harness
31
+ `setActionVector('move', 0, -1)` walks the character FORWARD (away from the
32
+ camera, -z at yaw 0) and `(0, 1)` walks it toward the camera — the guess that
33
+ drops a first probe off the back of the map.
30
34
 
31
35
  ## Movement model (vibe-starter-3d parity)
32
36
 
33
37
  Impulse-based on the dynamic body: target speed = `maxSpeed ×
34
38
  (1 + (sprintMultiplier−1)·intensity)` where keyboard intensity is 0.6
35
39
  (any move key) or 1.0 (+sprint). Defaults: maxSpeed 2.5, sprint ×2 → walk
36
- ≈4 m/s, sprint ≈5 m/s. `jumpVelocity` 4 (+20% at full sprint), falls get
40
+ ≈4 m/s, sprint ≈5 m/s. `jumpVelocity` 4 and `sprintJumpMultiplier` (1.2) scales with
41
+ the keyboard's INTENSITY, so a jump while walking is ×1.12 in velocity (+26% in height)
42
+ and while sprinting ×1.2 (+44%): standing 0.81 u, walking 1.01, sprinting 1.16 at the
43
+ defaults. Size a ledge against the moving jump, not the standing one; `incanto-feel`
44
+ prints all three. Falls get
37
45
  gravityScale 2.5 with a −20 m/s terminal clamp, and a hover spring holds the
38
- capsule BOTTOM `floatHeight` above the ground (5-ray probe). Author the Skin
39
- y-offset as `-(halfHeight + radius + floatHeight)` so the feet sit at the
40
- capsule bottom (the example templates do this).
46
+ capsule BOTTOM `floatHeight` above the ground (5-ray probe) in principle; the
47
+ spring sags about 0.13 m under gravity, so at the default `floatHeight` 0.01 the
48
+ capsule rests ON the floor (measured: bottom −0.001 m). Author the Skin y-offset
49
+ as `-(halfHeight + radius)` so the feet sit at the capsule bottom (the example
50
+ templates do this; adding `floatHeight` is off by that much).
41
51
 
42
52
  ## Game feel — the props a jump needs to stop feeling broken
43
53
 
@@ -54,17 +64,34 @@ did not until now.
54
64
  | `maxJumps` | `1` | `2` = double jump. Extra jumps work in mid-air. |
55
65
  | `airControl` | `0.2` | how much ground control you keep airborne (0 = committed, 1 = full). |
56
66
  | `fallGravity` | `2.5` | gravity multiplier while falling. Higher = snappier arc. |
67
+ | `wallJumpImpulse` | `[0, 0]` | `[away, up]` m/s — airborne and pressing INTO a wall, `jump` kicks off it: away along the wall's normal, up. Try `[6.5, 7]`. A fixed arc (the jump cut never halves it) and air control stands down for 0.18 s so the stick still pointing at the wall cannot steer the kick back into it. Emits `wallJumped(normal)`. |
68
+ | `wallSlideSpeed` | `0` | max fall speed while pressing into a wall (`fallGravity` stands down too). Try `2`. |
57
69
  | `platformCarry` | `true` | ride whatever you are standing on. Off = the controller steers in WORLD space, so standing still on a moving floor brakes you off it (measured: 95.9% of a moving deck's travel kept, 0.0% with this off). |
58
70
 
59
71
  Every one defaults to OFF (`0` / `1`) or to the previous hard-coded constant, so
60
72
  a scene that asks for nothing behaves exactly as before.
61
73
 
74
+ **The controller owns the wall-jump.** A kick written from a behaviour lasted
75
+ three frames on a platformer built from the tarball: the jump cut halved its
76
+ rise the moment the tap was released (7 → 2.2 m/s) and air control steered
77
+ the horizontal straight back into the wall (−6.5 → 0 in three steps). Anything
78
+ that fights the controller's own feel props has to live INSIDE them. The wall
79
+ is found by a short static-only ray in the input direction, so the character
80
+ must be pressing into it — a `RigidBody3D` has no `isOnWall`.
81
+
62
82
  ```json
63
83
  { "name": "Ctl", "type": "CharacterController3D",
64
84
  "props": { "coyoteSeconds": 0.12, "jumpBufferSeconds": 0.15,
65
85
  "jumpCutMultiplier": 0.45, "maxJumps": 2 } }
66
86
  ```
67
87
 
88
+ All six are composed in `examples/tps-3d` (the shipped third-person starter), and
89
+ `bunx incanto-feel` measures what they DO rather than echoing them — on that scene:
90
+ tapped apex 0.21 u vs held 0.81 u at the default `jumpVelocity` 4 — tps-3d itself, at 5,
91
+ measures 0.32 / 1.26 (`jumpCutMultiplier`), a double jump line
92
+ (`maxJumps: 2`), and the coyote and buffer windows still measured with a double
93
+ jump declared. It used to give up on both the moment `maxJumps` went above 1.
94
+
68
95
  **Jump reads the button EDGE.** Holding the jump key no longer re-jumps every
69
96
  frame — that auto-hop was never intended and it made coyote time incoherent (a
70
97
  held button would re-fire through the whole window).
@@ -75,6 +102,30 @@ props and the airborne animation use a stricter test: close to the support AND
75
102
  not moving upward. Without the velocity term the check is one frame stale, which
76
103
  at launch speed is ~6 cm — enough to still read as standing on the floor.
77
104
 
105
+ ### The two numbers a level is built against — composed and measured
106
+
107
+ `stepHeight` (0.35) and `jumpVelocity` (4) decide what a level can ask of the player, and
108
+ no example had set either. `tps-3d` now declares `stepHeight: 0.4` and `jumpVelocity: 5`
109
+ and its harness asserts each by what the player can DO: walked into a 0.3 m curb it rises
110
+ from y 0.84 to 1.14 and keeps going; walked into a 0.9 m ledge it stops at the face;
111
+ and the same scene with `jumpVelocity` back at 4 reaches 0.81 u where 5 reaches 1.26 u.
112
+ Read the number back from nothing — a curb you can climb is the proof.
113
+
114
+ ### The rig around the character — composed and measured
115
+
116
+ `cameraCollision` and `platformCarry` are on by default and no example had ever set or
117
+ measured them. `tps-3d` declares both and its harness asserts each by an effect: standing
118
+ 1.25 m in front of a wall with the boom pointing at it, the camera closes from 4.2 m to
119
+ 0.45 m; standing still on a `Lift` (a `StaticBody3D` walked by `Patrol`) for two seconds,
120
+ the lift travels 4 m and the rider drifts under 0.25 m off it.
121
+
122
+ `floatHeight` was composed too and taken back out. The hover spring holds the capsule
123
+ bottom `floatHeight` above the ground in principle, but it SAGS about 0.13 m under gravity,
124
+ so below that the capsule simply rests on contact and the prop changes nothing — measured
125
+ at rest: 0.01 → y 0.839, 0.05 → 0.840, 0.3 → 1.008, 0.6 → 1.308 for a capsule whose
126
+ contact height is 0.84. Leave it at the default and use `stepHeight` for ledges (the
127
+ docstring's own advice: a raised `floatHeight` makes the character weightless).
128
+
78
129
  ## Views
79
130
 
80
131
  | view | what it does | key props |
@@ -157,6 +208,14 @@ skin.animationUpper = '$anims/attack'; // spine-up plays the attack…
157
208
  - `animationUpperLoop: true` for sustained upper loops (carry, aim, wave).
158
209
  - Works with the controller's `animations` map untouched — locomotion logic
159
210
  never learns the attack exists.
211
+ - Composed in `examples/tps-3d`: `Shoot.fire()` sets
212
+ `skin.animationUpper = '$anims/shoot'` and nothing else — the run clip keeps
213
+ the legs, the shot rides the spine up. Measured on the CDN's
214
+ `mixamorig/shoot.glb` (4.03 s, 54 tracks retargeted onto `mixamorigSpine…`):
215
+ the layer's action is live 3 frames after the shot and still at weight 1 with
216
+ the base clip on `$anims/run`. While the clip GLB is still LOADING the layer
217
+ waits silently and starts when it lands — a shot fired in the first second of
218
+ a cold page plays late, not never.
160
219
 
161
220
  ## Animations without code
162
221
 
@@ -210,7 +269,11 @@ A sword in the hand, a hat on the head, sparks on a wingtip — parent them to a
210
269
  adjustments).
211
270
  - Purely VISUAL: it follows the rendered skeleton, so headless the node stays
212
271
  at its prop transform — keep hit checks range-based from the body, never
213
- bone-based.
272
+ bone-based. The sword MESH rides the bone; the sword HITBOX is an `Area3D`
273
+ on the body, aimed from the skin's yaw and armed for the swing's active
274
+ frames — the recipe, with its numbers, is under `DamageOnContact` in
275
+ `incanto-gameplay-behaviors.md` ("A melee swing"). Do not parent the hitbox
276
+ to the skin: physics composes ancestor offsets only, so it never turns.
214
277
  - `model.boneNames()` lists every bone at runtime when you need to hunt one
215
278
  (the editor's inspector offers the same list as a dropdown on the `bone` prop).
216
279
 
@@ -320,6 +383,51 @@ This cannot be an `auditScene` warning: a scene file has no velocity, and the bu
320
383
  is a behavior writing a heading at runtime. `facingReport()` from `incanto/test`
321
384
  is the same check for your own tests.
322
385
 
386
+ ## Facing the mouse in a top-down view (twin-stick)
387
+
388
+ In `quarter` view the mouse does not look, and the controller turns the skin
389
+ toward the MOVE direction — a twin-stick game wants it toward the CURSOR. Set
390
+ `skinPath: ""` so the controller leaves the skin alone (then drive
391
+ `skin.animation` yourself from `movementStateChanged`), and each frame put the
392
+ cursor on the ground and face it — `engine.toWorld(sx, sy)` is the renderer's
393
+ ray to the y=0 plane, `null` headless, so a harness sets the aim directly:
394
+
395
+ ```ts
396
+ const at = this.input.pointerPosition();
397
+ const w = at && this.engine.toWorld?.(at.x, at.y); // [x, y, z] | null
398
+ const [tx, tz] = w ? [w[0], w[2]] : this.aim ?? [NaN, NaN];
399
+ const dx = tx - player.position[0], dz = tz - player.position[2];
400
+ if (Math.hypot(dx, dz) > 0.2) skin.rotation = [0, Math.atan2(dx, dz) * RAD2DEG, 0];
401
+ ```
402
+
403
+ Measured on `examples/survivor-3d`: a cursor 10 m east is yaw 90°, and a bolt
404
+ given `direction: [dx, 0, dz]` flies where the skin looks. Boot with
405
+ `pointer: { lockOnClick: false }` — a locked cursor is no aim at all.
406
+
407
+ ## Lock-on and a dodge, in a few lines each
408
+
409
+ The controller's camera yaw is a public number in radians, and the rig's
410
+ forward is `(-sin yaw, 0, -cos yaw)` — so putting the camera behind the player
411
+ and on the enemy is one assignment per frame, measured on `examples/melee-3d`
412
+ (forward · toEnemy = 1.00):
413
+
414
+ ```ts
415
+ const dx = enemy.position[0] - player.position[0];
416
+ const dz = enemy.position[2] - player.position[2];
417
+ controller.yaw = Math.atan2(-dx, -dz); // every frame while locked
418
+ ```
419
+
420
+ A dodge is a dash plus i-frames that start BEFORE the hit — `Health.protect`,
421
+ not `invulnerableFor` (which opens only after one):
422
+
423
+ ```ts
424
+ (player.behavior as Health).protect(0.35); // immune for the roll
425
+ body.linearVelocity = [dir[0] * 9, body.linearVelocity[1], dir[2] * 9];
426
+ ```
427
+
428
+ Keep writing the velocity for the roll's frames — the controller brakes toward
429
+ its own target speed every step, so a single impulse dies in a few frames.
430
+
323
431
  ## First-person weapon viewmodel + the tracer-from-the-muzzle rule
324
432
 
325
433
  A weapon VIEWMODEL is just a `MeshInstance3D`/group child of the **root** `Camera`
@@ -9,7 +9,8 @@ description: Where game art comes from in Incanto — the FULL built-in catalog
9
9
  > installed engine version. Sibling skills live in `node_modules/incanto/skills/`.
10
10
 
11
11
  Resolve art in this order: ① built-ins (zero setup) → ② asset MCP servers /
12
- known URLs → ③ art-free primitives. Never invent asset URLs.
12
+ known URLs → ③ art-free primitives. Never invent asset URLs — `incanto-check`
13
+ asks every remote url's server whether it exists, and a 404 is missing art.
13
14
 
14
15
  **3D characters and props are step ②, always** — the built-in catalog is 2D
15
16
  sprites, tiles, items, foliage/terrain textures and audio, with no models in it.
@@ -182,8 +183,13 @@ does not contain:
182
183
 
183
184
  That used to be a browser-only failure — visible in `assetErrors()`, which needs
184
185
  a running game and someone to look at it — and it is the single most common way
185
- a scene draws nothing. Remote (`https:`) and inline (`data:`) urls are left
186
- alone: the command cannot know, and guessing would be worse than the bug.
186
+ a scene draws nothing. Remote (`https:`) urls are ASKED a HEAD request with a
187
+ four-second deadline, one per url per run and a 404 is the same warning
188
+ (`$avatar → https://…/base-model.glb is not found on its server (HTTP 404)`);
189
+ a host that cannot be reached is a `note:` and never a failure, so the check
190
+ works offline. Inline (`data:`) urls are left alone. This is the half of "never
191
+ invent asset URLs" a server can answer: a model at a made-up CDN path passed
192
+ every instrument except the browser before it.
187
193
 
188
194
  ## A spritesheet grid that does not fit says so
189
195
 
@@ -79,7 +79,7 @@ under an existing name (re-registering a different class without it → `DUPLICA
79
79
  ## What a Behavior can reach
80
80
 
81
81
  - `this.node` — the host node (cast to its type when needed)
82
- - `this.getNode(path)` / `this.emit(...)` / `this.on(...)` — node delegates
82
+ - `this.getNode(path)` / `this.getNodeOrNull(path)` / `this.emit(...)` / `this.on(...)` — node delegates
83
83
  - `this.engine` / `this.input` — via the scene tree (TREE_VIOLATION if the scene isn't
84
84
  set on an Engine)
85
85
  - Engine access in `onReady` (`this.engine` / `this.rng` / `this.log`) works under
@@ -299,12 +299,36 @@ what that cost: with the `plays` rung finally counting defects,
299
299
  the player walking off the terrain and falling forever. One `Respawn` node each,
300
300
  no TypeScript, and all five are clean.
301
301
 
302
+ ## Checkpoint (built-in behaviour — move where the Respawn puts you)
303
+
304
+ The second most common thing a platformer has. On an `Area3D`/`Area2D`:
305
+
306
+ ```json
307
+ { "name": "Check1", "type": "Area3D", "groups": ["checkpoint"],
308
+ "props": { "position": [7, 4.5, -37], "collider": { "shape": "box", "size": [3, 3, 3] } },
309
+ "script": { "name": "Checkpoint", "props": { "respawn": "/root/Player/Catch", "dropBelow": 0.6 } } }
310
+ ```
311
+
312
+ | Prop | Default | Meaning |
313
+ |---|---|---|
314
+ | `respawn` | `""` (required) | node path of the `Respawn` to redirect |
315
+ | `group` | `"player"` | who can light it (`""` = anything) |
316
+ | `once` | `true` | light it a single time; `false` re-fires on every entry |
317
+ | `dropBelow` | `0` | how far below the area's origin the respawn point sits, so the character lands ON the flag |
318
+
319
+ Touching it sets the `Respawn`'s `to` to the checkpoint's world position and
320
+ emits `activated(other)`; `lit` says whether it has fired. `Respawn.to` is read
321
+ at the moment of the catch, so a checkpoint lit mid-run is where the next fall
322
+ ends — it used to be read once at ready, which is what a platformer built from
323
+ the tarball found. `examples/platformer-3d` has two.
324
+
302
325
  ## Timer (core node — never setTimeout in game logic)
303
326
 
304
327
  ```json
305
328
  { "name": "Spawner", "type": "Timer", "props": { "waitTime": 2, "autostart": true } }
306
329
  ```
307
- Emits `timeout` every `waitTime` s (`oneShot` for once). API: `start(time?)`, `stop()`, `running`.
330
+ Emits `timeout` every `waitTime` s (`oneShot` for once). API: `start(time?)`, `stop()`, `running`,
331
+ `timeLeft` (seconds to the next `timeout`, `0` when stopped — the countdown a HUD draws).
308
332
  A `waitTime` of 0 is a load error: the update guard stops a timer with no period
309
333
  on its first frame, so it would never fire and never say so.
310
334
 
@@ -644,3 +644,12 @@ if (!isWebGLAvailable()) showMyOwnBrowserTooOldScreen(); // ask BEFORE booting
644
644
 
645
645
  `isWebGLAvailable()` probes a throwaway canvas and never touches yours; it
646
646
  returns `false` outside a browser.
647
+
648
+ ## Surfaces and a chaser that walks (2D)
649
+
650
+ A `StaticBody2D` carries the surface it is — `restitution 1` is a trampoline, `friction 0`
651
+ is ice — and its value WINS the pair (see `incanto-physics-and-input.md`). A platformer's
652
+ ground enemy is a `CharacterBody2D` with `Chase` on a child and `ground: true`, so a player
653
+ on a platform is not met by an enemy floating up to it. platformer-2d's course past its last
654
+ ground slab is all three, measured by its harness.
655
+
@@ -116,6 +116,9 @@ never be one node each. One instanced node draws them all at once:
116
116
  place.
117
117
  `{ "shape": "sphere", "radius": r }` / `{ "shape": "box", "size": [x,y,z] }`
118
118
  opt out into a cheaper approximation when a rough blocker is all you want.
119
+ The field follows its stones: replace `transforms`, move the node, or change
120
+ `mesh`, `size` or the `collider` spec at runtime and the hulls are rebuilt on the
121
+ next physics step.
119
122
 
120
123
  ### `LoftMesh3D` (smooth swept hulls — vehicle bodies, canopies)
121
124
 
@@ -142,6 +145,14 @@ saturated in sunlight: `metalness ~0.45, roughness ~0.35, clearcoat 1,
142
145
  clearcoatRoughness 0.06, envMapIntensity ~0.9` — high metalness washes the base colour
143
146
  out into the sky reflection.
144
147
 
148
+ **A 3D node with no art draws nothing, and `incanto-check` says so.** A
149
+ `ModelInstance3D` with no `model`, a `Sprite3D` with no `texture`, an
150
+ `AnimatedSprite3D` with no `sheet`, an `InstancedMesh3D` with no `transforms`
151
+ each load clean, validate, take a row in the editor's tree, and put no pixel on
152
+ screen. The audit names them (a template on a `PrefabShelf` and a node authored
153
+ `visible: false` are left alone) — the same rule `TileMap2D` got the day the
154
+ tilemap example shipped a floor nobody could see.
155
+
145
156
  ### `Sprite3D` / `AnimatedSprite3D` (2D billboard sprites — the 2.5D look)
146
157
  A 2D image drawn as a camera-facing quad inside the 3D world (Octopath / Don't
147
158
  Starve / classic MapleStory in a 3D scene). Unlit — the art shows as-is. The
@@ -318,6 +329,40 @@ notice in a screenshot.
318
329
  (~60–80) so the box tracks the camera and contact shadows stay crisp everywhere.
319
330
  - `OmniLight3D` — `color`, `intensity` (physical units — values like 20–40 are normal),
320
331
  `range` (0 = unlimited).
332
+ - `SpotLight3D` — a CONE that aims: a flashlight, a searchlight sweeping an arena,
333
+ headlights, a stage spot, a guard's torch. `color`, `intensity` (candela, physical
334
+ decay — see the numbers below), `range` (0 = unlimited), `coneDeg` (the FULL cone
335
+ angle, default 60 — a "30° beam" is 30; 0 and 180 are load errors), `penumbra`
336
+ (0 hard edge … 1 all falloff, default 0.3), `castShadow` + `shadowMapSize` (1024;
337
+ a perspective shadow from the apex — one extra render per frame per light), and
338
+ `lookAt` (a node path: aim at it every frame, exactly as `Camera3D.lookAt` does).
339
+ **It aims down its −Z like a camera**: at rest it points along −Z,
340
+ `rotation: [-90, 0, 0]` points it at the floor, and a headlamp on a +Z-forward
341
+ skin wants `rotation: [0, 180, 0]` (tilt it down with `[20, 180, 0]`).
342
+
343
+ **Intensity is candela with physical falloff**, so the number is about
344
+ DISTANCE: measured on tps-3d-neon's dark deck, a headlamp 2 m off the floor
345
+ reads well at ~500, and a searchlight 14 m up needs ~12 000 to leave a pool you
346
+ can see — 260 was invisible. Start ten times bigger than feels reasonable.
347
+
348
+ **Do not yaw a pitched light on the same node.** Rotation is Euler XYZ, so a
349
+ yaw applied to a node that already pitches −55° swings the cone sideways and,
350
+ at yaw 180, straight UP (measured: dir `(0, 0.82, 0.57)`). To SWEEP, put the
351
+ yaw on a parent pivot and the pitch on the light; to TRACK, use `lookAt`:
352
+
353
+ ```json
354
+ { "name": "SearchlightRig", "type": "Node3D", "props": { "position": [0, 14, 0] },
355
+ "script": { "name": "Oscillate", "props": { "mode": "rotation", "axis": "y", "amplitude": 70, "frequency": 0.08 } },
356
+ "children": [
357
+ { "name": "Searchlight", "type": "SpotLight3D",
358
+ "props": { "rotation": [-55, 0, 0], "color": "#ffe9b0", "intensity": 12000, "range": 60,
359
+ "coneDeg": 24, "penumbra": 0.4, "castShadow": true } }
360
+ ] }
361
+ ```
362
+ ```json
363
+ { "name": "Tracker", "type": "SpotLight3D",
364
+ "props": { "position": [0, 14, 0], "intensity": 12000, "coneDeg": 20, "lookAt": "%Player" } }
365
+ ```
321
366
 
322
367
  ### Vegetation & water
323
368
 
@@ -169,6 +169,10 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
169
169
  with the cursor whatever the world zoom is. (Picking used to look only at the
170
170
  world pass, so a HUD could not be selected in the viewport at all, and its
171
171
  outline was drawn wherever the game camera happened to be looking.)
172
+ A `HudLayer`'s DOM is drawn over the edit view but never takes the pointer
173
+ there — a centred pause menu used to sit exactly where the selected node's
174
+ gizmo is and eat the drag (a rotate ring under it never saw a pointerdown; a
175
+ move drag toggled Mute). Its widgets are live again in play.
172
176
  **`F` frames the SELECTED node** (its whole subtree; a light or empty node has
173
177
  no bounds, so the camera goes to it at a readable distance) — and the whole
174
178
  scene when nothing is selected, which is the Maya/Unity/Unreal meaning of the
@@ -247,7 +251,8 @@ files are formatted biome-compatibly (80-col inline-when-fits), so saving from
247
251
  the editor never breaks `pnpm lint`.
248
252
 
249
253
  Save activates only when the working scene differs from the original
250
- (`Ctrl/Cmd+S`); undo is `Ctrl/Cmd+Z`. Saving writes pretty-printed JSON to
254
+ (`Ctrl/Cmd+S`); undo is `Ctrl/Cmd+Z`, and it keeps your selection whenever the
255
+ selected node survives the undo (only undoing the add that created it deselects). Saving writes pretty-printed JSON to
251
256
  `--output` (default: the input file).
252
257
 
253
258
  ## The agent8 asset library (📚)
@@ -197,7 +197,12 @@ carries the heightfield as `[x, y, z, tile]` tuples (value noise; grass
197
197
  top, dirt fill, bedrock base). Reach for it when BLOCKS are the point —
198
198
  digging, building, minecraft-style games on `VoxelGrid3D` (colliders are
199
199
  the game's job: chunk trimeshes near the player, see the minecraft
200
- template). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
200
+ template). Baked `voxels` land WHEN THE PROP DOES — at load, before any
201
+ `onReady` — so a chunk builder at ready already sees them; a generator that
202
+ also fills the grid at ready must MERGE them, because `setBlocks` replaces
203
+ the map: `grid.setBlocks([...generated, ...grid.blocks()])`, baked blocks
204
+ last so a baked block wins over the terrain the noise put there (the
205
+ minecraft template's beacon tower is exactly this). Options: `seed`, `size` (32 blocks), `height` (8), `water`. The
201
206
  voxel `generateIsland` (radial dome falloff, sandy shore) also remains for
202
207
  the same use case.
203
208
 
@@ -800,7 +805,7 @@ hue reads grey.
800
805
  | prop | default | meaning |
801
806
  |---|---|---|
802
807
  | `size` | `[40, 40]` | [width, depth] meters — must be positive (load-time check) |
803
- | `color` | `"#2a6fbe"` | THE lake hue for `simple` (deep/surface/sky derived from it — use a vivid color); if customized it also tints the fancy ramp |
808
+ | `color` | `"#2a6fbe"` | THE lake hue for `simple` (deep/surface/sky derived from it — use a vivid color); if customized it also tints the fancy ramp — but only the parts `colors` (or a preset) leaves unnamed: with trough, surface and peak all set it only seeds the underwater murk, and the editor's inspector says so under the field |
804
809
  | `opacity` | `0.8` | water-body density: lower = clearer (scales how much the refracted scene shows through) |
805
810
  | `waveHeight` | `0.04` | wave intensity, meters-ish (0 = mirror-flat; 0.04 = calm sea; 0.08 = the older choppier look) |
806
811
  | `waveSpeed` | `1` | animation speed multiplier |
@@ -22,10 +22,11 @@ feel of /Game/Player
22
22
  input → motion 1 frame(s)
23
23
 
24
24
  jump apex 0.262 u (tapped)
25
- held apex 0.981 u ← size your level against THIS
25
+ held apex 0.981 u (standing)
26
+ moving apex 1.24 u walking · 1.41 u sprinting ← size your level against the LARGEST
26
27
  time to apex 133 ms
27
28
  airtime 267 ms
28
- coyote window 100 ms
29
+ coyote window 117 ms
29
30
  jump buffer 150 ms
30
31
  ```
31
32
 
@@ -55,19 +56,23 @@ peak 300 u/s ← SOMETHING ELSE moved it
55
56
  Measure feel somewhere nothing can hit you.
56
57
 
57
58
  **A character that jumps in MID-AIR has no coyote window to measure.** The probe
58
- lifts the body, waits, presses, and reads "vertical velocity went up" as a jump —
59
- so a double jump answers yes at any delay, and so does any behaviour that pushes
60
- the body up while `jump` is held (a glide, a jetpack, an updraft). It used to
61
- print `coyote window always (no window)`, which reads as the opposite of the
62
- truth, on two of the six shipped starters one of which declares
63
- `coyoteSeconds: 0.12` five lines from its glide. It says
59
+ lifts the body, waits, presses, and reads "going UP after the press, and sent
60
+ there by it" as a jump
61
+ so a double jump (or a jetpack, an updraft) answers yes at any delay. It used to
62
+ read "velocity CHANGED upward" instead, which a glide satisfies too: Beacon
63
+ Isle's glide clamps a −20 m/s fall to −1.6, a +18 change, and the report called
64
+ its coyote window `not measurable jumps in MID-AIR` about a character that
65
+ does not, with the buffer beside it marked suspect. Now it reads
64
66
 
65
67
  ```
66
- coyote window not measurable — this character jumps in MID-AIR
67
- jump buffer 33 ms (suspect — see above)
68
+ coyote window 117 ms (declared 120)
69
+ jump buffer 33 ms (declared 150)
68
70
  ```
69
71
 
70
- and names the declared value it cannot confirm. To measure the window, probe a
72
+ and the 33 is true, not suspect: holding `jump` while falling IS the glide,
73
+ so a press more than two frames before landing becomes a one-frame glide and
74
+ the buffered jump is spent. A starter that glides on the jump button buys a
75
+ short buffer with it. To measure the window of a true mid-air jumper, probe a
71
76
  build with the air jump off.
72
77
 
73
78
  **`jumpVelocity: 4` is not a height.** It is an impulse, and what it produces