incanto 0.74.0 → 0.75.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 (92) hide show
  1. package/bin/incanto-multiplay.mjs +52 -4
  2. package/dist/2d.d.ts +83 -7
  3. package/dist/2d.js +4 -3
  4. package/dist/3d.d.ts +743 -167
  5. package/dist/3d.js +6 -6
  6. package/dist/{agent8-CHTT4unP.js → agent8-B-bh3n0X.js} +1 -1
  7. package/dist/{audio-player-BOrk4eQx.d.ts → audio-player-BRo2uvG6.d.ts} +23 -2
  8. package/dist/{behavior-J1cmVah0.d.ts → behavior-B_245qRy.d.ts} +15 -1
  9. package/dist/{create-game-BAiA-FjP.js → create-game-DVUhHs2B.js} +79 -10
  10. package/dist/{create-game-ssvGvP7W.js → create-game-DnwhJ5nW.js} +83 -7
  11. package/dist/debug.d.ts +1 -1
  12. package/dist/{editor-switch-CFU9mCec.d.ts → editor-switch-O8mxpgRX.d.ts} +1 -1
  13. package/dist/editor.js +1600 -1529
  14. package/dist/env.d.ts +1 -1
  15. package/dist/{environment-presets-BkqlWewf.js → environment-presets-BP77Dl0K.js} +2005 -60
  16. package/dist/{gameplay-Bxe1sMVT.js → gameplay-D-JuGura.js} +9309 -6595
  17. package/dist/gameplay.d.ts +573 -31
  18. package/dist/gameplay.js +3 -2
  19. package/dist/index.d.ts +150 -6
  20. package/dist/index.js +6 -6
  21. package/dist/{loader-BAeWMYfF.d.ts → loader-CbkVdXL8.d.ts} +2 -2
  22. package/dist/net.d.ts +2 -2
  23. package/dist/net.js +2 -2
  24. package/dist/{physics-2d-C-SfvjzH.js → physics-2d-x3bCrQf0.js} +250 -31
  25. package/dist/{physics-3d-rOzeg890.js → physics-3d-Bz2VPQ6D.js} +673 -51
  26. package/dist/{picking-BMV34Pjl.js → picking-Buita5T9.js} +2 -2
  27. package/dist/{save-slots-CEUGeLuB.js → pose-ByFB_J3O.js} +217 -11
  28. package/dist/react.d.ts +2 -2
  29. package/dist/react.js +1 -1
  30. package/dist/{register-CjociOtt.js → register-B768Qedj.js} +596 -2
  31. package/dist/{register-DRQyZKGU.js → register-DxAbTO2T.js} +191 -430
  32. package/dist/{replay-B6BbgGM_.js → replay-DrqQ6dZj.js} +133 -18
  33. package/dist/{replay-wZK5Ok5c.d.ts → replay-W5nCw_cU.d.ts} +21 -2
  34. package/dist/{rng-Bb-IutXB.d.ts → rng-BsXZg3D6.d.ts} +7 -0
  35. package/dist/{split-screen-BxZ5rBe7.js → split-screen-B-dIcQsQ.js} +10 -5
  36. package/dist/{split-screen-D6sx8z3Z.d.ts → split-screen-CiLJf2-C.d.ts} +3 -3
  37. package/dist/{sprite-animation-Bz3DHD_4.js → sprite-animation-SQa5gIu2.js} +8 -6
  38. package/dist/{src-CqtvYtSN.js → src-DLKVc6Hj.js} +1 -1
  39. package/dist/{test-CEl0y2Sw.js → test-HlVUeCSn.js} +487 -82
  40. package/dist/test.d.ts +90 -12
  41. package/dist/test.js +2 -2
  42. package/dist/vite.js +3 -3
  43. package/editor/assets/{agent8-BlzturGi.js → agent8-BXlYM9rv.js} +1 -1
  44. package/editor/assets/{debug-BMUi8usj.js → debug-S42XGP4Y.js} +1 -1
  45. package/editor/assets/index-DgrgUKno.js +11046 -0
  46. package/editor/index.html +1 -1
  47. package/package.json +1 -1
  48. package/schemas/scene.schema.json +1630 -299
  49. package/skills/incanto-3d-character.md +358 -20
  50. package/skills/incanto-audio.md +24 -2
  51. package/skills/incanto-building-2d-games.md +21 -2
  52. package/skills/incanto-building-3d-games.md +38 -3
  53. package/skills/incanto-editor.md +17 -6
  54. package/skills/incanto-environment.md +227 -2
  55. package/skills/incanto-game-feel.md +60 -1
  56. package/skills/incanto-gameplay-behaviors.md +668 -20
  57. package/skills/incanto-hud.md +116 -0
  58. package/skills/incanto-multiplayer.md +141 -2
  59. package/skills/incanto-node-reference.md +393 -16
  60. package/skills/incanto-performance.md +32 -0
  61. package/skills/incanto-physics-and-input.md +241 -13
  62. package/skills/incanto-playtesting.md +2 -2
  63. package/skills/incanto-save-slots.md +61 -2
  64. package/skills/incanto-scene-json-authoring.md +13 -0
  65. package/skills/incanto-verifying-your-game.md +86 -3
  66. package/templates-app/beacon-isle-3d/PROJECT/Status.md +12 -0
  67. package/templates-app/beacon-isle-3d/package.json +1 -1
  68. package/templates-app/beacon-isle-3d/src/game.scene.json +151 -2
  69. package/templates-app/molehill-2d/PROJECT/Status.md +7 -0
  70. package/templates-app/molehill-2d/package.json +1 -1
  71. package/templates-app/molehill-2d/src/game.scene.json +129 -2
  72. package/templates-app/platformer-2d/PROJECT/Status.md +7 -0
  73. package/templates-app/platformer-2d/docs/project-2d-rules.md +3 -2
  74. package/templates-app/platformer-2d/package.json +1 -1
  75. package/templates-app/platformer-2d/src/behaviors.ts +40 -26
  76. package/templates-app/platformer-2d/src/game.scene.json +144 -2
  77. package/templates-app/platformer-2d/verify.ts +18 -1
  78. package/templates-app/star-survivor/PROJECT/Status.md +7 -0
  79. package/templates-app/star-survivor/package.json +1 -1
  80. package/templates-app/star-survivor/src/behaviors.ts +9 -8
  81. package/templates-app/star-survivor/src/game.scene.json +142 -4
  82. package/templates-app/star-survivor/verify.ts +13 -0
  83. package/templates-app/tps-3d/PROJECT/Status.md +6 -0
  84. package/templates-app/tps-3d/package.json +1 -1
  85. package/templates-app/tps-3d/src/game.scene.json +71 -10
  86. package/templates-app/village-quest-3d/PROJECT/Status.md +7 -0
  87. package/templates-app/village-quest-3d/package.json +1 -1
  88. package/templates-app/village-quest-3d/src/behaviors.ts +8 -16
  89. package/templates-app/village-quest-3d/src/grove.scene.json +121 -5
  90. package/templates-app/village-quest-3d/src/village.scene.json +126 -2
  91. package/templates-app/village-quest-3d/verify.ts +52 -2
  92. package/editor/assets/index-CrUCQoaB.js +0 -11046
@@ -64,6 +64,13 @@ did not until now.
64
64
  | `maxJumps` | `1` | `2` = double jump. Extra jumps work in mid-air. |
65
65
  | `airControl` | `0.2` | how much ground control you keep airborne (0 = committed, 1 = full). |
66
66
  | `fallGravity` | `2.5` | gravity multiplier while falling. Higher = snappier arc. |
67
+
68
+ And one that is about the CAMERA rather than the jump:
69
+
70
+ | prop | default | what it buys |
71
+ |---|---|---|
72
+ | `pitchStart` | `0` | where the camera RESTS before anyone looks around, in degrees. Negative looks down; `-14` is a good third-person default. On a PHONE it stops being a nicety: a portrait screen is very tall, so a level camera spends its top half on sky and a player with no mouse cannot fix it before they start dragging. |
73
+ | `mantleHeight` | `0` | the pull-up: on the way down from a jump, a ledge ahead no higher than this above the capsule's bottom is caught and pulled up onto. A jump a hand short of a roof is the most common near-miss in a platformer; try `0.9`. `0` = a jump near a wall feels exactly as it did. |
67
74
  | `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
75
  | `wallSlideSpeed` | `0` | max fall speed while pressing into a wall (`fallGravity` stands down too). Try `2`. |
69
76
  | `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). |
@@ -71,6 +78,15 @@ did not until now.
71
78
  Every one defaults to OFF (`0` / `1`) or to the previous hard-coded constant, so
72
79
  a scene that asks for nothing behaves exactly as before.
73
80
 
81
+ **Getting hit is the controller's too.** When the body's `Health` (on the body,
82
+ or on a child such as `Vitals`) declares `knockback` / `knockUp` /
83
+ `staggerSeconds`, the controller drops the stick, the jump and the dash for the
84
+ stagger and writes the kick as the body's velocity — `knockUp` is the hop. A
85
+ club with `DamageOnContact` and a player Health of `knockback 7, knockUp 2.5,
86
+ staggerSeconds 0.2` is a hit that knocks the player half a metre back, off the
87
+ ground, and takes the controls for a fifth of a second — no game code. See
88
+ "Hit reactions" in `incanto-gameplay-behaviors.md`.
89
+
74
90
  **The controller owns the wall-jump.** A kick written from a behaviour lasted
75
91
  three frames on a platformer built from the tarball: the jump cut halved its
76
92
  rise the moment the tap was released (7 → 2.2 m/s) and air control steered
@@ -92,6 +108,13 @@ measures 0.32 / 1.26 (`jumpCutMultiplier`), a double jump line
92
108
  (`maxJumps: 2`), and the coyote and buffer windows still measured with a double
93
109
  jump declared. It used to give up on both the moment `maxJumps` went above 1.
94
110
 
111
+ **`stepHeight` steps onto a ledge at the APEX of a jump too.** A fence 1.5 m
112
+ high was climbed by a jump that peaks at 1.3 m: at the apex the fence top sat
113
+ 0.2 m above the capsule's bottom, inside `stepHeight` 0.4, and the step-up
114
+ lifted the character over. That is what makes landing on a ledge forgiving; a
115
+ wall that must stop a walker is jump apex + `stepHeight` tall (`examples/steed-3d`'s
116
+ fence is 2 m).
117
+
95
118
  **Jump reads the button EDGE.** Holding the jump key no longer re-jumps every
96
119
  frame — that auto-hop was never intended and it made coyote time incoherent (a
97
120
  held button would re-fire through the whole window).
@@ -126,6 +149,248 @@ at rest: 0.01 → y 0.839, 0.05 → 0.840, 0.3 → 1.008, 0.6 → 1.308 for a ca
126
149
  contact height is 0.84. Leave it at the default and use `stepHeight` for ledges (the
127
150
  docstring's own advice: a raised `floatHeight` makes the character weightless).
128
151
 
152
+ ## Swimming (water too deep to stand in)
153
+
154
+ A `Water3D` is a surface with no bottom, and until now the controller never
155
+ asked it anything: a player who walked off a beach into a four-metre lagoon
156
+ stood on the bed with the camera underwater and the run clip playing, in every
157
+ 3D scene with water. Now the body SWIMS:
158
+
159
+ | prop | default | what it buys |
160
+ |---|---|---|
161
+ | `swimSpeed` | `1.6` | the pace a plain key gives in water (m/s; `sprint` still multiplies, by the ratio it multiplies a run). **`0` turns swimming OFF** — the body sinks to the bed, which is what a drowning pit wants and what every scene got before. |
162
+ | `swimDepth` | `0.35` | how far the body's origin rides BELOW the surface. The default keeps a 1.7 m character's head out of the water. |
163
+ | `diveAction` | `"dive"` | held: swim DOWN at `swimSpeed`. Not in the input map = no diving, silently. |
164
+
165
+ - **When.** The body swims when floating would put it HIGHER than standing:
166
+ the waterline (`surface − swimDepth`) above where the hover spring would
167
+ hold it on the bed it can see, or no bed within reach. That one comparison
168
+ is the whole wade-in/wade-out — walk down a beach and the water takes you
169
+ where the bed drops away; swim toward a shelf and the spring takes over
170
+ where it rises. No zones, no triggers.
171
+ - **What.** Gravity is off, a spring holds the origin at the waterline (a
172
+ swimmer rides the swell the way a `Buoyancy` raft does — the same
173
+ `heightAt`), horizontal control is full (water gives purchase), and the
174
+ state is `swim` for the `animations` map / `movementStateChanged`.
175
+ - **Diving.** Hold `dive` to go down; let go and you rise on your own. With
176
+ the head under (`controller.submerged`), holding `jump` swims UP. Air,
177
+ drowning and what is down there are your game's business — read
178
+ `controller.submerged` in a behavior and count.
179
+ - **Getting out.** A swimmer's step is measured from the WATER, not from the
180
+ foot: swim into a shelf, a dock or a rock whose top is no more than
181
+ `stepHeight` above the surface and the body CLIMBS out on its own — driven
182
+ up at 2 m/s until its bottom is level with the ledge, then onto it. (The
183
+ capsule hangs `swimDepth + halfHeight` under the surface, so from its bottom
184
+ a shelf a hand under the waterline would be a 0.6 m wall; a swimmer at that
185
+ shelf simply puts their hands on it. The hover spring could not do this —
186
+ it is sized to hold a capsule a hair off the ground and stalled with the
187
+ body's round bottom a third of a metre under the ledge, pressed to its
188
+ face.) Higher than `stepHeight` above the water it is a wall, exactly as on
189
+ land. `jump` at the surface hops — SIZED to clear the surface by
190
+ `stepHeight`, whatever `jumpVelocity` says — and the same climb takes a
191
+ ledge on the way down. So author a dock or a rock a swimmer should reach
192
+ **no more than `stepHeight` above the water**. A beach needs nothing: the
193
+ bed rises, the standing height passes the waterline, and you walk out.
194
+ - **Readbacks.** `controller.swimming`, `controller.submerged`.
195
+ - **Found on the way (fixed, same release):** the ground probe's side rays
196
+ start `radius` out from the capsule's centre, so pressed against a wall one
197
+ of them started INSIDE the wall and hit it at distance zero — a character
198
+ hugging a wall in mid-air read as standing, the ground jump came back while
199
+ falling, and a hop out of the water at a rock's face was cancelled the frame
200
+ it started. A zero-distance hit is ignored now. And `wallSlideSpeed` held a
201
+ third of a metre a second loose (the solver added one tick of gravity back
202
+ after the clamp); at the cap gravity stands down.
203
+
204
+ ```json
205
+ { "name": "Controller", "type": "CharacterController3D",
206
+ "props": { "view": "free", "swimSpeed": 1.8, "stepHeight": 0.5,
207
+ "animations": { "idle": "$idle", "run": "$run", "swim": "$anims/swim" } } }
208
+ ```
209
+
210
+ Composed in `examples/lagoon-3d` (a pearl dive with an air meter); the
211
+ mixamorig `swim.glb` clip is on the CDN beside the locomotion set.
212
+
213
+ ## Ladders
214
+
215
+ Every 3D game with a second floor has one, and until now an `Area3D`
216
+ against a wall was a box the player walked into. A ladder is any `Area3D` in
217
+ the controller's `ladderGroup` — no node type, no code:
218
+
219
+ ```json
220
+ { "name": "Ladder", "type": "Area3D", "groups": ["ladder"],
221
+ "props": { "position": [-3, 2.2, 0.35], "collider": { "shape": "box", "size": [1, 4.4, 0.7] } },
222
+ "children": [ …rails and rungs as MeshInstance3D… ] }
223
+ ```
224
+
225
+ | prop | default | what it buys |
226
+ |---|---|---|
227
+ | `climbSpeed` | `1.5` | up and down the rungs, m/s, from the stick's OWN vertical (W/S — the camera gets no say). **`0` = no ladders.** |
228
+ | `ladderGroup` | `"ladder"` | which `Area3D`s are ladders |
229
+
230
+ - **Grabbing.** Push UP into a ladder you overlap and you are on it: gravity
231
+ off, held to the rungs, state `climb`. Walking PAST one never grabs it —
232
+ only pushing up does. Falling past one while holding DOWN grabs it too,
233
+ which is how you climb down from a ledge: stand at its edge over the
234
+ ladder, hold back, step off.
235
+ - **The top.** The same climb that takes a swimmer out of the water takes the
236
+ body onto the ledge: the ladder should lean on the FACE of the thing you
237
+ are climbing onto (a terrace, a wall with a platform on top, a loft's
238
+ edge) so the ledge is AHEAD of the climber — a ladder up through a hole in
239
+ a floor has its ledge to the sides, and the climb-out looks forward.
240
+ - **The bottom.** Down while standing at its foot steps off.
241
+ - **Letting go.** `jump` pushes off the rungs, away from the ladder, with a
242
+ moment's lockout so it is not grabbed straight back.
243
+ - **Animation.** The state is `climb` — map it in `animations`. The CDN's
244
+ mixamorig set has no climbing clip; `walk` reads as climbing well enough
245
+ on the rungs (`examples/tower-3d` does that).
246
+ - **Readback.** `controller.onLadder`.
247
+
248
+ Composed in `examples/tower-3d` ("Lantern Tower": three terraces, three
249
+ ladders, a sweeping beam and a lantern that wants three flasks).
250
+
251
+ ## Mantling (the pull-up)
252
+
253
+ A jump at `jumpVelocity: 5` rises 1.27 m and the capsule's round bottom rides
254
+ a corner up to ~0.3 m more, so a 1.6 m roof is cleared and a 2 m one is a
255
+ wall — a hand short. `mantleHeight: 0.9` catches it: on the way down from
256
+ ANY jump, while the stick is held toward the ledge, a top ahead within that
257
+ height of the capsule's bottom is taken by the same climb that takes a
258
+ swimmer out of the water and a climber off a ladder (driven up at 2 m/s,
259
+ then onto it; state `climb` meanwhile). Above the reach it is still a wall;
260
+ a vertical hop beside a ledge is still a hop. Composed in
261
+ `examples/rooftops-3d`.
262
+
263
+ ## Crouching (`crouchHeight`)
264
+
265
+ A duct to crawl through, a laser to duck under, a desk to hide behind:
266
+ every stealth game and shooter has a crouch, and the controller could not
267
+ make itself shorter. One prop turns it on:
268
+
269
+ ```json
270
+ { "name": "Ctl", "type": "CharacterController3D",
271
+ "props": { "crouchHeight": 0.3, "crouchSpeedMultiplier": 0.5 } }
272
+ ```
273
+
274
+ - **`crouchHeight`** (m, `0` = no crouch) is the capsule's `height` while the
275
+ `crouch` key is held — the standing capsule is `height` + 2 × `radius`
276
+ tall (1.6 m for the default rig), the crouched one `crouchHeight` +
277
+ 2 × `radius` (0.94 m), so a duct with a metre of headroom is crawled
278
+ through and stops a standing character at its mouth. The body sinks by
279
+ half the difference in the same step, so the feet stay put and the eye
280
+ drops with them in first person; the Skin's authored offset is lifted by
281
+ the same amount so the model's feet stay on the floor, and restored on
282
+ standing.
283
+ - **Hold-to-crouch.** The key let go stands you up — if there is headroom.
284
+ A ray from the capsule's top asks; under a duct you stay down until you
285
+ are out from under it, and `crouching` reads true the whole time. A jump
286
+ press while the key is held is not a hop and not a stand.
287
+ - **`crouchSpeedMultiplier`** (0.5) is the pace, as a fraction of the standing
288
+ pace. `state` reads `crouch` (still) and `sneak` (moving), so the
289
+ `animations` map takes clips for both.
290
+ - Declare the action: `"crouch": { "type": "button", "keys": ["KeyC", "ControlLeft"] }`
291
+ (`crouchAction` renames it). A missing action is tolerated and reported
292
+ once, like `sprint`.
293
+
294
+ `examples/vents-3d` is the composition: ducts, a laser grid at head height,
295
+ a guard's sightline you crouch under.
296
+
297
+ ## A death that falls — `Ragdoll3D`
298
+
299
+ Every enemy in every example died the same way: the model played a clip, or
300
+ vanished. Nothing crumpled, slid down a slope or hung off the rail, because a
301
+ ragdoll is ten bodies and nine joints built from a skeleton nobody wanted to
302
+ write. One node under the body, beside the Skin:
303
+
304
+ ```json
305
+ { "name": "Brute", "type": "CharacterBody3D", "children": [
306
+ { "name": "Skin", "type": "ModelInstance3D", "props": { "model": "$base" } },
307
+ { "name": "Hp", "type": "Node", "script": { "name": "Health", "props": { "max": 50 } } },
308
+ { "name": "Ragdoll", "type": "Ragdoll3D", "props": { "target": "../Skin", "lifetime": 8 } }
309
+ ] }
310
+ ```
311
+ ```jsonc
312
+ { "signal": "died", "from": "Brute/Hp", "to": "Brute/Ragdoll", "handler": "activate" }
313
+ ```
314
+
315
+ - **Idle it is nothing.** `activate()` reads the skeleton at `target`
316
+ (Mixamo names — `Hips`, `Spine`, `Head`, `LeftArm`… with the forgiving
317
+ lookup `findBone` uses), builds a capsule `RigidBody3D` per limb where the
318
+ bones are — torso, head, upper arms, forearms, thighs, shins — joins each
319
+ to its parent with a spherical `Joint3D`, gives them the velocity of the
320
+ body they fell out of, and from then on writes the bones' world matrices
321
+ from the limbs every frame. The limbs go under the scene root in the
322
+ `ragdoll` group; `limbs()` lists them.
323
+ - **Turn the rest off beside it**: the body's collider (`enabled: false`) so
324
+ the capsule stops holding the corpse up, the controller or the chase.
325
+ `Health.freeOnDeath` would delete the model the ragdoll is driving — leave
326
+ it off and let `lifetime` free the limbs, then free the corpse yourself on
327
+ the `reset` signal.
328
+ - `mass` (70 kg, shared out by limb), `damping` (3 — how fast the flailing
329
+ dies), `collide` (limbs against each other; adjacent ones never). A rig
330
+ without hands or a `HeadTop_End` still builds — the forearm ends at the
331
+ wrist bone, the head is a ball.
332
+ - **Headless there is no skeleton**: `activate(pose)` takes bone positions
333
+ (`{ Hips: [x, y, z], … }`), so a harness can build one where the model would
334
+ be and ask that it fell, that the elbow still joins, that the limbs carried
335
+ the run. `active` reads true while it is up.
336
+
337
+ ## Letting go — `enabled`
338
+
339
+ The player gets into a car, sits through a cutscene, opens a menu: the
340
+ controller has to LET GO, and `enabled: false` is that. For getting into a car or
341
+ onto a horse, the whole handoff — this controller off, the body parked and
342
+ riding along, the Skin on the saddle, the steed's controller or `Vehicle3D`
343
+ on, and back — is the `Mount` gameplay behaviour (`incanto-gameplay-behaviors.md`). Off, it reads no
344
+ input, applies no forces (no hover spring either), drives no camera and
345
+ sets no animation — the body under it is an ordinary `RigidBody3D` until it
346
+ is on again. A game parks the character out of sight beside it:
347
+
348
+ ```ts
349
+ ctl.enabled = false; // the controller lets go
350
+ player.enabled = false; // the body's collider is off — nothing bumps the empty seat
351
+ player.visible = false;
352
+ player.gravityScale = 0; // a collider-less dynamic body would fall through the floor
353
+ // …and each frame while driving: player.position = car.position
354
+ ```
355
+
356
+ and on the way out puts it back where the door is, `enabled`/`visible` on,
357
+ `gravityScale` 1, THEN `ctl.enabled = true` — the controller re-finds the
358
+ current camera and drives it from the next frame (the camera pops to the
359
+ rig; smooth it yourself if the cut shows). `Vehicle3D.enabled` is the same
360
+ switch on the car's side, and `examples/errands-3d` flips both with one
361
+ `Interactable` press.
362
+
363
+ ## Two players at one keyboard
364
+
365
+ A second player is a second ACTION SET, and a controller told to read it:
366
+
367
+ ```jsonc
368
+ // the scene's input map declares both
369
+ "input": {
370
+ "move": { "type": "vector2", "keys": { "up": ["KeyW"], "down": ["KeyS"], "left": ["KeyA"], "right": ["KeyD"] } },
371
+ "jump": { "type": "button", "keys": ["Space"] },
372
+ "move2": { "type": "vector2", "keys": { "up": ["ArrowUp"], "down": ["ArrowDown"], "left": ["ArrowLeft"], "right": ["ArrowRight"] } },
373
+ "jump2": { "type": "button", "keys": ["ShiftRight"] }
374
+ }
375
+ ```
376
+ ```json
377
+ { "name": "Controller", "type": "CharacterController3D",
378
+ "props": { "view": "quarter", "camera": "none",
379
+ "moveAction": "move2", "jumpAction": "jump2", "sprintAction": "sprint2" } }
380
+ ```
381
+
382
+ - `moveAction` / `jumpAction` / `sprintAction` have been there since the
383
+ controller shipped and no example had ever set them. They work: measured on
384
+ `examples/coop-3d`, player one's keys move smith one 2.4 m and smith two
385
+ **0.00 m**.
386
+ - **Every player's controller needs `camera: "none"`**, and one camera gets a
387
+ `GroupCamera` (see `incanto-gameplay-behaviors.md`) — otherwise two
388
+ controllers and the group camera all write the same camera every frame.
389
+ - Anything else a player operates takes the same treatment: `Carry.action`,
390
+ `Interactable.action`, `Shoot`'s action — name the second player's own.
391
+ - The playtest bot drives EVERY controller it finds, each on its own action set,
392
+ so a two-player game is exercised rather than half-exercised.
393
+
129
394
  ## Views
130
395
 
131
396
  | view | what it does | key props |
@@ -135,7 +400,21 @@ docstring's own advice: a raised `floatHeight` makes the character weightless).
135
400
  | `quarter` | fixed isometric pitch 35.264°, NO mouse look | `camDistance` 40, `mouseLook: false` |
136
401
  | `side` | camera at +z `camDistance`, lock movement to ±x | `mouseLook: false` |
137
402
 
138
- The controller drives the scene's `current` Camera3D every frame (smoothed).
403
+ The controller drives the scene's `current` Camera3D every frame (smoothed)
404
+ unless you take it off that duty:
405
+
406
+ ```json
407
+ { "name": "Controller", "type": "CharacterController3D",
408
+ "props": { "view": "quarter", "camera": "none" } }
409
+ ```
410
+
411
+ **`camera: "none"` leaves the camera exactly where the scene puts it.** A whole
412
+ shelf of games has a FIXED view — a board game, a bomb arena, an isometric
413
+ puzzle, a fixed-angle horror — and could not use this controller at all, because
414
+ it re-posed their camera every frame. Pair it with an authored `Camera3D` (and
415
+ a `FollowCamera` behaviour if the board scrolls); `incanto-check` no longer
416
+ calls that pair a fight. The default is `"drive"`, which is what every existing
417
+ scene already gets.
139
418
  Wheel zooms only when `zoomMax > zoomMin`.
140
419
 
141
420
  In `free` view the camera AIMS at the character every frame (`lookAt`), so the
@@ -160,7 +439,7 @@ enemy never yanks the camera in. `firstPerson` skips it (the camera is at the ey
160
439
 
161
440
  ## Animations
162
441
 
163
- `controller.state` is `idle | walk | run | fastRun | airborne`; the
442
+ `controller.state` is `idle | walk | run | fastRun | airborne | swim | climb | crouch | sneak | dash`; the
164
443
  `movementStateChanged` signal fires on transitions — map states to clips
165
444
  (`$animation` assets are retargeted onto the skin's own rig — GLB and VRM alike — so a
166
445
  clip never changes your character's proportions):
@@ -168,7 +447,8 @@ clip never changes your character's proportions):
168
447
  ```ts
169
448
  controller.on('movementStateChanged', (state) => {
170
449
  skin.animation = { idle: '$idle', run: '$run', fastRun: '$runFast',
171
- airborne: '$jump', walk: '$walk' }[state];
450
+ airborne: '$jump', walk: '$walk', swim: '$swim',
451
+ climb: '$climb' }[state];
172
452
  });
173
453
  ```
174
454
 
@@ -225,7 +505,7 @@ Map movement states straight in JSON — no behavior needed:
225
505
  { "name": "Controller", "type": "CharacterController3D",
226
506
  "props": { "view": "free",
227
507
  "animations": { "idle": "$idle", "walk": "$walk", "run": "$run",
228
- "fastRun": "$sprint", "airborne": "$jump" } } }
508
+ "fastRun": "$sprint", "airborne": "$jump", "swim": "$swim" } } }
229
509
  ```
230
510
 
231
511
  The controller writes `skin.animation` on every state change (crossfaded by
@@ -233,7 +513,7 @@ the model). The `movementStateChanged` signal still fires for extras.
233
513
 
234
514
  **Where those clips come from.** Locomotion clips live at
235
515
  `https://agent8-games.verse8.io/assets/3d/animations/mixamorig/<name>.glb`
236
- (`idle-00`, `walk`, `run-medium`, `run-fast`, `jump`) and play on any mixamorig
516
+ (`idle-00`, `walk`, `run-medium`, `run-fast`, `jump`, `swim`) and play on any mixamorig
237
517
  model as they are — retargeted through the humanoid map for a VRM. You do not
238
518
  have to assemble any of this by hand: run the character URL your asset MCP gave
239
519
  you through `bunx incanto-model <url>` and it prints the whole thing — assets,
@@ -292,6 +572,11 @@ NPCs feel alive when they track you. One node, zero code:
292
572
  - `weight` (0.85) is how far the head commits; `forwardAxis` defaults to the
293
573
  mixamo head convention (+z).
294
574
  - Also good for chests (`bone: "Spine2"`, small weight) and turrets.
575
+ - **`node.engaged`** (0..1) is how far it is committed RIGHT NOW — the whole
576
+ observable effect of this node, and until now nothing could read it. It turns
577
+ a rendered bone, so it stays 0 in a headless run (no GLB, no skeleton): check
578
+ it in the browser, where `attachedBone` tells you the bone was found and
579
+ `engaged` tells you the head is following.
295
580
 
296
581
  ## Facing a direction in 3D — the +Z-FORWARD rule (read before turning ANY skin)
297
582
 
@@ -383,6 +668,12 @@ This cannot be an `auditScene` warning: a scene file has no velocity, and the bu
383
668
  is a behavior writing a heading at runtime. `facingReport()` from `incanto/test`
384
669
  is the same check for your own tests.
385
670
 
671
+ Like the feel numbers above it, this presses your title screen's START before
672
+ driving the character — a paused world moves nowhere, and `did not move —
673
+ nothing to compare` was the answer it gave about three shipped games whose only
674
+ fault was that their menu was up. When nothing on screen starts the game it says
675
+ THAT instead.
676
+
386
677
  ## Facing the mouse in a top-down view (twin-stick)
387
678
 
388
679
  In `quarter` view the mouse does not look, and the controller turns the skin
@@ -404,29 +695,76 @@ Measured on `examples/survivor-3d`: a cursor 10 m east is yaw 90°, and a bolt
404
695
  given `direction: [dx, 0, dz]` flies where the skin looks. Boot with
405
696
  `pointer: { lockOnClick: false }` — a locked cursor is no aim at all.
406
697
 
407
- ## Lock-on and a dodge, in a few lines each
698
+ ## Lock-on (`lockOnAction`) and a dodge
408
699
 
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):
700
+ Lock-on used to be a one-line recipe the camera yaw toward the enemy, every
701
+ frame and the half it never had was the one that matters: the SKIN kept
702
+ facing the move direction, so a locked player circling a brute showed it a
703
+ shoulder, and a sword hitbox hung off the skin's yaw swung sideways. The
704
+ controller owns it now:
413
705
 
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
706
+ | prop | default | what it does |
707
+ |---|---|---|
708
+ | `lockOnAction` | `""` | the button that toggles the lock (`""` = no lock-on) |
709
+ | `lockGroup` | `"enemy"` | who can be locked: the nearest live, visible member within… |
710
+ | `lockRange` | `12` | …this many metres; the lock lets go by itself at 1.5 × the range, or when the target dies or leaves the tree |
711
+
712
+ ```json
713
+ { "name": "Controller", "type": "CharacterController3D",
714
+ "props": { "view": "free", "lockOnAction": "lockOn", "lockRange": 14 } }
418
715
  ```
419
716
 
420
- A dodge is a dash plus i-frames that start BEFORE the hit — `Health.protect`,
421
- not `invulnerableFor` (which opens only after one):
717
+ **Locked means the character FACES the target while its feet go anywhere.**
718
+ The camera yaw follows player target every frame, so the stick — which is
719
+ camera-relative — strafes round it; the skin turns to the target (at
720
+ `turnSpeed`) even standing still; `state` stays `walk`/`run` while circling.
721
+ Anything you hang off the skin's yaw — `examples/melee-3d`'s `Edge` hitbox at
722
+ `sin/cos(skin yaw)` — swings at the target. Methods `lock(node)`, `unlock()`,
723
+ `toggleLock()`; readback `lockTarget`; signal `lockChanged(target | null)`.
724
+ `examples/boss-3d` locks onto the colossus with `lockGroup: "boss"`,
725
+ `lockRange: 40`.
726
+
727
+ `incanto-feel` knows: a controller with a `lockTarget` is reported as
728
+ `LOCKED ON` rather than measured against its travel — a strafe is the
729
+ intended reading, not a defect.
730
+
731
+ A dodge is a dash plus i-frames that start BEFORE the hit. The dash is the
732
+ controller's (`dashSpeed` — see below); the i-frames are `Health.protect`,
733
+ not `invulnerableFor` (which opens only after one), hung on the `dashed`
734
+ signal:
422
735
 
736
+ ```json
737
+ { "name": "Ctl", "type": "CharacterController3D",
738
+ "props": { "dashSpeed": 9, "dashSeconds": 0.35, "dashCooldown": 0.5 } }
739
+ ```
423
740
  ```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];
741
+ controller.on('dashed', () => (player.behavior as Health).protect(0.35));
426
742
  ```
427
743
 
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.
744
+ ## A dash / dodge roll (`dashSpeed`)
745
+
746
+ Two examples carried the same twelve lines — a timer, a direction, and
747
+ `body.linearVelocity = dir × 9` written EVERY frame because the controller
748
+ brakes toward its own pace and a single impulse dies — while
749
+ `CharacterController2D` had `dashSpeed` since 0.33. Now the 3D controller
750
+ has it too:
751
+
752
+ | prop | default | what it does |
753
+ |---|---|---|
754
+ | `dashSpeed` | `0` | m/s for the roll; `0` = no dash |
755
+ | `dashSeconds` | `0.25` | how long the velocity is held |
756
+ | `dashCooldown` | `0` | seconds after a roll ends before the next can start |
757
+ | `dashAction` | `dash` | pressed = roll (tolerated when missing, like `sprint`) |
758
+ | `dashBackstep` | `false` | with no stick, roll AWAY from where the skin faces — the Souls-like backstep a lock-on game wants |
759
+
760
+ - The roll goes where the stick points (camera-relative), or with no stick
761
+ where the skin faces — or away from it with `dashBackstep`, which is what
762
+ a neutral roll means while locked on to something.
763
+ - `state` reads `dash` for the roll — map a clip to it — and `dashed` fires
764
+ once per roll. Not while crouched, on a ladder or swimming.
765
+ - The velocity is written last, every step of the roll, over whatever the
766
+ step's impulses did; gravity still runs, so a roll off a ledge falls.
767
+ - `examples/melee-3d` rolls with it and keeps its i-frames on `dashed`.
430
768
 
431
769
  ## First-person weapon viewmodel + the tracer-from-the-muzzle rule
432
770
 
@@ -226,8 +226,30 @@ non-WebAudio paths use, so headless gameplay is unaffected.
226
226
  > (`refDistance: 100`, `maxDistance: 900` is a reasonable start); `incanto check`
227
227
  > warns when it sees `spatial: true` in a 2D scene with the defaults left alone.
228
228
 
229
- > **Headless / verify VM:** spatial is a no-op like all audio `play()` won't
230
- > throw and your gameplay logic is unaffected.
229
+ > **Headless / verify VM:** no sound comes out and the ANSWER still does. The
230
+ > audio record carries where each spatial sound arrived from, computed from the
231
+ > scene's own current camera when no renderer has fed a pose:
232
+
233
+ ```ts
234
+ const ring = engine.audio.recent().at(-1);
235
+ ring.pan; // −1 hard left … +1 hard right (absent when the sound is not spatial)
236
+ ring.gain; // 0..1 after the distance rolloff — 1 inside `refDistance`
237
+ ring.distance; // metres (px in 2D) from the listener when it sounded
238
+ ```
239
+
240
+ Thirty shipped scenes set `spatial: true` before this existed and not one of
241
+ them could be checked: the record said a sound FIRED and nothing about whether
242
+ it was audible or which side it came from. Now "walk toward the ringing" is a
243
+ mechanic a harness can verify — `examples/earshot-3d` is a whole game of it
244
+ (a bell on the left records `pan −0.98`; turn the listener around and the same
245
+ bell reads `+0.98`).
246
+
247
+ Two things to know when you measure it:
248
+
249
+ - **A sound that is not spatial has no `pan`/`gain`/`distance` at all** — the
250
+ fields are absent rather than zero, so a UI click never looks centred.
251
+ - **A scene with no current camera has no listener**, and the record says
252
+ nothing rather than inventing a sound in the middle of your head.
231
253
 
232
254
  ---
233
255
 
@@ -216,7 +216,8 @@ ALWAYS confirm in the browser (`bun run dev`) that the feet touch the surface.
216
216
  | `smoothing` | `0` | 0 = snap; 0.85–0.95 = smooth chase (frame-rate independent) |
217
217
  | `zoom` | `1` | 2 = world pixels doubled |
218
218
  | `limits` | `[]` | `[minX,minY,maxX,maxY]` — view rect clamped inside |
219
- | `current` | `false` | mark exactly one |
219
+ | `current` | `false` | mark exactly one — switch at runtime with `cam.makeCurrent()`, which clears the others (a wirable handler) |
220
+ | `blendSeconds` | `0` | seconds the view FLIES (centre and zoom, eased) from the last drawn view to this camera when it becomes current; `0` = a cut; `makeCurrent(0.5)` asks once |
220
221
  Camera `position` is the view CENTER, and it composes with its ancestors like any
221
222
  other node — so parenting the camera to the player (the Godot/Phaser idiom) works,
222
223
  and so does putting it inside the container that holds the level. `follow` and the
@@ -346,6 +347,12 @@ between a readable and an unusable phone build.
346
347
 
347
348
  ## Game flow recipes
348
349
 
350
+ - **A title screen that HOLDS the world**: `GameFlow.pause()` from a behaviour's
351
+ `onReady`, and `resume()` when the player presses your RUN button. The freeze
352
+ survives the boot — the tree readies inside `loadScene` and `setScene` resets
353
+ the time scale for the game-over swap, so a pause asked for during the load
354
+ used to be erased and the game booted with `state: 'paused'` and the player
355
+ walking around behind the title. `examples/shell-3d` is the whole shell.
349
356
  - **Scene transition** (level 2, title → game):
350
357
  ```ts
351
358
  import { loadScene } from 'incanto';
@@ -545,6 +552,8 @@ behaves exactly as before.
545
552
  | `dashSpeed` / `dashSeconds` / `dashAction` | `0` / `0.15` / `dash` | a straight-line dash |
546
553
  | `wallSlideSpeed` | `0` | capped fall while against a wall |
547
554
  | `wallJumpImpulse` | `[0, 0]` | `[away, up]` push off a wall |
555
+ | `crouchHeight` | `0` | the collider's height (px) while `crouch` is held — a rect's `size[1]`, a capsule's `height`; the body drops by half the difference so the feet stay put, a low gap is ducked through, and a ceiling keeps you down until you are out from under it. Hold-to-crouch: a jump press under a held key is not a hop |
556
+ | `crouchSpeedMultiplier` / `crouchAction` | `0.5` / `crouch` | the pace while crouched; the action (tolerated when missing) |
548
557
 
549
558
  ```json
550
559
  { "name": "Controller", "type": "CharacterController2D",
@@ -553,8 +562,18 @@ behaves exactly as before.
553
562
  "wallSlideSpeed": 60, "wallJumpImpulse": [260, 380] } }
554
563
  ```
555
564
 
565
+ **Getting hit is the controller's too.** A `Health` on the body (or a child of
566
+ it) with `knockback` / `knockUp` / `staggerSeconds` takes the stick and the jump
567
+ for the stagger, and the controller writes the kick as the body's velocity — in
568
+ topDown as the whole velocity, in platformer as `x` with `knockUp` as the hop
569
+ (pass it positive; the engine flips it to `−y`). The platformer's hand-rolled
570
+ `KNOCK_X` / `KNOCK_UP` / `CONTROL_LOCK` are `knockback 420, knockbackSeconds
571
+ 0.22, knockUp 360, staggerSeconds 0.22`; a controller of your own reads the
572
+ kick itself (`applyKnockback: false`) — see "Hit reactions" in
573
+ `incanto-gameplay-behaviors.md`.
574
+
556
575
  **`movementStateChanged(state)`** fires on every change — `idle` · `run` ·
557
- `jump` · `fall` · `wallSlide` · `dash` — so an `AnimatedSprite2D` follows the
576
+ `jump` · `fall` · `wallSlide` · `dash` · `crouch` · `sneak` — so an `AnimatedSprite2D` follows the
558
577
  character from scene JSON instead of a behavior polling velocities. The 3D
559
578
  sibling has had this signal since it shipped.
560
579
 
@@ -58,7 +58,10 @@ One call does register → load → physics → input → renderer → start. Ph
58
58
  disables); `pointer: { lockOnClick: false }` for look without lock. `seed` /
59
59
  `fixedHz` / `pixelRatio` / `adaptiveResolution` pass through (adaptive
60
60
  resolution is ON by default: slow frames render fewer pixels and recover
61
- automatically — pass `false` for screenshots and pixel-diff tests). The manual `Engine` + `loadScene` +
61
+ automatically — pass `false` for screenshots and pixel-diff tests). `autoStart: false`
62
+ hands back a game that has not run a frame — call `game.engine.start()` once
63
+ everything that must exist before the first one does (a `NetworkManager`; see
64
+ `incanto-multiplayer.md`). The manual `Engine` + `loadScene` +
62
65
  `Renderer3D` boot still exists for full control.
63
66
 
64
67
  Imperative per-frame logic can hang off `game.engine.updated.connect((dt) => { ... })`; for
@@ -281,9 +284,41 @@ and `Fill.position = [-(1 - frac) / 2, 0, 0.01]`. The offsets stay local to the
281
284
  (rotated) bar, so the drain always hugs the bar's left edge.
282
285
 
283
286
  ### `Camera3D`
284
- `fov: 60`, `near: 0.1`, `far: 1000`, `current: false`, `lookAt: ""`.
287
+ `fov: 60`, `near: 0.1`, `far: 1000`, `current: false`, `lookAt: ""`, `blendSeconds: 0`,
288
+ `minHorizontalFov: 0`.
285
289
  Mark exactly ONE camera `current: true` (otherwise the first camera in tree order is used).
286
- Aspect ratio is automatic.
290
+
291
+ **`fov` is VERTICAL**, and the aspect comes from the canvas — so what the player
292
+ sees ACROSS is `2·atan(tan(fov/2)·aspect)`. A `fov: 60` camera shows 91° across a
293
+ 16:9 window and **30° across a 390×844 phone held in portrait**.
294
+ `minHorizontalFov` is the floor: ask for a width in degrees and the vertical fov
295
+ widens until the screen carries it, while any window already wider keeps the
296
+ authored fov. `52` suits a game meant to be playable in one hand — and note what
297
+ the geometry costs: 52° across a 9:19.5 screen is a 93° vertical fov, so ask for
298
+ a width, not for everything.
299
+
300
+ `cam.viewFov(aspect)` answers what the camera will actually render with at a
301
+ given shape of screen — the authored `fov`, or the widened one. Use it in a
302
+ harness rather than copying the formula:
303
+
304
+ ```ts
305
+ const cam = game.scene.root.getNode('Camera') as Camera3D;
306
+ cam.viewFov(390 / 844); // 93 — the vertical fov a portrait phone gets
307
+ cam.viewFov(16 / 9); // 60 — the authored one, on a desktop window
308
+ ```
309
+ To SWITCH at runtime call `cam.makeCurrent()` — it clears every other camera's
310
+ `current`, which setting your own never did, so a cutscene camera that set
311
+ itself `current` changed nothing visible. It is a handler a connection can name:
312
+ `triggerEnter → DoorCam.makeCurrent`. Aspect ratio is automatic.
313
+
314
+ **A switch is a CUT unless the camera asks otherwise.** `blendSeconds` on the
315
+ camera being switched TO makes the picture FLY from the last drawn pose to that
316
+ camera over so many seconds of real time — position, orientation and fov, eased
317
+ in and out — and `makeCurrent(0.6)` asks for it once. The nodes never move:
318
+ picking, framing and the editor read the node; only the picture takes the long
319
+ way round. A second switch mid-flight starts from where the picture IS.
320
+ `examples/heist-3d`'s door camera and gameplay camera declare `0.6`; a camera
321
+ that is the FIRST frame (an intro rail) has nothing to fly from and cuts.
287
322
 
288
323
  **Point it with `lookAt`, not with a `rotation` you worked out.**
289
324
 
@@ -134,12 +134,23 @@ Opens a local page (default `http://127.0.0.1:5179/`) with three panes:
134
134
  node in the scene; the handler is a dropdown of real methods when the target
135
135
  has no script, and free text when it does (those handlers live in your
136
136
  TypeScript, which the editor never loads). An end that no longer resolves is
137
- marked red with the reason. For what a row cannot express — `filter` and `once`
138
- — the scene header still has `connections` as raw JSON. (It used to say "bound
139
- args" as well: a connection has never had them. Its keys are exactly
140
- `[signal, from, to, handler, once, filter]`, and since they became a closed
141
- set an invented one is a load error rather than a key that quietly does
142
- nothing.)
137
+ marked red with the reason. For what a row cannot express — `filter`, `once`
138
+ and `args` — the scene header still has `connections` as raw JSON. A
139
+ connection's keys are exactly `[signal, from, to, handler, once, filter,
140
+ args]`, a closed set, so an invented one is a load error rather than a key
141
+ that quietly does nothing. **`args` survives editing but is not shown**: a row
142
+ whose wire carries bound arguments reads as `phase2 → HUD/Banner.show` with no
143
+ hint of the words it says.
144
+
145
+ **Re-pointing a target keeps your handler.** A handler the new target does not
146
+ have is replaced (the row would otherwise stay green here and hard-fail at
147
+ load), and the row remembers what it started with — so changing your mind and
148
+ putting the target back puts the handler back. Before that, a round trip left
149
+ `phase2 → HUD/Banner.show` as `HUD/Banner.childNames`: the replacement took
150
+ the target's first offer, and the offers included the tree plumbing every node
151
+ inherits. They do not any more — the node base class is read the way the
152
+ behavior base class always was, so `queueFree` stays and `childNames`,
153
+ `wiringPath` and the rest are gone.
143
154
  **Validation now runs WITH connections**: the editor used to strip them before
144
155
  loading, so it reported green for a scene the game hard-fails to open.
145
156
  - **Viewport with DIRECT MANIPULATION** — a mode toolbar (top left) + the